@opengeni/react 0.11.0 → 0.13.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/README.md +1 -1
- package/dist/{chunk-5C7RGAWA.js → chunk-NFYVQWIB.js} +170 -64
- package/dist/chunk-NFYVQWIB.js.map +1 -0
- package/dist/index.d.ts +791 -145
- package/dist/index.js +5447 -2213
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-CnlMb7E-.d.ts} +5 -5
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +1 -1
- package/package.json +17 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +32 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +135 -39
- package/src/components/code-editor.tsx +28 -36
- package/src/components/command-palette.tsx +26 -5
- package/src/components/desktop-viewer.tsx +29 -16
- package/src/components/diff-view.tsx +27 -189
- package/src/components/enrollment-consent.tsx +28 -10
- package/src/components/enrollment-device-flow.tsx +8 -5
- package/src/components/file-browser.tsx +190 -56
- package/src/components/fleet-tile.tsx +13 -4
- package/src/components/machine-card.tsx +16 -4
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-metrics.tsx +31 -8
- package/src/components/machine-status-pill.tsx +26 -5
- package/src/components/machines-dashboard.tsx +8 -2
- package/src/components/markdown.tsx +39 -9
- package/src/components/message-timeline.tsx +633 -109
- package/src/components/pierre-diff.tsx +83 -15
- package/src/components/pierre-file.tsx +10 -9
- package/src/components/sandbox-files.tsx +110 -227
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +823 -0
- package/src/components/session-status.tsx +28 -2
- package/src/components/workbench-changes.tsx +541 -0
- package/src/components/workspace-dock.tsx +85 -22
- package/src/hooks/internal.ts +5 -2
- package/src/hooks/use-available-models.ts +7 -2
- package/src/hooks/use-billing-usage.ts +4 -1
- package/src/hooks/use-codex-accounts.ts +74 -44
- package/src/hooks/use-composer.ts +57 -36
- package/src/hooks/use-environments.ts +92 -47
- package/src/hooks/use-file-attachments.ts +101 -55
- package/src/hooks/use-goal.ts +52 -16
- package/src/hooks/use-machine-chip.ts +134 -0
- package/src/hooks/use-machines.ts +34 -16
- package/src/hooks/use-packs.ts +24 -19
- package/src/hooks/use-relay-frame-stream.ts +5 -2
- package/src/hooks/use-rigs.ts +335 -0
- package/src/hooks/use-sandbox-files.ts +213 -39
- package/src/hooks/use-sandbox-git.ts +188 -11
- package/src/hooks/use-sandbox-terminal.ts +18 -14
- package/src/hooks/use-scheduled-tasks.ts +10 -2
- package/src/hooks/use-session-capabilities.ts +77 -20
- package/src/hooks/use-session-control.ts +49 -21
- package/src/hooks/use-session-events.ts +48 -20
- package/src/hooks/use-session-lineage.ts +119 -0
- package/src/hooks/use-session.ts +43 -28
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +74 -147
- package/src/hooks/use-windowed-sections.ts +204 -0
- package/src/hooks/use-workspace-capture.ts +233 -0
- package/src/hooks/use-workspace-edit.ts +231 -0
- package/src/hooks/use-workspace-sessions.ts +48 -5
- package/src/hooks/use-workspaces.ts +18 -15
- package/src/index.ts +128 -23
- package/src/lib/format.ts +44 -6
- package/src/lib/xterm-renderer.ts +65 -0
- package/src/lib/xterm-theme.ts +224 -18
- package/src/machines.ts +13 -3
- package/src/provider.tsx +3 -1
- package/src/timeline/activity-rail.tsx +213 -54
- package/src/timeline/disclosure-context.tsx +12 -2
- package/src/timeline/index.ts +21 -2
- package/src/timeline/parsers.ts +44 -10
- package/src/timeline/projection.ts +435 -84
- package/src/timeline/shared.tsx +111 -24
- package/src/timeline/tool-diff.tsx +24 -20
- package/src/timeline/tool-renderers.tsx +98 -21
- package/src/timeline/turn-summary.tsx +76 -23
- package/src/timeline/types.ts +96 -12
- package/styles/tokens.css +2 -2
- package/dist/chunk-5C7RGAWA.js.map +0 -1
|
@@ -1,118 +1,163 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
CreateVariableSetRequest,
|
|
3
|
+
UpdateVariableSetRequest,
|
|
4
|
+
VariableSet,
|
|
5
|
+
VariableSetVariableMetadata,
|
|
6
6
|
} from "@opengeni/sdk";
|
|
7
|
-
import { useCallback } from "react";
|
|
7
|
+
import { useCallback, useMemo } from "react";
|
|
8
8
|
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
9
9
|
import { useMutationRunner, usePolledValue } from "./internal";
|
|
10
10
|
|
|
11
|
-
export type
|
|
11
|
+
export type UseVariableSetsOptions = ClientOverride & {
|
|
12
12
|
pollIntervalMs?: number | undefined;
|
|
13
13
|
enabled?: boolean | undefined;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
export type
|
|
17
|
-
|
|
16
|
+
export type UseVariableSetsResult = {
|
|
17
|
+
variableSets: VariableSet[];
|
|
18
18
|
loading: boolean;
|
|
19
19
|
error: Error | null;
|
|
20
20
|
refresh: () => Promise<void>;
|
|
21
|
-
create: (request:
|
|
22
|
-
update: (
|
|
23
|
-
remove: (
|
|
21
|
+
create: (request: CreateVariableSetRequest) => Promise<VariableSet | null>;
|
|
22
|
+
update: (variableSetId: string, request: UpdateVariableSetRequest) => Promise<VariableSet | null>;
|
|
23
|
+
remove: (variableSetId: string) => Promise<boolean>;
|
|
24
24
|
/** Set/rotate a variable. Values are write-only — reads expose metadata only. */
|
|
25
|
-
setVariable: (
|
|
26
|
-
|
|
25
|
+
setVariable: (
|
|
26
|
+
variableSetId: string,
|
|
27
|
+
name: string,
|
|
28
|
+
value: string,
|
|
29
|
+
) => Promise<VariableSetVariableMetadata | null>;
|
|
30
|
+
deleteVariable: (variableSetId: string, name: string) => Promise<boolean>;
|
|
27
31
|
mutating: boolean;
|
|
28
32
|
mutationError: Error | null;
|
|
29
33
|
clearMutationError: () => void;
|
|
30
34
|
};
|
|
31
35
|
|
|
32
36
|
/**
|
|
33
|
-
*
|
|
37
|
+
* Variable sets (named, encrypted variable sets attached to sessions
|
|
34
38
|
* and scheduled tasks). Variable values are write-only end to end: this hook
|
|
35
39
|
* never sees a value after it is sent.
|
|
36
40
|
*/
|
|
37
|
-
export function
|
|
41
|
+
export function useVariableSets(options: UseVariableSetsOptions = {}): UseVariableSetsResult {
|
|
38
42
|
const { client, workspaceId } = useOpenGeni(options);
|
|
39
|
-
const load = useCallback(
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
const load = useCallback(
|
|
44
|
+
async () => await client.listVariableSets(workspaceId),
|
|
45
|
+
[client, workspaceId],
|
|
46
|
+
);
|
|
47
|
+
const { data, loading, error, refresh } = usePolledValue(load, {
|
|
48
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
49
|
+
enabled: options.enabled,
|
|
50
|
+
});
|
|
51
|
+
const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
|
|
42
52
|
|
|
43
53
|
const create = useCallback(
|
|
44
|
-
async (request:
|
|
45
|
-
const result = await
|
|
54
|
+
async (request: CreateVariableSetRequest): Promise<VariableSet | null> => {
|
|
55
|
+
const result = await run(() => client.createVariableSet(workspaceId, request));
|
|
46
56
|
if (result) {
|
|
47
|
-
await
|
|
57
|
+
await refresh();
|
|
48
58
|
}
|
|
49
59
|
return result;
|
|
50
60
|
},
|
|
51
|
-
[client, workspaceId,
|
|
61
|
+
[client, workspaceId, run, refresh],
|
|
52
62
|
);
|
|
53
63
|
|
|
54
64
|
const update = useCallback(
|
|
55
|
-
async (
|
|
56
|
-
|
|
65
|
+
async (
|
|
66
|
+
variableSetId: string,
|
|
67
|
+
request: UpdateVariableSetRequest,
|
|
68
|
+
): Promise<VariableSet | null> => {
|
|
69
|
+
const result = await run(() => client.updateVariableSet(workspaceId, variableSetId, request));
|
|
57
70
|
if (result) {
|
|
58
|
-
await
|
|
71
|
+
await refresh();
|
|
59
72
|
}
|
|
60
73
|
return result;
|
|
61
74
|
},
|
|
62
|
-
[client, workspaceId,
|
|
75
|
+
[client, workspaceId, run, refresh],
|
|
63
76
|
);
|
|
64
77
|
|
|
65
78
|
const remove = useCallback(
|
|
66
|
-
async (
|
|
67
|
-
const result = await
|
|
68
|
-
await client.
|
|
79
|
+
async (variableSetId: string): Promise<boolean> => {
|
|
80
|
+
const result = await run(async () => {
|
|
81
|
+
await client.deleteVariableSet(workspaceId, variableSetId);
|
|
69
82
|
return true;
|
|
70
83
|
});
|
|
71
84
|
if (result) {
|
|
72
|
-
await
|
|
85
|
+
await refresh();
|
|
73
86
|
}
|
|
74
87
|
return result === true;
|
|
75
88
|
},
|
|
76
|
-
[client, workspaceId,
|
|
89
|
+
[client, workspaceId, run, refresh],
|
|
77
90
|
);
|
|
78
91
|
|
|
79
92
|
const setVariable = useCallback(
|
|
80
|
-
async (
|
|
81
|
-
|
|
93
|
+
async (
|
|
94
|
+
variableSetId: string,
|
|
95
|
+
name: string,
|
|
96
|
+
value: string,
|
|
97
|
+
): Promise<VariableSetVariableMetadata | null> => {
|
|
98
|
+
const result = await run(() =>
|
|
99
|
+
client.setVariableSetVariable(workspaceId, variableSetId, name, value),
|
|
100
|
+
);
|
|
82
101
|
if (result) {
|
|
83
|
-
await
|
|
102
|
+
await refresh();
|
|
84
103
|
}
|
|
85
104
|
return result;
|
|
86
105
|
},
|
|
87
|
-
[client, workspaceId,
|
|
106
|
+
[client, workspaceId, run, refresh],
|
|
88
107
|
);
|
|
89
108
|
|
|
90
109
|
const deleteVariable = useCallback(
|
|
91
|
-
async (
|
|
92
|
-
const result = await
|
|
93
|
-
await client.
|
|
110
|
+
async (variableSetId: string, name: string): Promise<boolean> => {
|
|
111
|
+
const result = await run(async () => {
|
|
112
|
+
await client.deleteVariableSetVariable(workspaceId, variableSetId, name);
|
|
94
113
|
return true;
|
|
95
114
|
});
|
|
96
115
|
if (result) {
|
|
97
|
-
await
|
|
116
|
+
await refresh();
|
|
98
117
|
}
|
|
99
118
|
return result === true;
|
|
100
119
|
},
|
|
101
|
-
[client, workspaceId,
|
|
120
|
+
[client, workspaceId, run, refresh],
|
|
102
121
|
);
|
|
103
122
|
|
|
104
123
|
return {
|
|
105
|
-
|
|
106
|
-
loading
|
|
107
|
-
error
|
|
108
|
-
refresh
|
|
124
|
+
variableSets: data ?? [],
|
|
125
|
+
loading,
|
|
126
|
+
error,
|
|
127
|
+
refresh,
|
|
109
128
|
create,
|
|
110
129
|
update,
|
|
111
130
|
remove,
|
|
112
131
|
setVariable,
|
|
113
132
|
deleteVariable,
|
|
114
|
-
mutating
|
|
115
|
-
mutationError
|
|
116
|
-
clearMutationError
|
|
133
|
+
mutating,
|
|
134
|
+
mutationError,
|
|
135
|
+
clearMutationError,
|
|
117
136
|
};
|
|
118
137
|
}
|
|
138
|
+
|
|
139
|
+
/** @deprecated use UseVariableSetsOptions */
|
|
140
|
+
export type UseEnvironmentsOptions = UseVariableSetsOptions;
|
|
141
|
+
/** @deprecated use UseVariableSetsResult */
|
|
142
|
+
export type UseEnvironmentsResult = UseVariableSetsResult & { environments: VariableSet[] };
|
|
143
|
+
/** @deprecated use useVariableSets */
|
|
144
|
+
export function useEnvironments(options: UseEnvironmentsOptions = {}): UseEnvironmentsResult {
|
|
145
|
+
const legacyClient = useMemo(() => {
|
|
146
|
+
if (!options.client) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
...options.client,
|
|
151
|
+
listVariableSets: options.client.listEnvironments ?? options.client.listVariableSets,
|
|
152
|
+
createVariableSet: options.client.createEnvironment ?? options.client.createVariableSet,
|
|
153
|
+
updateVariableSet: options.client.updateEnvironment ?? options.client.updateVariableSet,
|
|
154
|
+
deleteVariableSet: options.client.deleteEnvironment ?? options.client.deleteVariableSet,
|
|
155
|
+
setVariableSetVariable:
|
|
156
|
+
options.client.setEnvironmentVariable ?? options.client.setVariableSetVariable,
|
|
157
|
+
deleteVariableSetVariable:
|
|
158
|
+
options.client.deleteEnvironmentVariable ?? options.client.deleteVariableSetVariable,
|
|
159
|
+
};
|
|
160
|
+
}, [options.client]);
|
|
161
|
+
const result = useVariableSets({ ...options, ...(legacyClient ? { client: legacyClient } : {}) });
|
|
162
|
+
return { ...result, environments: result.variableSets };
|
|
163
|
+
}
|
|
@@ -61,7 +61,9 @@ const isImage = (file: File): boolean => file.type.startsWith("image/");
|
|
|
61
61
|
* `resources`. Workspace-scoped, so it resolves both client and workspace from
|
|
62
62
|
* the {@link OpenGeniProvider} (or a per-call `{ client, workspaceId }`).
|
|
63
63
|
*/
|
|
64
|
-
export function useFileAttachments(
|
|
64
|
+
export function useFileAttachments(
|
|
65
|
+
options: UseFileAttachmentsOptions = {},
|
|
66
|
+
): UseFileAttachmentsResult {
|
|
65
67
|
const { client, workspaceId } = useOpenGeni(options);
|
|
66
68
|
const pasteFilter = options.pasteFilter ?? isImage;
|
|
67
69
|
const [attachments, setAttachments] = useState<FileAttachment[]>([]);
|
|
@@ -72,60 +74,102 @@ export function useFileAttachments(options: UseFileAttachmentsOptions = {}): Use
|
|
|
72
74
|
// Run (or re-run) the upload for one already-tracked attachment id. Sets it
|
|
73
75
|
// back to `uploading`, then resolves to `ready` (with the asset) or `failed`
|
|
74
76
|
// (with the error message).
|
|
75
|
-
const startUpload = useCallback(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
77
|
+
const startUpload = useCallback(
|
|
78
|
+
(id: string, file: File) => {
|
|
79
|
+
void client
|
|
80
|
+
.uploadFile(workspaceId, {
|
|
81
|
+
filename: file.name || "file",
|
|
82
|
+
contentType: file.type || "application/octet-stream",
|
|
83
|
+
data: file,
|
|
84
|
+
})
|
|
85
|
+
.then((asset) => {
|
|
86
|
+
setAttachments((current) =>
|
|
87
|
+
current.map((attachment) =>
|
|
88
|
+
attachment.id === id
|
|
89
|
+
? {
|
|
90
|
+
...attachment,
|
|
91
|
+
status: "ready",
|
|
92
|
+
file: asset,
|
|
93
|
+
name: asset.filename,
|
|
94
|
+
contentType: asset.contentType,
|
|
95
|
+
sizeBytes: asset.sizeBytes,
|
|
96
|
+
error: undefined,
|
|
97
|
+
}
|
|
98
|
+
: attachment,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
})
|
|
102
|
+
.catch((error: unknown) => {
|
|
103
|
+
setAttachments((current) =>
|
|
104
|
+
current.map((attachment) =>
|
|
105
|
+
attachment.id === id
|
|
106
|
+
? {
|
|
107
|
+
...attachment,
|
|
108
|
+
status: "failed",
|
|
109
|
+
error: error instanceof Error ? error.message : String(error),
|
|
110
|
+
}
|
|
111
|
+
: attachment,
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
[client, workspaceId],
|
|
117
|
+
);
|
|
90
118
|
|
|
91
|
-
const addFiles = useCallback(
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
119
|
+
const addFiles = useCallback(
|
|
120
|
+
(files: Iterable<File>) => {
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
const id = crypto.randomUUID();
|
|
123
|
+
sources.current.set(id, file);
|
|
124
|
+
const previewUrl = isImage(file) ? URL.createObjectURL(file) : undefined;
|
|
125
|
+
setAttachments((current) => [
|
|
126
|
+
...current,
|
|
127
|
+
{
|
|
128
|
+
id,
|
|
129
|
+
name: file.name || "image",
|
|
130
|
+
contentType: file.type || "application/octet-stream",
|
|
131
|
+
sizeBytes: file.size,
|
|
132
|
+
status: "uploading",
|
|
133
|
+
...(previewUrl ? { previewUrl } : {}),
|
|
134
|
+
},
|
|
135
|
+
]);
|
|
136
|
+
startUpload(id, file);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
[startUpload],
|
|
140
|
+
);
|
|
107
141
|
|
|
108
|
-
const retry = useCallback(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
142
|
+
const retry = useCallback(
|
|
143
|
+
(id: string) => {
|
|
144
|
+
const file = sources.current.get(id);
|
|
145
|
+
if (!file) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
setAttachments((current) =>
|
|
149
|
+
current.map((attachment) =>
|
|
150
|
+
attachment.id === id
|
|
151
|
+
? { ...attachment, status: "uploading", error: undefined }
|
|
152
|
+
: attachment,
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
startUpload(id, file);
|
|
156
|
+
},
|
|
157
|
+
[startUpload],
|
|
158
|
+
);
|
|
118
159
|
|
|
119
|
-
const addFromPaste = useCallback(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
160
|
+
const addFromPaste = useCallback(
|
|
161
|
+
(event: { clipboardData: DataTransfer | null }) => {
|
|
162
|
+
const clipboardFiles = event.clipboardData?.files;
|
|
163
|
+
if (!clipboardFiles) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const files = [...clipboardFiles].filter(pasteFilter);
|
|
167
|
+
if (files.length > 0) {
|
|
168
|
+
addFiles(files);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
[addFiles, pasteFilter],
|
|
172
|
+
);
|
|
129
173
|
|
|
130
174
|
const remove = useCallback((id: string) => {
|
|
131
175
|
sources.current.delete(id);
|
|
@@ -152,9 +196,11 @@ export function useFileAttachments(options: UseFileAttachmentsOptions = {}): Use
|
|
|
152
196
|
|
|
153
197
|
return {
|
|
154
198
|
attachments,
|
|
155
|
-
readyResources: attachments.flatMap((attachment): FileResourceRef[] =>
|
|
156
|
-
|
|
157
|
-
|
|
199
|
+
readyResources: attachments.flatMap((attachment): FileResourceRef[] =>
|
|
200
|
+
attachment.status === "ready" && attachment.file
|
|
201
|
+
? [{ kind: "file", fileId: attachment.file.id }]
|
|
202
|
+
: [],
|
|
203
|
+
),
|
|
158
204
|
uploading: attachments.some((attachment) => attachment.status === "uploading"),
|
|
159
205
|
addFiles,
|
|
160
206
|
addFromPaste,
|
package/src/hooks/use-goal.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { OpenGeniApiError, type SessionEvent, type SessionGoal } from "@opengeni/sdk";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useDebouncedCallback,
|
|
6
|
+
useMutationRunner,
|
|
7
|
+
useSessionEventTrigger,
|
|
8
|
+
type SessionEventFeedOptions,
|
|
9
|
+
} from "./internal";
|
|
5
10
|
|
|
6
11
|
/** Event types that change the session goal (set/updated/completed/paused/...). */
|
|
7
12
|
export function isGoalEvent(event: Pick<SessionEvent, "type">): boolean {
|
|
8
13
|
return event.type.startsWith("goal.");
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
export type UseGoalOptions = ClientOverride &
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
export type UseGoalOptions = ClientOverride &
|
|
17
|
+
SessionEventFeedOptions & {
|
|
18
|
+
/** Optional safety-net polling (ms). Off by default — goal.* events drive updates. */
|
|
19
|
+
pollIntervalMs?: number | undefined;
|
|
20
|
+
};
|
|
15
21
|
|
|
16
22
|
export type UseGoalResult = {
|
|
17
23
|
/** The session goal, or null when the session has none. */
|
|
@@ -27,7 +33,11 @@ export type UseGoalResult = {
|
|
|
27
33
|
pause: (rationale?: string) => Promise<SessionGoal | null>;
|
|
28
34
|
/** Resume a paused goal: resets counters and re-arms continuations. */
|
|
29
35
|
resume: () => Promise<SessionGoal | null>;
|
|
30
|
-
/**
|
|
36
|
+
/** Clear the session goal; goal-less sessions remain a successful no-op. */
|
|
37
|
+
clearGoal: () => Promise<void>;
|
|
38
|
+
/** Alias for `clearGoal`. */
|
|
39
|
+
deleteGoal: () => Promise<void>;
|
|
40
|
+
/** True while a pause/resume/clear is in flight. */
|
|
31
41
|
updating: boolean;
|
|
32
42
|
mutationError: Error | null;
|
|
33
43
|
clearMutationError: () => void;
|
|
@@ -39,7 +49,10 @@ export type UseGoalResult = {
|
|
|
39
49
|
* `goal: null` (the 404 is absorbed). Live-updates on `goal.*` events —
|
|
40
50
|
* pass `options.events` from `useSessionEvents` to reuse its stream.
|
|
41
51
|
*/
|
|
42
|
-
export function useGoal(
|
|
52
|
+
export function useGoal(
|
|
53
|
+
sessionId: string | null | undefined,
|
|
54
|
+
options: UseGoalOptions = {},
|
|
55
|
+
): UseGoalResult {
|
|
43
56
|
const { client, workspaceId } = useOpenGeni(options);
|
|
44
57
|
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
45
58
|
const sharedEvents = options.events;
|
|
@@ -47,7 +60,7 @@ export function useGoal(sessionId: string | null | undefined, options: UseGoalOp
|
|
|
47
60
|
const [goal, setGoal] = useState<SessionGoal | null>(null);
|
|
48
61
|
const [loading, setLoading] = useState(enabled);
|
|
49
62
|
const [error, setError] = useState<Error | null>(null);
|
|
50
|
-
const
|
|
63
|
+
const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
|
|
51
64
|
const generation = useRef(0);
|
|
52
65
|
const targetKeyRef = useRef<string | null>(null);
|
|
53
66
|
|
|
@@ -121,29 +134,50 @@ export function useGoal(sessionId: string | null | undefined, options: UseGoalOp
|
|
|
121
134
|
if (!sessionId) {
|
|
122
135
|
return null;
|
|
123
136
|
}
|
|
124
|
-
const result = await
|
|
137
|
+
const result = await run(() =>
|
|
125
138
|
client.updateGoal(workspaceId, sessionId, {
|
|
126
139
|
status: "paused",
|
|
127
140
|
...(rationale !== undefined ? { rationale } : {}),
|
|
128
|
-
})
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
129
143
|
if (result) {
|
|
130
144
|
setGoal(result);
|
|
131
145
|
}
|
|
132
146
|
return result;
|
|
133
147
|
},
|
|
134
|
-
[client, workspaceId, sessionId,
|
|
148
|
+
[client, workspaceId, sessionId, run],
|
|
135
149
|
);
|
|
136
150
|
|
|
137
151
|
const resume = useCallback(async (): Promise<SessionGoal | null> => {
|
|
138
152
|
if (!sessionId) {
|
|
139
153
|
return null;
|
|
140
154
|
}
|
|
141
|
-
const result = await
|
|
155
|
+
const result = await run(() => client.updateGoal(workspaceId, sessionId, { status: "active" }));
|
|
142
156
|
if (result) {
|
|
143
157
|
setGoal(result);
|
|
144
158
|
}
|
|
145
159
|
return result;
|
|
146
|
-
}, [client, workspaceId, sessionId,
|
|
160
|
+
}, [client, workspaceId, sessionId, run]);
|
|
161
|
+
|
|
162
|
+
const clearGoal = useCallback(async (): Promise<void> => {
|
|
163
|
+
if (!sessionId) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// deleteGoal resolves void, so distinguish success (a truthy sentinel) from
|
|
167
|
+
// mutation.run's null-on-failure. Only a SUCCESSFUL delete hides the goal:
|
|
168
|
+
// a failed one leaves the pill up so its mutationError renders. And invalidate
|
|
169
|
+
// any in-flight load first so a slower getGoal started before the delete can't
|
|
170
|
+
// commit and repopulate the just-cleared goal.
|
|
171
|
+
const ok = await run(async () => {
|
|
172
|
+
await client.deleteGoal(workspaceId, sessionId);
|
|
173
|
+
return true as const;
|
|
174
|
+
});
|
|
175
|
+
if (ok) {
|
|
176
|
+
generation.current += 1;
|
|
177
|
+
setGoal(null);
|
|
178
|
+
setError(null);
|
|
179
|
+
}
|
|
180
|
+
}, [client, workspaceId, sessionId, run]);
|
|
147
181
|
|
|
148
182
|
return {
|
|
149
183
|
goal,
|
|
@@ -155,8 +189,10 @@ export function useGoal(sessionId: string | null | undefined, options: UseGoalOp
|
|
|
155
189
|
refresh: load,
|
|
156
190
|
pause,
|
|
157
191
|
resume,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
192
|
+
clearGoal,
|
|
193
|
+
deleteGoal: clearGoal,
|
|
194
|
+
updating: mutating,
|
|
195
|
+
mutationError,
|
|
196
|
+
clearMutationError,
|
|
161
197
|
};
|
|
162
198
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { MachineState } from "@opengeni/sdk";
|
|
3
|
+
import type { SessionCapabilitiesState } from "./use-session-capabilities";
|
|
4
|
+
|
|
5
|
+
/** The one truthful machine indicator (dossier §3 #10). Honest staleness beats
|
|
6
|
+
* fake liveness: a cold/asleep box reads "offline — as of <time>", never "live". */
|
|
7
|
+
export type MachineChipState = "live" | "waking" | "offline";
|
|
8
|
+
|
|
9
|
+
export type MachineChip = {
|
|
10
|
+
state: MachineChipState;
|
|
11
|
+
/** A ready-to-render label ("Live" / "Waking…" / "Offline — as of 3m ago"). */
|
|
12
|
+
label: string;
|
|
13
|
+
/** The capture time backing an offline/stale label (ISO), or null. M4 may
|
|
14
|
+
* reformat this; `label` already embeds a relative form. */
|
|
15
|
+
asOf: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type DeriveMachineChipInput = {
|
|
19
|
+
/** `capabilities.liveness` — "warm" | "draining" | "cold" (or null pre-negotiation). */
|
|
20
|
+
liveness?: string | null | undefined;
|
|
21
|
+
/** `useSessionCapabilities().state` — drives the "waking" (negotiating) read. */
|
|
22
|
+
capabilitiesState?: SessionCapabilitiesState | null | undefined;
|
|
23
|
+
/** The ACTIVE machine's connection state (from `useMachines`), when known. A
|
|
24
|
+
* self-hosted machine that is `offline` cannot be remotely woken. */
|
|
25
|
+
activeMachineState?: MachineState | null | undefined;
|
|
26
|
+
/** Whether the active sandbox is a user-owned self-hosted machine (no remote wake). */
|
|
27
|
+
activeIsSelfhosted?: boolean | undefined;
|
|
28
|
+
/** An interaction (wake-on-edit, terminal focus) is actively warming the box. */
|
|
29
|
+
wantsWarm?: boolean | undefined;
|
|
30
|
+
/** The latest capture's `capturedAt` (ISO) — the "as of <time>" backing. */
|
|
31
|
+
capturedAt?: string | null | undefined;
|
|
32
|
+
/** Injectable clock for deterministic relative labels (defaults to Date.now). */
|
|
33
|
+
now?: number | undefined;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Compact relative-time for the "as of" label. Pure, no Intl dependency. */
|
|
37
|
+
export function formatAsOf(capturedAt: string, now: number): string {
|
|
38
|
+
const then = Date.parse(capturedAt);
|
|
39
|
+
if (Number.isNaN(then)) return "recently";
|
|
40
|
+
const seconds = Math.max(0, Math.round((now - then) / 1000));
|
|
41
|
+
if (seconds < 45) return "just now";
|
|
42
|
+
const minutes = Math.round(seconds / 60);
|
|
43
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
44
|
+
const hours = Math.round(minutes / 60);
|
|
45
|
+
if (hours < 24) return `${hours}h ago`;
|
|
46
|
+
const days = Math.round(hours / 24);
|
|
47
|
+
return `${days}d ago`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Derive the machine-state chip from the live capability/liveness surface, the
|
|
52
|
+
* active machine's connection state, and the latest capture time. PURE (M4 renders
|
|
53
|
+
* it; the whole point is a deterministic, testable projection).
|
|
54
|
+
*
|
|
55
|
+
* Precedence:
|
|
56
|
+
* 1. warm/draining lease → **live** (a warm box always wins).
|
|
57
|
+
* 2. actively warming (negotiating, wake-on-edit, or a reconnecting/enrolling
|
|
58
|
+
* machine) → **waking**.
|
|
59
|
+
* 3. everything else (a cold/asleep box, a self-hosted machine that's offline,
|
|
60
|
+
* an error) → **offline**, labelled "as of <capture time>".
|
|
61
|
+
*/
|
|
62
|
+
export function deriveMachineChip(input: DeriveMachineChipInput): MachineChip {
|
|
63
|
+
const now = input.now ?? Date.now();
|
|
64
|
+
const asOf = input.capturedAt ?? null;
|
|
65
|
+
const offlineLabel = asOf ? `Offline — as of ${formatAsOf(asOf, now)}` : "Offline";
|
|
66
|
+
|
|
67
|
+
// 1. A warm (or draining) lease is live regardless of anything else.
|
|
68
|
+
if (input.liveness === "warm" || input.liveness === "draining") {
|
|
69
|
+
return { state: "live", label: "Live", asOf: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A self-hosted machine that reports offline cannot be woken remotely — honest
|
|
73
|
+
// "offline", never "waking" (dossier §3 #5 / #10).
|
|
74
|
+
const selfhostedOffline =
|
|
75
|
+
input.activeIsSelfhosted === true && input.activeMachineState === "offline";
|
|
76
|
+
|
|
77
|
+
// 2. Actively coming up: negotiation in flight, an edit/terminal is warming, or
|
|
78
|
+
// the active machine is (re)connecting. Never when self-hosted is hard-offline.
|
|
79
|
+
const machineWarming =
|
|
80
|
+
input.activeMachineState === "reconnecting" || input.activeMachineState === "enrolling";
|
|
81
|
+
if (
|
|
82
|
+
!selfhostedOffline &&
|
|
83
|
+
(input.capabilitiesState === "negotiating" || input.wantsWarm === true || machineWarming)
|
|
84
|
+
) {
|
|
85
|
+
return { state: "waking", label: "Waking…", asOf };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 3. Cold / asleep / offline / error → honest stale label.
|
|
89
|
+
return { state: "offline", label: offlineLabel, asOf };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type UseMachineChipOptions = DeriveMachineChipInput;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Thin memoized wrapper over `deriveMachineChip` for the dock header (M4). The
|
|
96
|
+
* dock already runs `useSessionCapabilities` + `useMachines` + `useWorkspace
|
|
97
|
+
* capture`; it feeds their `liveness` / `state` / active-machine state /
|
|
98
|
+
* `capturedAt` here. Pass a periodically-updated `now` if you want the relative
|
|
99
|
+
* "as of" label to tick.
|
|
100
|
+
*/
|
|
101
|
+
export function useMachineChip(input: UseMachineChipOptions): MachineChip {
|
|
102
|
+
const {
|
|
103
|
+
liveness,
|
|
104
|
+
capabilitiesState,
|
|
105
|
+
activeMachineState,
|
|
106
|
+
activeIsSelfhosted,
|
|
107
|
+
wantsWarm,
|
|
108
|
+
capturedAt,
|
|
109
|
+
now,
|
|
110
|
+
} = input;
|
|
111
|
+
return useMemo(
|
|
112
|
+
() =>
|
|
113
|
+
deriveMachineChip({
|
|
114
|
+
liveness,
|
|
115
|
+
capabilitiesState,
|
|
116
|
+
activeMachineState,
|
|
117
|
+
activeIsSelfhosted,
|
|
118
|
+
wantsWarm,
|
|
119
|
+
capturedAt,
|
|
120
|
+
now,
|
|
121
|
+
}),
|
|
122
|
+
// Depend on the primitive fields (not the object identity) so a caller passing
|
|
123
|
+
// a fresh object each render doesn't recompute needlessly.
|
|
124
|
+
[
|
|
125
|
+
liveness,
|
|
126
|
+
capabilitiesState,
|
|
127
|
+
activeMachineState,
|
|
128
|
+
activeIsSelfhosted,
|
|
129
|
+
wantsWarm,
|
|
130
|
+
capturedAt,
|
|
131
|
+
now,
|
|
132
|
+
],
|
|
133
|
+
);
|
|
134
|
+
}
|