Skip to content

Commit f437f6b

Browse files
committed
Enhance executor functionality and improve code organization
- Added `@executor/core` as a dependency in multiple package configurations. - Updated the Next.js configuration to include `@executor/core` in the transpile packages. - Refactored type definitions and imports to streamline code and improve clarity. - Enhanced connection management UI with improved messaging and error handling. - Introduced new utility functions for better source authentication handling. - Updated tests to reflect changes in type definitions and ensure compatibility.
1 parent cf2df42 commit f437f6b

38 files changed

Lines changed: 490 additions & 1393 deletions

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

executor/apps/web/next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const appShellRewriteExclusions = [
2626

2727
const nextConfig: NextConfig = {
2828
output: "standalone",
29-
transpilePackages: ["@executor/convex", "@executor/ui"],
29+
transpilePackages: ["@executor/core", "@executor/convex", "@executor/ui"],
3030
env: {
3131
// Map canonical env vars to NEXT_PUBLIC_ so they're available client-side.
3232
// This lets us keep a single root .env without NEXT_PUBLIC_ prefixes.

executor/apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"lint": "eslint"
1111
},
1212
"dependencies": {
13+
"@executor/core": "workspace:*",
1314
"@convex-dev/workos": "^0.0.1",
1415
"@executor/convex": "workspace:*",
1516
"@executor/ui": "workspace:*",

executor/apps/web/src/components/tasks/code-editor-types.ts

Lines changed: 1 addition & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ToolDescriptor } from "@/lib/types";
2+
export { OPENAPI_HELPER_TYPES } from "@executor/core/openapi-helper-types";
23

34
interface NamespaceNode {
45
children: Map<string, NamespaceNode>;
@@ -125,65 +126,6 @@ export function generateToolsDts(tools: ToolDescriptor[], dtsSources: Set<string
125126
return dts;
126127
}
127128

128-
export const OPENAPI_HELPER_TYPES = `
129-
type _Normalize<T> = Exclude<T, undefined | null>;
130-
type _OrEmpty<T> = [_Normalize<T>] extends [never] ? {} : _Normalize<T>;
131-
type _Simplify<T> = { [K in keyof T]: T[K] } & {};
132-
type _ParamsOf<Op> =
133-
Op extends { parameters: infer P } ? P :
134-
Op extends { parameters?: infer P } ? P :
135-
never;
136-
type _ParamAt<Op, K extends "query" | "path" | "header" | "cookie"> =
137-
_ParamsOf<Op> extends { [P in K]?: infer V } ? V : never;
138-
type _BodyOf<Op> =
139-
Op extends { requestBody?: infer B } ? B :
140-
Op extends { requestBody: infer B } ? B :
141-
never;
142-
type _BodyContent<B> =
143-
B extends { content: infer C }
144-
? C extends Record<string, infer V> ? V : never
145-
: never;
146-
type ToolInput<Op> = _Simplify<
147-
_OrEmpty<_ParamAt<Op, "query">> &
148-
_OrEmpty<_ParamAt<Op, "path">> &
149-
_OrEmpty<_ParamAt<Op, "header">> &
150-
_OrEmpty<_ParamAt<Op, "cookie">> &
151-
_OrEmpty<_BodyContent<_BodyOf<Op>>>
152-
>;
153-
type _ResponsesOf<Op> = Op extends { responses: infer R } ? R : never;
154-
type _RespAt<Op, Code extends PropertyKey> =
155-
_ResponsesOf<Op> extends { [K in Code]?: infer R } ? R : never;
156-
type _ResponsePayload<R> =
157-
[R] extends [never] ? never :
158-
R extends { content: infer C }
159-
? C extends Record<string, infer V> ? V : unknown
160-
: R extends { schema: infer S } ? S : unknown;
161-
type _HasStatus<Op, Code extends PropertyKey> =
162-
[_RespAt<Op, Code>] extends [never] ? false : true;
163-
type _PayloadAt<Op, Code extends PropertyKey> =
164-
Code extends 204 | 205
165-
? (_HasStatus<Op, Code> extends true ? void : never)
166-
: _ResponsePayload<_RespAt<Op, Code>>;
167-
type _FirstKnown<T extends readonly unknown[]> =
168-
T extends readonly [infer H, ...infer Rest]
169-
? [H] extends [never] ? _FirstKnown<Rest> : H
170-
: unknown;
171-
type ToolOutput<Op> = _FirstKnown<[
172-
_PayloadAt<Op, 200>,
173-
_PayloadAt<Op, 201>,
174-
_PayloadAt<Op, 202>,
175-
_PayloadAt<Op, 203>,
176-
_PayloadAt<Op, 204>,
177-
_PayloadAt<Op, 205>,
178-
_PayloadAt<Op, 206>,
179-
_PayloadAt<Op, 207>,
180-
_PayloadAt<Op, 208>,
181-
_PayloadAt<Op, 226>,
182-
_PayloadAt<Op, "default">,
183-
unknown
184-
]>;
185-
`;
186-
187129
export const BASE_ENVIRONMENT_DTS = `
188130
interface Console {
189131
/** Console output is discarded; use explicit return values for results. */

executor/apps/web/src/components/tools/connection-auth-fields.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ export function ConnectionAuthFields({
4444
<>
4545
{editing && (
4646
<p className="text-[11px] text-muted-foreground">
47-
Stored secret is hidden. Enter a new value to rotate it, or leave fields blank to keep it.
47+
Existing secret values are hidden. Enter new values to rotate credentials, or leave blank to keep them.
4848
</p>
4949
)}
5050

5151
{selectedAuth.type === "none" ? (
5252
<p className="text-[11px] text-terminal-amber">This source does not currently require auth.</p>
5353
) : selectedAuth.type === "mixed" ? (
5454
<p className="text-[11px] text-terminal-amber">
55-
This source has mixed auth requirements. Link an existing connection for now.
55+
This API uses multiple auth schemes. Reuse compatible saved credentials for now.
5656
</p>
5757
) : selectedAuth.type === "apiKey" ? (
5858
<div className="space-y-1.5">

executor/apps/web/src/components/tools/connection-form-dialog-state.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,16 @@ export function compatibleConnections(
7373

7474
export function selectedAuthBadge(type: SourceAuthType, mode?: CredentialScope): string {
7575
if (type === "none") {
76-
return "none";
76+
return "No auth";
7777
}
7878
if (type === "mixed") {
79-
return "mixed";
79+
return "Mixed auth";
8080
}
81-
return `${type}:${mode ?? "workspace"}`;
81+
const authLabel =
82+
type === "apiKey"
83+
? "API key"
84+
: type === "basic"
85+
? "Basic"
86+
: "Bearer";
87+
return `${authLabel} (${mode === "actor" ? "per-user" : "workspace"})`;
8288
}

executor/apps/web/src/components/tools/connection-form-dialog.tsx

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
connectionDisplayName,
3434
parseHeaderOverrides,
3535
} from "@/lib/credentials-source-helpers";
36+
import { sourceForCredentialKey } from "@/lib/tools-source-helpers";
3637
import { ConnectionAuthFields } from "./connection-auth-fields";
3738
import { type ConnectionMode } from "./connection-form-dialog-state";
3839
import { useConnectionFormDialogForm } from "./connection-form-dialog-form";
@@ -50,6 +51,7 @@ export function ConnectionFormDialog({
5051
sources,
5152
credentials,
5253
sourceAuthProfiles,
54+
loadingSourceNames = [],
5355
}: {
5456
open: boolean;
5557
onOpenChange: (open: boolean) => void;
@@ -58,6 +60,7 @@ export function ConnectionFormDialog({
5860
sources: ToolSourceRecord[];
5961
credentials: CredentialRecord[];
6062
sourceAuthProfiles: Record<string, SourceAuthProfile>;
63+
loadingSourceNames?: string[];
6164
}) {
6265
const { context, clientConfig } = useSession();
6366
const upsertCredential = useAction(convexApi.credentialsNode.upsertCredential);
@@ -101,17 +104,27 @@ export function ConnectionFormDialog({
101104
const storageCopy = clientConfig?.authProviderMode === "workos"
102105
? "Stored encrypted"
103106
: "Stored locally on this machine";
107+
const selectedSource = sourceForCredentialKey(sources, sourceKey);
108+
const selectedSourceName = selectedSource?.name;
109+
const authDetectionPending =
110+
!editing
111+
&& selectedAuth.type === "none"
112+
&& Boolean(selectedSource)
113+
&& Boolean(selectedSource?.type === "openapi" || selectedSource?.type === "graphql")
114+
&& selectedSource?.config.auth === undefined
115+
&& (selectedSourceName ? loadingSourceNames.includes(selectedSourceName) : false);
116+
const detectedAuthLabel = authDetectionPending ? "Detecting..." : authBadge;
104117

105118
const handleSave = async () => {
106119
if (!context) {
107120
return;
108121
}
109122
if (!sourceKey.trim()) {
110-
toast.error("Source key is required");
123+
toast.error("Choose an API source");
111124
return;
112125
}
113126
if (scope === "actor" && !actorId.trim()) {
114-
toast.error("Actor ID is required for actor-scoped credentials");
127+
toast.error("User ID is required for personal credentials");
115128
return;
116129
}
117130

@@ -123,21 +136,25 @@ export function ConnectionFormDialog({
123136

124137
const linkExisting = !editing && connectionMode === "existing";
125138
if (linkExisting && !existingConnectionId) {
126-
toast.error("Select an existing connection to link");
139+
toast.error("Select saved credentials");
127140
return;
128141
}
129142
if (linkExisting && !compatibleConnectionOptions.some((connection) => connection.id === existingConnectionId)) {
130-
toast.error("Selected connection does not match this scope");
143+
toast.error("Selected credentials do not match this scope");
131144
return;
132145
}
133146

134147
if (selectedAuth.type === "none") {
135-
toast.error("This source does not require auth");
148+
if (authDetectionPending) {
149+
toast.error("Still detecting auth from the API spec. Try again in a few seconds.");
150+
return;
151+
}
152+
toast.error("This source is currently set to no auth. Configure auth before adding a connection.");
136153
return;
137154
}
138155

139156
if (selectedAuth.type === "mixed" && !linkExisting && !editing) {
140-
toast.error("Mixed-auth sources must link to an existing connection");
157+
toast.error("This API uses mixed auth and must reuse saved credentials");
141158
return;
142159
}
143160

@@ -183,16 +200,16 @@ export function ConnectionFormDialog({
183200
<DialogContent className="bg-card border-border sm:max-w-lg">
184201
<DialogHeader>
185202
<DialogTitle className="text-sm font-medium">
186-
{editing ? "Edit Connection" : "Add Connection"}
203+
{editing ? "Update Connection" : "Connect API"}
187204
</DialogTitle>
188205
</DialogHeader>
189206
<div className="space-y-3">
190207
<div className="space-y-1.5">
191-
<Label className="text-xs text-muted-foreground">Source</Label>
208+
<Label className="text-xs text-muted-foreground">API Source</Label>
192209
{sourceOptions.length > 0 ? (
193210
<Select value={sourceKey} onValueChange={handleSourceKeyChange}>
194211
<SelectTrigger className="h-8 text-xs bg-background">
195-
<SelectValue />
212+
<SelectValue placeholder="Select a source" />
196213
</SelectTrigger>
197214
<SelectContent>
198215
{sourceOptions.map((entry) => (
@@ -206,42 +223,46 @@ export function ConnectionFormDialog({
206223
<Input
207224
value={sourceKey}
208225
onChange={(e) => handleSourceKeyChange(e.target.value)}
209-
placeholder="source:<source-id>"
226+
placeholder="Enter source id"
210227
className="h-8 text-xs font-mono bg-background"
211228
/>
212229
)}
213-
{sourceKey && (
214-
<p className="text-[10px] text-muted-foreground font-mono">key: {sourceKey}</p>
215-
)}
216230
</div>
217231

218-
<div className="flex items-center gap-2 flex-wrap">
219-
<span className="text-[11px] text-muted-foreground">Detected auth</span>
220-
<Badge variant="outline" className="text-[9px] font-mono uppercase tracking-wider">
221-
{authBadge}
222-
</Badge>
223-
{selectedAuth.inferred && (
232+
<div className="rounded-md border border-border/70 bg-muted/30 px-2.5 py-2">
233+
<div className="flex items-center gap-2 flex-wrap">
234+
<span className="text-[11px] text-muted-foreground">Detected auth</span>
224235
<Badge variant="outline" className="text-[9px] font-mono uppercase tracking-wider">
225-
inferred
236+
{detectedAuthLabel}
226237
</Badge>
238+
{selectedAuth.inferred && !authDetectionPending && (
239+
<Badge variant="outline" className="text-[9px] uppercase tracking-wider">
240+
inferred from spec
241+
</Badge>
242+
)}
243+
</div>
244+
{authDetectionPending && (
245+
<p className="text-[10px] text-muted-foreground mt-1">
246+
This spec is still loading. Auth settings will auto-fill once parsing completes.
247+
</p>
227248
)}
228249
{selectedAuth.type === "apiKey" && selectedAuth.header && (
229-
<span className="text-[10px] font-mono text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
230-
header: {selectedAuth.header}
231-
</span>
250+
<p className="text-[10px] text-muted-foreground mt-1">
251+
API key header: <span className="font-mono">{selectedAuth.header}</span>
252+
</p>
232253
)}
233254
</div>
234255

235256
<div className="grid grid-cols-2 gap-3">
236257
<div className="space-y-1.5">
237-
<Label className="text-xs text-muted-foreground">Scope</Label>
258+
<Label className="text-xs text-muted-foreground">Who this is for</Label>
238259
<Select value={scope} onValueChange={(value) => setScope(value as CredentialScope)}>
239260
<SelectTrigger className="h-8 text-xs bg-background">
240261
<SelectValue />
241262
</SelectTrigger>
242263
<SelectContent>
243-
<SelectItem value="workspace" className="text-xs">Workspace</SelectItem>
244-
<SelectItem value="actor" className="text-xs">Per-user (actor)</SelectItem>
264+
<SelectItem value="workspace" className="text-xs">Workspace-wide</SelectItem>
265+
<SelectItem value="actor" className="text-xs">Only me (current user)</SelectItem>
245266
</SelectContent>
246267
</Select>
247268
</div>
@@ -253,7 +274,7 @@ export function ConnectionFormDialog({
253274

254275
{scope === "actor" && (
255276
<div className="space-y-1.5">
256-
<Label className="text-xs text-muted-foreground">Actor ID</Label>
277+
<Label className="text-xs text-muted-foreground">User ID</Label>
257278
<Input
258279
value={actorId}
259280
onChange={(e) => setActorId(e.target.value)}
@@ -265,36 +286,36 @@ export function ConnectionFormDialog({
265286

266287
{!editing && connectionOptions.length > 0 && (
267288
<div className="space-y-1.5">
268-
<Label className="text-xs text-muted-foreground">Connection Mode</Label>
289+
<Label className="text-xs text-muted-foreground">How to connect</Label>
269290
<Select value={connectionMode} onValueChange={(value) => setConnectionMode(value as ConnectionMode)}>
270291
<SelectTrigger className="h-8 text-xs bg-background">
271292
<SelectValue />
272293
</SelectTrigger>
273294
<SelectContent>
274-
<SelectItem value="new" className="text-xs">Create new connection</SelectItem>
275-
<SelectItem value="existing" className="text-xs">Use existing connection</SelectItem>
295+
<SelectItem value="new" className="text-xs">Enter new credentials</SelectItem>
296+
<SelectItem value="existing" className="text-xs">Reuse saved credentials</SelectItem>
276297
</SelectContent>
277298
</Select>
278299
</div>
279300
)}
280301

281302
{!editing && connectionMode === "existing" && (
282303
<div className="space-y-1.5">
283-
<Label className="text-xs text-muted-foreground">Existing Connection</Label>
304+
<Label className="text-xs text-muted-foreground">Saved Credentials</Label>
284305
<Select value={existingConnectionId} onValueChange={setExistingConnectionId}>
285306
<SelectTrigger className="h-8 text-xs bg-background">
286-
<SelectValue placeholder="Select a connection" />
307+
<SelectValue placeholder="Select saved credentials" />
287308
</SelectTrigger>
288309
<SelectContent>
289310
{compatibleConnectionOptions.map((connection) => (
290311
<SelectItem key={connection.id} value={connection.id} className="text-xs">
291-
{connectionDisplayName(sources, connection)} ({connection.sourceKeys.size} source{connection.sourceKeys.size === 1 ? "" : "s"})
312+
{connectionDisplayName(sources, connection)} ({connection.sourceKeys.size} API{connection.sourceKeys.size === 1 ? "" : "s"})
292313
</SelectItem>
293314
))}
294315
</SelectContent>
295316
</Select>
296317
{compatibleConnectionOptions.length === 0 && (
297-
<p className="text-[10px] text-muted-foreground">No compatible existing connections for this scope.</p>
318+
<p className="text-[10px] text-muted-foreground">No compatible saved credentials for this scope.</p>
298319
)}
299320
</div>
300321
)}
@@ -314,7 +335,7 @@ export function ConnectionFormDialog({
314335
/>
315336

316337
<div className="space-y-1.5">
317-
<Label className="text-xs text-muted-foreground">Custom Headers (optional)</Label>
338+
<Label className="text-xs text-muted-foreground">Extra Headers (optional)</Label>
318339
<Textarea
319340
value={customHeadersText}
320341
onChange={(e) => setCustomHeadersText(e.target.value)}
@@ -324,7 +345,7 @@ export function ConnectionFormDialog({
324345
/>
325346
</div>
326347

327-
<Button onClick={handleSave} disabled={saving} className="w-full h-9" size="sm">
348+
<Button onClick={handleSave} disabled={saving || authDetectionPending} className="w-full h-9" size="sm">
328349
{connectionSubmitCopy(Boolean(editing), saving, connectionMode)}
329350
</Button>
330351
</div>

executor/apps/web/src/components/tools/connection-form-save.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function buildSecretJson({
6262
}
6363

6464
export function connectionSuccessCopy(editing: boolean, linkExisting: boolean): string {
65-
return editing ? "Connection updated" : linkExisting ? "Connection linked" : "Connection saved";
65+
return editing ? "Connection updated" : linkExisting ? "Credentials linked" : "Connection saved";
6666
}
6767

6868
export function connectionSubmitCopy(editing: boolean, saving: boolean, connectionMode: "new" | "existing"): string {
@@ -72,5 +72,5 @@ export function connectionSubmitCopy(editing: boolean, saving: boolean, connecti
7272
if (editing) {
7373
return "Update Connection";
7474
}
75-
return connectionMode === "existing" ? "Link Connection" : "Save Connection";
75+
return connectionMode === "existing" ? "Link Credentials" : "Save Connection";
7676
}

0 commit comments

Comments
 (0)