@shipfox/client-onboarding 22.0.2 → 23.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.ts +1 -0
- package/.storybook/preview.css +7 -0
- package/.storybook/preview.tsx +56 -0
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +32 -0
- package/README.md +110 -0
- package/dist/core/integration-readiness.d.ts +35 -0
- package/dist/core/integration-readiness.d.ts.map +1 -0
- package/dist/core/integration-readiness.js +44 -0
- package/dist/core/integration-readiness.js.map +1 -0
- package/dist/core/setup-checklist.d.ts +49 -0
- package/dist/core/setup-checklist.d.ts.map +1 -0
- package/dist/core/setup-checklist.js +110 -0
- package/dist/core/setup-checklist.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/dist/workspace-setup-route.d.ts.map +1 -1
- package/dist/workspace-setup-route.js +14 -7
- package/dist/workspace-setup-route.js.map +1 -1
- package/package.json +10 -5
- package/src/core/integration-readiness.test.ts +277 -0
- package/src/core/integration-readiness.ts +93 -0
- package/src/core/setup-checklist.test.ts +342 -0
- package/src/core/setup-checklist.ts +156 -0
- package/src/index.ts +15 -0
- package/src/workspace-setup-route.test.tsx +24 -0
- package/src/workspace-setup-route.ts +20 -6
- package/tsconfig.build.tsbuildinfo +1 -1
- package/vitest.config.ts +57 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import {describe, expect, test} from '@shipfox/vitest/vi';
|
|
2
|
+
import {deriveIntegrationReadiness} from './integration-readiness.js';
|
|
3
|
+
import {
|
|
4
|
+
deriveSetupChecklist,
|
|
5
|
+
type SetupChecklistInput,
|
|
6
|
+
type SetupChecklistItem,
|
|
7
|
+
} from './setup-checklist.js';
|
|
8
|
+
|
|
9
|
+
function readiness(overrides: Partial<ReturnType<typeof deriveIntegrationReadiness>> = {}) {
|
|
10
|
+
return {
|
|
11
|
+
...deriveIntegrationReadiness({
|
|
12
|
+
providers: [
|
|
13
|
+
{
|
|
14
|
+
provider: 'github',
|
|
15
|
+
displayName: 'GitHub',
|
|
16
|
+
capabilities: ['source_control', 'agent_tools'],
|
|
17
|
+
},
|
|
18
|
+
{provider: 'linear', displayName: 'Linear', capabilities: ['agent_tools']},
|
|
19
|
+
{provider: 'webhook', displayName: 'Webhook', capabilities: []},
|
|
20
|
+
],
|
|
21
|
+
connections: [],
|
|
22
|
+
}),
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function input(overrides: Partial<SetupChecklistInput> = {}): SetupChecklistInput {
|
|
28
|
+
return {
|
|
29
|
+
readiness: readiness(),
|
|
30
|
+
installationRunners: 'managed',
|
|
31
|
+
workspaceRunnerCapacity: false,
|
|
32
|
+
modelProvider: {installationProvided: true, configured: false},
|
|
33
|
+
membership: {memberCount: 1, pendingInvitationCount: 0},
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function itemIds(items: readonly SetupChecklistItem[]) {
|
|
39
|
+
return items.map((item) => item.id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('deriveSetupChecklist', () => {
|
|
43
|
+
test('keeps rows in spec order', () => {
|
|
44
|
+
const checklist = deriveSetupChecklist(input());
|
|
45
|
+
|
|
46
|
+
expect(itemIds(checklist.items)).toEqual([
|
|
47
|
+
'source-control',
|
|
48
|
+
'project',
|
|
49
|
+
'tools',
|
|
50
|
+
'first-workflow',
|
|
51
|
+
'teammates',
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('renders the Cloud shape as 2 of 3 done', () => {
|
|
56
|
+
const checklist = deriveSetupChecklist(input());
|
|
57
|
+
|
|
58
|
+
expect(checklist.items.map((item) => [item.id, item.status])).toEqual([
|
|
59
|
+
['source-control', 'done'],
|
|
60
|
+
['project', 'done'],
|
|
61
|
+
['tools', 'open'],
|
|
62
|
+
['first-workflow', 'info'],
|
|
63
|
+
['teammates', 'info'],
|
|
64
|
+
]);
|
|
65
|
+
expect(checklist.trackedCount).toBe(3);
|
|
66
|
+
expect(checklist.openCount).toBe(1);
|
|
67
|
+
expect(checklist.complete).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('completes on Cloud once a tool is connected', () => {
|
|
71
|
+
const checklist = deriveSetupChecklist(
|
|
72
|
+
input({readiness: readiness({hasToolIntegration: true})}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(checklist.openCount).toBe(0);
|
|
76
|
+
expect(checklist.complete).toBe(true);
|
|
77
|
+
expect(checklist.items.find((item) => item.id === 'tools')?.status).toBe('done');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('renders the bare self-host shape as 2 of 5 done', () => {
|
|
81
|
+
const checklist = deriveSetupChecklist(
|
|
82
|
+
input({
|
|
83
|
+
installationRunners: 'none',
|
|
84
|
+
modelProvider: {installationProvided: false, configured: false},
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(itemIds(checklist.items)).toEqual([
|
|
89
|
+
'source-control',
|
|
90
|
+
'project',
|
|
91
|
+
'tools',
|
|
92
|
+
'runner',
|
|
93
|
+
'model-provider',
|
|
94
|
+
'first-workflow',
|
|
95
|
+
'teammates',
|
|
96
|
+
]);
|
|
97
|
+
expect(checklist.trackedCount).toBe(5);
|
|
98
|
+
expect(checklist.openCount).toBe(3);
|
|
99
|
+
expect(checklist.complete).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('completes on a bare self-host only when tools, runner, and model provider are done', () => {
|
|
103
|
+
const checklist = deriveSetupChecklist(
|
|
104
|
+
input({
|
|
105
|
+
readiness: readiness({hasToolIntegration: true}),
|
|
106
|
+
installationRunners: 'none',
|
|
107
|
+
workspaceRunnerCapacity: true,
|
|
108
|
+
modelProvider: {installationProvided: false, configured: true},
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(checklist.openCount).toBe(0);
|
|
113
|
+
expect(checklist.complete).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('keeps the checklist incomplete while any conditional row is open', () => {
|
|
117
|
+
const checklist = deriveSetupChecklist(
|
|
118
|
+
input({
|
|
119
|
+
readiness: readiness({hasToolIntegration: true}),
|
|
120
|
+
installationRunners: 'none',
|
|
121
|
+
workspaceRunnerCapacity: false,
|
|
122
|
+
modelProvider: {installationProvided: false, configured: true},
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(checklist.openCount).toBe(1);
|
|
127
|
+
expect(checklist.complete).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('hides the runner row when the installation provides managed capacity', () => {
|
|
131
|
+
const checklist = deriveSetupChecklist(
|
|
132
|
+
input({installationRunners: 'managed', workspaceRunnerCapacity: false}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(itemIds(checklist.items)).not.toContain('runner');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('shows the runner row when the installation provides no capacity', () => {
|
|
139
|
+
const checklist = deriveSetupChecklist(
|
|
140
|
+
input({installationRunners: 'none', workspaceRunnerCapacity: false}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(checklist.items.find((item) => item.id === 'runner')).toMatchObject({
|
|
144
|
+
status: 'open',
|
|
145
|
+
tracked: true,
|
|
146
|
+
action: {href: '/settings/runners'},
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('marks the runner row done when the workspace has capacity', () => {
|
|
151
|
+
const checklist = deriveSetupChecklist(
|
|
152
|
+
input({installationRunners: 'none', workspaceRunnerCapacity: true}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(checklist.items.find((item) => item.id === 'runner')?.status).toBe('done');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('hides the model-provider row when the installation provides inference', () => {
|
|
159
|
+
const checklist = deriveSetupChecklist(
|
|
160
|
+
input({modelProvider: {installationProvided: true, configured: false}}),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(itemIds(checklist.items)).not.toContain('model-provider');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('shows the model-provider row only when the installation provides no inference', () => {
|
|
167
|
+
const checklist = deriveSetupChecklist(
|
|
168
|
+
input({modelProvider: {installationProvided: false, configured: false}}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(checklist.items.find((item) => item.id === 'model-provider')).toMatchObject({
|
|
172
|
+
status: 'open',
|
|
173
|
+
tracked: true,
|
|
174
|
+
action: {href: '/settings/agents'},
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('marks the model-provider row done when a config exists', () => {
|
|
179
|
+
const checklist = deriveSetupChecklist(
|
|
180
|
+
input({modelProvider: {installationProvided: false, configured: true}}),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(checklist.items.find((item) => item.id === 'model-provider')?.status).toBe('done');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('keeps source control and project rows always done', () => {
|
|
187
|
+
const checklist = deriveSetupChecklist(input());
|
|
188
|
+
|
|
189
|
+
expect(checklist.items.find((item) => item.id === 'source-control')?.status).toBe('done');
|
|
190
|
+
expect(checklist.items.find((item) => item.id === 'project')?.status).toBe('done');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('names a single attention provider in the tools title', () => {
|
|
194
|
+
const checklist = deriveSetupChecklist(
|
|
195
|
+
input({
|
|
196
|
+
readiness: readiness({
|
|
197
|
+
attentionProviders: ['linear'],
|
|
198
|
+
hasToolIntegration: false,
|
|
199
|
+
}),
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(checklist.items.find((item) => item.id === 'tools')).toMatchObject({
|
|
204
|
+
status: 'open',
|
|
205
|
+
title: 'Linear needs attention',
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('uses the provider display name for a key with brand capitalization', () => {
|
|
210
|
+
const checklist = deriveSetupChecklist(
|
|
211
|
+
input({
|
|
212
|
+
readiness: readiness({
|
|
213
|
+
providers: [
|
|
214
|
+
{
|
|
215
|
+
provider: 'github',
|
|
216
|
+
displayName: 'GitHub',
|
|
217
|
+
capabilities: ['agent_tools'],
|
|
218
|
+
connected: false,
|
|
219
|
+
attention: true,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
attentionProviders: ['github'],
|
|
223
|
+
hasToolIntegration: false,
|
|
224
|
+
}),
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(checklist.items.find((item) => item.id === 'tools')?.title).toBe(
|
|
229
|
+
'GitHub needs attention',
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('does not name a source-control provider in the tools title', () => {
|
|
234
|
+
const checklist = deriveSetupChecklist(
|
|
235
|
+
input({
|
|
236
|
+
readiness: readiness({
|
|
237
|
+
attentionProviders: ['github'],
|
|
238
|
+
hasToolIntegration: false,
|
|
239
|
+
}),
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(checklist.items.find((item) => item.id === 'tools')?.title).toBe('Connect your tools');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('counts several attention providers in the tools title', () => {
|
|
247
|
+
const checklist = deriveSetupChecklist(
|
|
248
|
+
input({
|
|
249
|
+
readiness: readiness({
|
|
250
|
+
attentionProviders: ['linear', 'webhook'],
|
|
251
|
+
hasToolIntegration: false,
|
|
252
|
+
}),
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
expect(checklist.items.find((item) => item.id === 'tools')?.title).toBe(
|
|
257
|
+
'2 integrations need attention',
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('keeps the base tools title when nothing needs attention', () => {
|
|
262
|
+
const checklist = deriveSetupChecklist(input());
|
|
263
|
+
|
|
264
|
+
expect(checklist.items.find((item) => item.id === 'tools')?.title).toBe('Connect your tools');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('keeps the base tools title when the tools row is done despite attention', () => {
|
|
268
|
+
const checklist = deriveSetupChecklist(
|
|
269
|
+
input({
|
|
270
|
+
readiness: readiness({
|
|
271
|
+
attentionProviders: ['linear'],
|
|
272
|
+
hasToolIntegration: true,
|
|
273
|
+
}),
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
expect(checklist.items.find((item) => item.id === 'tools')).toMatchObject({
|
|
278
|
+
status: 'done',
|
|
279
|
+
title: 'Connect your tools',
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('carries the tools purpose and settings link on the open row', () => {
|
|
284
|
+
const checklist = deriveSetupChecklist(input());
|
|
285
|
+
|
|
286
|
+
expect(checklist.items.find((item) => item.id === 'tools')).toMatchObject({
|
|
287
|
+
purpose:
|
|
288
|
+
'Connect issue tracking, messaging, observability, or any Shipfox integration so workflows can react to events and agents can use them',
|
|
289
|
+
action: {href: '/settings/integrations'},
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('treats the first-workflow row as a pointer that never counts', () => {
|
|
294
|
+
const checklist = deriveSetupChecklist(input());
|
|
295
|
+
|
|
296
|
+
expect(checklist.items.find((item) => item.id === 'first-workflow')).toMatchObject({
|
|
297
|
+
status: 'info',
|
|
298
|
+
tracked: false,
|
|
299
|
+
action: {href: '/docs/getting-started'},
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('treats the teammates row as a pointer that never counts', () => {
|
|
304
|
+
const checklist = deriveSetupChecklist(input());
|
|
305
|
+
|
|
306
|
+
expect(checklist.items.find((item) => item.id === 'teammates')).toMatchObject({
|
|
307
|
+
status: 'info',
|
|
308
|
+
tracked: false,
|
|
309
|
+
action: {href: '/settings/members'},
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('renders the teammates row done once a second member joined', () => {
|
|
314
|
+
const checklist = deriveSetupChecklist(
|
|
315
|
+
input({membership: {memberCount: 2, pendingInvitationCount: 0}}),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(checklist.items.find((item) => item.id === 'teammates')?.status).toBe('done');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('renders the teammates row done once an invitation is pending', () => {
|
|
322
|
+
const checklist = deriveSetupChecklist(
|
|
323
|
+
input({membership: {memberCount: 1, pendingInvitationCount: 1}}),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(checklist.items.find((item) => item.id === 'teammates')?.status).toBe('done');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('never counts pointers toward trackedCount or openCount', () => {
|
|
330
|
+
const checklist = deriveSetupChecklist(
|
|
331
|
+
input({
|
|
332
|
+
membership: {memberCount: 1, pendingInvitationCount: 0},
|
|
333
|
+
installationRunners: 'none',
|
|
334
|
+
modelProvider: {installationProvided: false, configured: false},
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
expect(checklist.trackedCount).toBe(5);
|
|
339
|
+
expect(checklist.openCount).toBe(3);
|
|
340
|
+
expect(checklist.items.filter((item) => !item.tracked)).toHaveLength(2);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type {WorkspaceIntegrationReadiness} from './integration-readiness.js';
|
|
2
|
+
|
|
3
|
+
export type SetupChecklistItemStatus = 'done' | 'open' | 'info';
|
|
4
|
+
|
|
5
|
+
export type SetupChecklistItemId =
|
|
6
|
+
| 'source-control'
|
|
7
|
+
| 'project'
|
|
8
|
+
| 'tools'
|
|
9
|
+
| 'runner'
|
|
10
|
+
| 'model-provider'
|
|
11
|
+
| 'first-workflow'
|
|
12
|
+
| 'teammates';
|
|
13
|
+
|
|
14
|
+
export interface SetupChecklistAction {
|
|
15
|
+
label: string;
|
|
16
|
+
href: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SetupChecklistItem {
|
|
20
|
+
id: SetupChecklistItemId;
|
|
21
|
+
title: string;
|
|
22
|
+
status: SetupChecklistItemStatus;
|
|
23
|
+
/** Tracked items count toward completion; pointers never do. */
|
|
24
|
+
tracked: boolean;
|
|
25
|
+
/** One line of purpose, rendered for open and pointer rows. */
|
|
26
|
+
purpose?: string;
|
|
27
|
+
/** Primary action, a link to the settings page where the work happens. */
|
|
28
|
+
action?: SetupChecklistAction;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SetupChecklist {
|
|
32
|
+
/** Rows in spec order: done progress, activation asks, then pointers. */
|
|
33
|
+
items: readonly SetupChecklistItem[];
|
|
34
|
+
/** Open tracked rows. */
|
|
35
|
+
openCount: number;
|
|
36
|
+
/** Rows that count toward completion. */
|
|
37
|
+
trackedCount: number;
|
|
38
|
+
/** True when every tracked row is done. */
|
|
39
|
+
complete: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SetupChecklistInput {
|
|
43
|
+
readiness: WorkspaceIntegrationReadiness;
|
|
44
|
+
installationRunners: 'managed' | 'none';
|
|
45
|
+
workspaceRunnerCapacity: boolean;
|
|
46
|
+
modelProvider: {installationProvided: boolean; configured: boolean};
|
|
47
|
+
membership: {memberCount: number; pendingInvitationCount: number};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const TOOLS_TITLE = 'Connect your tools';
|
|
51
|
+
const TOOLS_PURPOSE =
|
|
52
|
+
'Connect issue tracking, messaging, observability, or any Shipfox integration so workflows can react to events and agents can use them';
|
|
53
|
+
const RUNNER_PURPOSE = 'Jobs wait in `pending` until a runner is online';
|
|
54
|
+
const MODEL_PROVIDER_PURPOSE = 'Agent steps need a model provider to run';
|
|
55
|
+
const FIRST_WORKFLOW_PURPOSE =
|
|
56
|
+
'Add a workflow file under `.shipfox/workflows/` and Shipfox picks it up on the next push';
|
|
57
|
+
const TEAMMATES_PURPOSE = 'Everyone in the workspace can edit workflows and see runs';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Derives the workspace setup checklist from integration readiness and the
|
|
61
|
+
* runner, model-provider, and membership facts. Rows follow the spec order;
|
|
62
|
+
* the runner and model-provider rows exist only when the installation does
|
|
63
|
+
* not already provide the capability.
|
|
64
|
+
*/
|
|
65
|
+
export function deriveSetupChecklist({
|
|
66
|
+
readiness,
|
|
67
|
+
installationRunners,
|
|
68
|
+
workspaceRunnerCapacity,
|
|
69
|
+
modelProvider,
|
|
70
|
+
membership,
|
|
71
|
+
}: SetupChecklistInput): SetupChecklist {
|
|
72
|
+
const items: SetupChecklistItem[] = [
|
|
73
|
+
{id: 'source-control', title: 'Connect source control', status: 'done', tracked: true},
|
|
74
|
+
{id: 'project', title: 'Create a project', status: 'done', tracked: true},
|
|
75
|
+
{
|
|
76
|
+
id: 'tools',
|
|
77
|
+
title: toolsTitle(readiness),
|
|
78
|
+
status: readiness.hasToolIntegration ? 'done' : 'open',
|
|
79
|
+
tracked: true,
|
|
80
|
+
purpose: TOOLS_PURPOSE,
|
|
81
|
+
action: {label: 'Connect', href: '/settings/integrations'},
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
if (installationRunners === 'none') {
|
|
86
|
+
items.push({
|
|
87
|
+
id: 'runner',
|
|
88
|
+
title: 'Set up runner capacity',
|
|
89
|
+
status: workspaceRunnerCapacity ? 'done' : 'open',
|
|
90
|
+
tracked: true,
|
|
91
|
+
purpose: RUNNER_PURPOSE,
|
|
92
|
+
action: {label: 'Set up', href: '/settings/runners'},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!modelProvider.installationProvided) {
|
|
97
|
+
items.push({
|
|
98
|
+
id: 'model-provider',
|
|
99
|
+
title: 'Configure a model provider',
|
|
100
|
+
status: modelProvider.configured ? 'done' : 'open',
|
|
101
|
+
tracked: true,
|
|
102
|
+
purpose: MODEL_PROVIDER_PURPOSE,
|
|
103
|
+
action: {label: 'Configure', href: '/settings/agents'},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
items.push(
|
|
108
|
+
{
|
|
109
|
+
id: 'first-workflow',
|
|
110
|
+
title: 'Push your first workflow',
|
|
111
|
+
status: 'info',
|
|
112
|
+
tracked: false,
|
|
113
|
+
purpose: FIRST_WORKFLOW_PURPOSE,
|
|
114
|
+
action: {label: 'Read the quickstart', href: '/docs/getting-started'},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'teammates',
|
|
118
|
+
title: 'Invite your teammates',
|
|
119
|
+
status:
|
|
120
|
+
membership.memberCount >= 2 || membership.pendingInvitationCount >= 1 ? 'done' : 'info',
|
|
121
|
+
tracked: false,
|
|
122
|
+
purpose: TEAMMATES_PURPOSE,
|
|
123
|
+
action: {label: 'Invite', href: '/settings/members'},
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const trackedItems = items.filter((item) => item.tracked);
|
|
128
|
+
const openCount = trackedItems.filter((item) => item.status === 'open').length;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
items,
|
|
132
|
+
openCount,
|
|
133
|
+
trackedCount: trackedItems.length,
|
|
134
|
+
complete: openCount === 0,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toolsTitle(readiness: WorkspaceIntegrationReadiness): string {
|
|
139
|
+
if (!readiness.hasToolIntegration) {
|
|
140
|
+
const attentionToolProviders = readiness.attentionProviders
|
|
141
|
+
.map((providerKey) => readiness.providers.find(({provider}) => provider === providerKey))
|
|
142
|
+
.filter(
|
|
143
|
+
(provider) => provider !== undefined && !provider.capabilities.includes('source_control'),
|
|
144
|
+
);
|
|
145
|
+
if (attentionToolProviders.length === 1) {
|
|
146
|
+
const provider = attentionToolProviders[0];
|
|
147
|
+
if (provider !== undefined) {
|
|
148
|
+
return `${provider.displayName || provider.provider} needs attention`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (attentionToolProviders.length > 1) {
|
|
152
|
+
return `${attentionToolProviders.length} integrations need attention`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return TOOLS_TITLE;
|
|
156
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
deriveIntegrationReadiness,
|
|
3
|
+
type IntegrationProviderReadiness,
|
|
4
|
+
type IntegrationReadinessInput,
|
|
5
|
+
type WorkspaceIntegrationReadiness,
|
|
6
|
+
} from '#core/integration-readiness.js';
|
|
7
|
+
export {
|
|
8
|
+
deriveSetupChecklist,
|
|
9
|
+
type SetupChecklist,
|
|
10
|
+
type SetupChecklistAction,
|
|
11
|
+
type SetupChecklistInput,
|
|
12
|
+
type SetupChecklistItem,
|
|
13
|
+
type SetupChecklistItemId,
|
|
14
|
+
type SetupChecklistItemStatus,
|
|
15
|
+
} from '#core/setup-checklist.js';
|
|
1
16
|
export {
|
|
2
17
|
loadWorkspaceSetupRoute,
|
|
3
18
|
type WorkspaceSetupRouteOptions,
|
|
@@ -82,6 +82,7 @@ interface SetupFetchOptions {
|
|
|
82
82
|
projectsFail?: boolean;
|
|
83
83
|
connectionsFail?: boolean;
|
|
84
84
|
providerConfigsFail?: boolean;
|
|
85
|
+
workspaceProviders?: 'enabled' | 'disabled';
|
|
85
86
|
projectsPending?: boolean;
|
|
86
87
|
workspaceStatus?: 'active' | 'suspended' | 'deleted';
|
|
87
88
|
}
|
|
@@ -95,6 +96,7 @@ function setupFetch(options: SetupFetchOptions = {}) {
|
|
|
95
96
|
projectsFail = false,
|
|
96
97
|
connectionsFail = false,
|
|
97
98
|
providerConfigsFail = false,
|
|
99
|
+
workspaceProviders,
|
|
98
100
|
projectsPending = false,
|
|
99
101
|
workspaceStatus = 'active',
|
|
100
102
|
} = options;
|
|
@@ -140,6 +142,14 @@ function setupFetch(options: SetupFetchOptions = {}) {
|
|
|
140
142
|
}),
|
|
141
143
|
);
|
|
142
144
|
}
|
|
145
|
+
if (url.endsWith('/agent/model-provider-catalog')) {
|
|
146
|
+
return Promise.resolve(
|
|
147
|
+
jsonResponse({
|
|
148
|
+
providers: [],
|
|
149
|
+
...(workspaceProviders === undefined ? {} : {workspace_providers: workspaceProviders}),
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
143
153
|
return Promise.resolve(jsonResponse({}, {status: 404}));
|
|
144
154
|
});
|
|
145
155
|
}
|
|
@@ -353,6 +363,20 @@ describe('workspace setup route hook', () => {
|
|
|
353
363
|
expect(screen.getByTestId('project-navigation')).toHaveTextContent('hidden');
|
|
354
364
|
});
|
|
355
365
|
|
|
366
|
+
test('skips provider onboarding when workspace providers are managed by the instance', async () => {
|
|
367
|
+
const fetchImpl = setupFetch({
|
|
368
|
+
connections: [sourceConnection()],
|
|
369
|
+
providerConfigs: [],
|
|
370
|
+
defaultProviderId: null,
|
|
371
|
+
workspaceProviders: 'disabled',
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
renderSetupRoute(`/w/${WORKSPACE_SLUG}/integrations`, fetchImpl);
|
|
375
|
+
|
|
376
|
+
expect(await screen.findByText('Create project')).toBeInTheDocument();
|
|
377
|
+
expect(calledUrls(fetchImpl).some((url) => url.endsWith('/agent/model-providers'))).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
|
|
356
380
|
test('keeps the provider onboarding route available while provider setup is pending', async () => {
|
|
357
381
|
renderSetupRoute(
|
|
358
382
|
`/w/${WORKSPACE_SLUG}/model-provider`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isModelProviderOnboardingDismissed,
|
|
3
|
+
modelProviderCatalogQueryOptions,
|
|
3
4
|
modelProviderConfigsQueryOptions,
|
|
4
5
|
} from '@shipfox/client-agent';
|
|
5
6
|
import {
|
|
@@ -140,14 +141,27 @@ async function hasHandledModelProviderOnboarding(
|
|
|
140
141
|
): Promise<boolean> {
|
|
141
142
|
if (isModelProviderOnboardingDismissed(workspaceId)) return true;
|
|
142
143
|
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
const catalogOptions = modelProviderCatalogQueryOptions();
|
|
145
|
+
const configOptions = modelProviderConfigsQueryOptions(workspaceId);
|
|
146
|
+
const [catalogResult, configsResult] = await Promise.allSettled([
|
|
147
|
+
queryClient.fetchQuery(catalogOptions),
|
|
148
|
+
queryClient.fetchQuery(configOptions),
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
catalogResult.status === 'fulfilled' &&
|
|
153
|
+
catalogResult.value.workspaceProviders === 'disabled'
|
|
154
|
+
) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (configsResult.status === 'fulfilled') {
|
|
159
|
+
const configs = configsResult.value;
|
|
146
160
|
return configs.configs.length > 0;
|
|
147
|
-
} catch {
|
|
148
|
-
const cached = queryClient.getQueryData<{configs: unknown[]}>(options.queryKey);
|
|
149
|
-
return cached?.configs.length !== 0;
|
|
150
161
|
}
|
|
162
|
+
|
|
163
|
+
const cached = queryClient.getQueryData<{configs: unknown[]}>(configOptions.queryKey);
|
|
164
|
+
return cached?.configs.length !== 0;
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
function normalizePath(pathname: string) {
|