Skip to content

Commit 29c0d01

Browse files
authored
Merge pull request #397 from orbyn-dev/refactor/web-api-operations
refactor(web): move feature API calls behind named operations
2 parents 78b1e94 + 22c8e27 commit 29c0d01

46 files changed

Lines changed: 526 additions & 246 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/refactor.md‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ Notes:
106106
| W1 | Dead code and shared helpers | done |
107107
| W2 | CSS ownership: no class defined twice | open, needs the owner's go-ahead |
108108
| W3 | Break feature cycles; `features/content` and `index.ts` front doors | done |
109-
| W4 | Per-feature `api.ts` and hooks | in progress: front doors done |
109+
| W4 | Per-feature `api.ts` and hooks | done |
110110
| W5 | Giant components into sub-folders (composer, trail, sidebar, panels, settings, pulls, usage) | done |
111111
| W6 | Naming pass and `web/README.md` | `web/README.md` exists (W7a); the naming pass is open |
112112
| W7a | Product and generic tiers for shared web code (`product/`) | done |
@@ -220,12 +220,10 @@ The backend (B) and web (W) tracks can run in parallel.
220220
transport and its tests use and `source_control::git` only `is_git_repo`; the rest is
221221
`pub(crate)`, with no call site changed.
222222

223-
- W4 (per-feature API doors): started. Every feature now has `api.ts`: it re-exports the
224-
client pieces that feature uses, and 119 files import it instead of `api/`. Value imports
225-
from `api/client`, `shell`, `pulls`, `sse`, `token` and `origin` in feature code went from
226-
158 to 0; type-only imports of `api/types` stay, since they carry no call. Named
227-
operations and hooks are next: the maintenance page, `BrowserPanel`, the terminal drawer
228-
and the settings pages still call `request(...)` with endpoint paths through that door.
223+
- W4 (per-feature API doors): done. Every feature has one `api.ts` with named operations;
224+
106 raw `request(...)` calls across 43 files moved behind it, so no feature file imports
225+
the transport or builds an endpoint path. Type-only `api/types` imports stay, since they
226+
carry no call. Every operation keeps its original path, method and payload.
229227

230228
- W7a (product and generic tiers): done for the shared folders; front-door helpers are open (see
231229
the audit below). `state/` became `product/state/`, and `lib/timeline` became `product/timeline`.
@@ -308,7 +306,6 @@ have no baseline.
308306
| Unit | Work | Size and notes |
309307
| --- | --- | --- |
310308
| W2 | CSS ownership: `styles/thread.css` (about 4,500 lines, mostly panels and onboarding rules), `pages.css`, `shell.css`, `base.css` move next to the components they style; shared pieces (`btn`, `dialog`, `pill`, `empty` and the like, defined in up to 10 files) are defined once in `ui/` | **Needs the owner's go-ahead.** The tests cannot see a changed cascade. Compare screenshots before and after in Light, Dark and Dim for every moved rule, and keep the CSS load order identical (compare the first-import order from `main.tsx`, as the W7a review did). |
311-
| W4 | Per-feature `api.ts` and hooks: named operations replace the raw `request(...)` calls the feature front doors still re-export | Web. Front doors are in (119 files). Next: Maintenance, `BrowserPanel`, the terminal drawer and the settings pages. |
312309
| W6 | Naming pass: Page is a route, Panel is a side-panel tab, Dialog is a modal, no "Pane" (`SurfacePane`, `SideChatPane`, `PullsPage` against `PullPage`); keep `web/README.md` in step | Do after W2 so files are renamed once. |
313310
| Front-door TODO rows | Hoist the helper exports in the audit table (one other feature uses each) to `product/` or `lib/` when a second feature needs them; hoist `copyText` to `lib/` | Opportunistic. |
314311
| Closeout | Turn any remaining baselined rule into a hard error, run a packaged-app build and walk-through (`cd desktop && npm run build -- --bundles app`), run the worktree cleanup report | The packaged-app check has not been run since the logo, tools, picker and tab strip merged into main. |

