Donne à l'utilisateur la surface pour activer le serveur depuis l'app. - `features/settings/` : `SettingsView`, `DeploymentSettings`, `useDeployment`. - `adapters/desktopServer.ts` + port `DesktopServerGateway` et DTO associés ; adapters mock et http/unsupported alignés (le mode web n'expose pas le contrôle du serveur qui l'héberge). - `ProjectsView` : le `showSettings: boolean` devient une navigation interne `AI Profiles` / `Deployment`. Le libellé alternant « Close AI Profiles » disparaît — Settings existait déjà dans cette vue, la surface évolue au lieu d'ajouter un `PanelId`. CORRECTION D'UN BRIEF FAUX, remontée spontanément par DevFrontend et qui mérite de survivre : le cadrage décrivait le mode `remoteProxyOtherMachine` avec deux champs. `validate_settings` en exige un troisième, `lanBindAddress`, et rejette loopback comme unspecified. Construit selon la spec, chaque save et chaque start en mode 3 aurait échoué — le lot serait parti vert et cassé. Le panneau expose donc un select alimenté par `candidateLanAddresses` fourni par le backend : la règle « le frontend n'invente jamais une IP » tient. QA ré-exécutée par Git avant merge : `npm run typecheck` exit 0 · `npx vitest run` 87 files / 789 passed, 0 échec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
4.4 KiB
TypeScript
122 lines
4.4 KiB
TypeScript
/**
|
|
* Desktop-only surfaces that have no web equivalent in V1 (ticket #13, lot F1):
|
|
* the native folder picker, OS view-windows and the main↔detached focused-project
|
|
* channel, plus SSH/WSL remote. On the web client these must fail with a clear,
|
|
* explicit error rather than reaching for Tauri (which is absent).
|
|
*
|
|
* The B0 inventory classified these as `desktop-only`; F1 does NOT implement them
|
|
* for web. If/when the product wants an equivalent (e.g. a server-side folder
|
|
* browser to replace `pickFolder`), it gets its own lot.
|
|
*/
|
|
|
|
import type {
|
|
EmbeddedServerStatus,
|
|
GatewayError,
|
|
ServerExposurePreview,
|
|
ServerExposureSettings,
|
|
Unsubscribe,
|
|
} from "@/domain";
|
|
import type {
|
|
DesktopServerGateway,
|
|
FocusedProject,
|
|
FocusedProjectGateway,
|
|
RemoteGateway,
|
|
ViewWindowClosed,
|
|
ViewWindowSnapshot,
|
|
WindowGateway,
|
|
} from "@/ports";
|
|
|
|
/** Throws a stable, explicit "unsupported on web" {@link GatewayError}. */
|
|
export function unsupportedOnWeb(what: string): never {
|
|
const err: GatewayError = {
|
|
code: "UNSUPPORTED_ON_WEB",
|
|
message: `${what} is not available in the web client (desktop-only).`,
|
|
};
|
|
throw err;
|
|
}
|
|
|
|
/** Web stub: OS view-windows are desktop-only (no `WebviewWindow` on the web). */
|
|
export class WebWindowGateway implements WindowGateway {
|
|
openViewWindow(): Promise<void> {
|
|
return unsupportedOnWeb("Detaching a panel into an OS window");
|
|
}
|
|
closeViewWindow(): Promise<void> {
|
|
return unsupportedOnWeb("Closing a detached OS window");
|
|
}
|
|
listOpenViewWindows(): Promise<ViewWindowSnapshot[]> {
|
|
// Best-effort by contract (callers treat rejection as "no reconciliation"),
|
|
// but there are simply never detached OS windows on web → empty list.
|
|
return Promise.resolve([]);
|
|
}
|
|
onViewWindowClosed(
|
|
_handler: (event: ViewWindowClosed) => void,
|
|
): Promise<Unsubscribe> {
|
|
// No OS windows ⇒ nothing ever fires; return a no-op unsubscribe.
|
|
return Promise.resolve(() => {});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Web stub for the focused-project channel. With no detached windows on web the
|
|
* channel has no cross-window consumer; F1 keeps it inert (no publish, no focus).
|
|
* A future multi-tab web build could back this with `BroadcastChannel`.
|
|
*/
|
|
export class WebFocusedProjectGateway implements FocusedProjectGateway {
|
|
setFocusedProject(_project: FocusedProject | null): Promise<void> {
|
|
// No-op: nothing reads the channel on web V1.
|
|
return Promise.resolve();
|
|
}
|
|
getFocusedProject(): Promise<FocusedProject | null> {
|
|
return Promise.resolve(null);
|
|
}
|
|
onFocusedProjectChanged(
|
|
_handler: (project: FocusedProject | null) => void,
|
|
): Promise<Unsubscribe> {
|
|
return Promise.resolve(() => {});
|
|
}
|
|
}
|
|
|
|
/** Web stub: SSH/WSL remote connection is desktop-only in V1. */
|
|
export class WebRemoteGateway implements RemoteGateway {
|
|
connect(): Promise<void> {
|
|
return unsupportedOnWeb("Remote (SSH/WSL) connection");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Web stub: the embedded server is desktop-only (ticket #68). A web client is
|
|
* *served by* this very server, so letting it reconfigure or stop the server
|
|
* would let a remote device saw off the branch it sits on. The desktop app owns
|
|
* this surface; every call fails explicitly rather than reaching for Tauri.
|
|
*/
|
|
export class WebDesktopServerGateway implements DesktopServerGateway {
|
|
// `async` on purpose: these must *reject* rather than throw synchronously, so
|
|
// callers using `.then(ok, err)` (not just `await`) still see the failure.
|
|
async getExposureSettings(): Promise<ServerExposureSettings> {
|
|
return unsupportedOnWeb("Embedded server settings");
|
|
}
|
|
async saveExposureSettings(_settings: ServerExposureSettings): Promise<void> {
|
|
return unsupportedOnWeb("Embedded server settings");
|
|
}
|
|
async previewExposure(
|
|
_settings: ServerExposureSettings,
|
|
): Promise<ServerExposurePreview> {
|
|
return unsupportedOnWeb("Embedded server settings");
|
|
}
|
|
async status(): Promise<EmbeddedServerStatus> {
|
|
return unsupportedOnWeb("Embedded server status");
|
|
}
|
|
async start(): Promise<EmbeddedServerStatus> {
|
|
return unsupportedOnWeb("Starting the embedded server");
|
|
}
|
|
async stop(): Promise<EmbeddedServerStatus> {
|
|
return unsupportedOnWeb("Stopping the embedded server");
|
|
}
|
|
onStatusChanged(
|
|
_handler: (status: EmbeddedServerStatus) => void,
|
|
): Promise<Unsubscribe> {
|
|
// Never fires on web; a no-op unsubscribe keeps callers' teardown honest.
|
|
return Promise.resolve(() => {});
|
|
}
|
|
}
|