‎web/src/features/onboarding/WelcomeWizard.tsx‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { Check, Copy, RefreshCw } from "lucide-react";
22
import { useEffect, useState } from "react";
3-
import { api, request } from "./api";
3+
import { api, queryPreferences, updatePreferences } from "./api";
44
import { toFailure, useAsync } from "../../lib/useAsync";
5-
import type { ScopedPreferencesResult } from "../../generated/ScopedPreferencesResult";
65
import type { EngineStatus, PermissionMode, Project } from "../../api/types";
76
import { AddProjectPalette } from "../workspace";
87
import { ACCESS, offeredModes, shownAccess } from "../thread";
@@ -145,7 +144,7 @@ function useDefaultAccess() {
145144
const [revision, setRevision] = useState<{ version: string; row: string } | null>(null);
146145
useEffect(() => {
147146
const abort = new AbortController();
148-
request<ScopedPreferencesResult>("POST", "/api/settings/preferences/query", { scope: {}, keys: ["default_permission_mode"] }, abort.signal).then(
147+
queryPreferences({ scope: {}, keys: ["default_permission_mode"] }, abort.signal).then(
149148
(result) => {
150149
const row = result.rows?.find((entry) => entry.key === "default_permission_mode");
151150
if (!row || abort.signal.aborted) return;
@@ -158,7 +157,7 @@ function useDefaultAccess() {
158157
}, []);
159158
const save = async (mode: PermissionMode) => {
160159
if (!revision) return;
161-
await request<ScopedPreferencesResult>("PUT", "/api/settings/preferences", {
160+
await updatePreferences({
162161
scope: {},
163162
membership_version: revision.version,
164163
if_match: { default_permission_mode: revision.row },

‎web/src/features/onboarding/api.ts‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
11
// The one door this feature uses to reach the API client. Named operations and
2-
// hooks move here as they are extracted; until then it re-exports what the feature
3-
// already calls, so no file in the feature imports `api/` directly.
4-
export { api, request } from "../../api/client";
2+
// hooks live here; no file in the feature calls the transport directly.
3+
import { request } from "../../api/client";
4+
import type { PreferenceView } from "../../api/types";
5+
import type { ScopedPreferencesResult } from "../../generated/ScopedPreferencesResult";
6+
7+
const q = encodeURIComponent;
8+
9+
export function readPreferences(projectId: string | null = null, signal?: AbortSignal) {
10+
const path = projectId === null ? "/api/preferences" : `/api/preferences?project_id=${q(projectId)}`;
11+
return request<PreferenceView[]>("GET", path, undefined, signal);
12+
}
13+
14+
export function queryPreferences(body: unknown, signal?: AbortSignal) {
15+
return request<ScopedPreferencesResult>("POST", "/api/settings/preferences/query", body, signal);
16+
}
17+
18+
export function updatePreferences(body: unknown) {
19+
return request<ScopedPreferencesResult>("PUT", "/api/settings/preferences", body);
20+
}
21+
22+
export { api } from "../../api/client";

‎web/src/features/onboarding/useDraftCheckout.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useState } from "react";
2-
import { request } from "./api";
3-
import type { PreferenceView, Project } from "../../api/types";
2+
import { readPreferences } from "./api";
3+
import type { Project } from "../../api/types";
44
import { toFailure } from "../../lib/useAsync";
55

66
/** Defaults belong to the selected physical checkout; explicit draft picks win. */
@@ -16,7 +16,7 @@ export function useDraftCheckout(project: Pick<Project, "id" | "is_git"> | null,
1616
if (!scope || !git) return;
1717
const controller = new AbortController();
1818
// A device setting: a project never overrides where new threads start.
19-
request<PreferenceView[]>("GET", "/api/preferences", undefined, controller.signal)
19+
readPreferences(null, controller.signal)
2020
.then((views) => {
2121
if (controller.signal.aborted) return;
2222
const value = views.find((view) => view.key === "default_thread_env_mode")?.value;

‎web/src/features/onboarding/useDraftModelDefaults.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useState } from "react";
2-
import { request } from "./api";
2+
import { readPreferences } from "./api";
33
import type { EngineStatus, PreferenceView } from "../../api/types";
44
import { useUi } from "../../product/state/ui";
55
import { toFailure } from "../../lib/useAsync";
@@ -32,8 +32,7 @@ export function useDraftModelDefaults(projectId: string | null, engines: EngineS
3232
useEffect(() => {
3333
const controller = new AbortController();
3434
setLoaded(null);
35-
const path = projectId === null ? "/api/preferences" : `/api/preferences?project_id=${encodeURIComponent(projectId)}`;
36-
request<PreferenceView[]>("GET", path, undefined, controller.signal).then((views) => {
35+
readPreferences(projectId, controller.signal).then((views) => {
3736
if (!controller.signal.aborted) setLoaded({ scope: projectId, defaults: readDefaults(views), error: null });
3837
}).catch((error: unknown) => {
3938
if (!controller.signal.aborted) setLoaded({ scope: projectId, defaults: null,

‎web/src/features/panels/api.ts‎

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,148 @@
11
// The one door this feature uses to reach the API client. Named operations and
2-
// hooks move here as they are extracted; until then it re-exports what the feature
3-
// already calls, so no file in the feature imports `api/` directly.
4-
export { ApiFailure, api, authHeaders, request } from "../../api/client";
2+
// hooks live here; no file in the feature calls the transport directly.
3+
import { request, request as clientRequest } from "../../api/client";
4+
import type {
5+
BrowserProfile,
6+
FileContent,
7+
FileEntry,
8+
LocalServer,
9+
PickedElement,
10+
PreviewTab,
11+
ProjectFileView,
12+
SearchHit,
13+
TerminalInfo,
14+
} from "../../api/types";
15+
import type { PreviewCopy } from "../../generated/PreviewCopy";
16+
17+
const q = encodeURIComponent;
18+
const thread = (threadId: string) => `/api/threads/${q(threadId)}`;
19+
const previews = (threadId: string) => `${thread(threadId)}/previews`;
20+
const preview = (threadId: string, tabId: string) => `${previews(threadId)}/${q(tabId)}`;
21+
22+
export type PreviewSize = { width: number; height: number };
23+
export type PreviewView = { zoom?: number; appearance?: string; mobile?: boolean };
24+
export type PreviewInput = { kind: string; x?: number; y?: number; delta_y?: number; text?: string };
25+
26+
export function listPreviewTabs(threadId: string, signal?: AbortSignal) {
27+
return request<PreviewTab[]>("GET", previews(threadId), undefined, signal);
28+
}
29+
30+
export function openPreview(threadId: string, body: { url: string; width: number; height: number; private?: boolean; profile?: string }) {
31+
return request<PreviewTab>("POST", previews(threadId), body);
32+
}
33+
34+
export function navigatePreview(threadId: string, tabId: string, url: string) {
35+
return request("POST", `${preview(threadId, tabId)}/navigate`, { url });
36+
}
37+
38+
export function changePreviewView(threadId: string, tabId: string, change: PreviewView) {
39+
return request("POST", `${preview(threadId, tabId)}/view`, change);
40+
}
41+
42+
export function reloadPreview(threadId: string, tabId: string, hard: boolean) {
43+
return request("POST", `${preview(threadId, tabId)}/reload`, { hard });
44+
}
45+
46+
export function clearPreview(threadId: string, tabId: string, what: { cookies?: boolean; cache?: boolean }) {
47+
return request("POST", `${preview(threadId, tabId)}/clear`, what);
48+
}
49+
50+
export function resizePreview(threadId: string, tabId: string, size: PreviewSize) {
51+
return request("POST", `${preview(threadId, tabId)}/resize`, size);
52+
}
53+
54+
export function popOutPreview(threadId: string, tabId: string) {
55+
return request("POST", `${preview(threadId, tabId)}/window`);
56+
}
57+
58+
export function stepPreviewHistory(threadId: string, tabId: string, back: boolean) {
59+
return request("POST", `${preview(threadId, tabId)}/history`, { back });
60+
}
61+
62+
export function closePreview(threadId: string, tabId: string) {
63+
return request("DELETE", preview(threadId, tabId));
64+
}
65+
66+
export function setPreviewControl(threadId: string, tabId: string, human: boolean) {
67+
return request("POST", `${preview(threadId, tabId)}/control`, { human });
68+
}
69+
70+
export function sendPreviewInput(threadId: string, tabId: string, body: PreviewInput) {
71+
return request("POST", `${preview(threadId, tabId)}/input`, body);
72+
}
73+
74+
export function pickPreviewElement(threadId: string, tabId: string, point: { x: number; y: number }) {
75+
return request<PickedElement>("POST", `${preview(threadId, tabId)}/pick`, point);
76+
}
77+
78+
export function copyPreview(threadId: string, tabId: string) {
79+
return request<PreviewCopy>("POST", `${preview(threadId, tabId)}/copy`);
80+
}
81+
82+
export function previewRecordingEndpoint(threadId: string, tabId: string) {
83+
return `${preview(threadId, tabId)}/recording`;
84+
}
85+
86+
/** The transport a recording picks between writes and reads with. */
87+
export const previewTransport = clientRequest;
88+
89+
export function choosePreviewFiles(threadId: string, tabId: string, files: string[]) {
90+
return request("POST", `${preview(threadId, tabId)}/chooser`, { files });
91+
}
92+
93+
export function listBrowserProfiles(signal?: AbortSignal) {
94+
return request<BrowserProfile[]>("GET", "/api/browser-profiles", undefined, signal);
95+
}
96+
97+
export function listThreadServers(threadId: string, signal?: AbortSignal) {
98+
return request<LocalServer[]>("GET", `${thread(threadId)}/servers`, undefined, signal);
99+
}
100+
101+
export function stopThreadServer(threadId: string, pid: number) {
102+
return request<void>("POST", `${thread(threadId)}/servers/${pid}/stop`);
103+
}
104+
105+
export function readThreadFile(threadId: string, path: string, signal?: AbortSignal) {
106+
return request<FileContent>("GET", `${thread(threadId)}/files/content?path=${q(path)}`, undefined, signal);
107+
}
108+
109+
export function saveThreadFile(threadId: string, body: { path: string; content: string; base_revision: string | null }) {
110+
return request("PUT", `${thread(threadId)}/files/content`, body);
111+
}
112+
113+
export function listThreadFiles(threadId: string, path: string, signal?: AbortSignal) {
114+
return request<FileEntry[]>("GET", `${thread(threadId)}/files?path=${q(path)}`, undefined, signal);
115+
}
116+
117+
export function searchThreadFiles(threadId: string, query: string, signal?: AbortSignal) {
118+
return request<SearchHit[]>("GET", `${thread(threadId)}/files/search?q=${q(query)}&content=true`, undefined, signal);
119+
}
120+
121+
export function openThreadFileInBrowser(threadId: string, path: string) {
122+
return request("POST", `${thread(threadId)}/previews/file`, { path });
123+
}
124+
125+
export function listTerminals(threadId: string, signal?: AbortSignal) {
126+
return request<TerminalInfo[]>("GET", `${thread(threadId)}/terminals`, undefined, signal);
127+
}
128+
129+
export function startTerminal(threadId: string, body: unknown) {
130+
return request<TerminalInfo>("POST", `${thread(threadId)}/terminals`, body);
131+
}
132+
133+
export function startConsole(threadId: string) {
134+
return request<TerminalInfo>("POST", `${thread(threadId)}/console`, {});
135+
}
136+
137+
export function closeTerminal(threadId: string, terminalId: string) {
138+
return request("DELETE", `${thread(threadId)}/terminals/${q(terminalId)}`);
139+
}
140+
141+
export function listThreadScripts(threadId: string, signal?: AbortSignal) {
142+
return request<ProjectFileView>("GET", `${thread(threadId)}/scripts`, undefined, signal);
143+
}
144+
145+
export { ApiFailure, api, authHeaders } from "../../api/client";
5146
export { serviceSocketUrl, serviceUrl } from "../../api/origin";
6147
export { pullsApi } from "../../api/pulls";
7148
export { inDesktopApp } from "../../api/shell";

0 commit comments

Comments
 (0)