@opengeni/react 1.4.4 → 1.4.5-canary.37458084490001
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 +17 -6
- package/dist/artifacts.d.ts +1 -0
- package/dist/artifacts.js +22 -15
- package/dist/artifacts.js.map +1 -1
- package/dist/{browser-viewer-5EEAS3IU.js → browser-viewer-CV23VIHB.js} +8 -6
- package/dist/chunk-3ZC6QPOM.js +174 -0
- package/dist/chunk-3ZC6QPOM.js.map +1 -0
- package/dist/{chunk-YVFRBXST.js → chunk-4BCVCJSU.js} +2 -2
- package/dist/{chunk-JHTIMBOL.js → chunk-5JSUNNQN.js} +26 -4
- package/dist/{chunk-JHTIMBOL.js.map → chunk-5JSUNNQN.js.map} +1 -1
- package/dist/{chunk-I6OY3MWC.js → chunk-6FOTSVWU.js} +2 -2
- package/dist/{chunk-IUHHPMAX.js → chunk-7RRBX5EM.js} +14 -85
- package/dist/chunk-7RRBX5EM.js.map +1 -0
- package/dist/{chunk-L2J3PU7A.js → chunk-7ZB7I3KP.js} +24 -98
- package/dist/chunk-7ZB7I3KP.js.map +1 -0
- package/dist/{chunk-LVS3MH5T.js → chunk-ATIRUHBZ.js} +128 -10
- package/dist/chunk-ATIRUHBZ.js.map +1 -0
- package/dist/chunk-BOFEQC6K.js +162 -0
- package/dist/chunk-BOFEQC6K.js.map +1 -0
- package/dist/{chunk-2OQA3CPB.js → chunk-CESBPQP5.js} +19 -5
- package/dist/chunk-CESBPQP5.js.map +1 -0
- package/dist/{chunk-IYQCWGU5.js → chunk-GHJU7XG7.js} +35 -62
- package/dist/chunk-GHJU7XG7.js.map +1 -0
- package/dist/{chunk-G6YRCIYJ.js → chunk-HNVWMDNN.js} +55 -60
- package/dist/chunk-HNVWMDNN.js.map +1 -0
- package/dist/{chunk-PHBFJCEU.js → chunk-I5EGRXXG.js} +110 -7
- package/dist/chunk-I5EGRXXG.js.map +1 -0
- package/dist/{chunk-PBCZL4JP.js → chunk-INIZSNXT.js} +2 -2
- package/dist/{chunk-XS67YG5R.js → chunk-IX3K4THH.js} +24 -3
- package/dist/chunk-IX3K4THH.js.map +1 -0
- package/dist/chunk-IZV5FTJC.js +55 -0
- package/dist/chunk-IZV5FTJC.js.map +1 -0
- package/dist/chunk-JDJJMBB3.js +1883 -0
- package/dist/chunk-JDJJMBB3.js.map +1 -0
- package/dist/{chunk-C5BF4L26.js → chunk-KHF2VRXF.js} +7 -7
- package/dist/{chunk-QMMCJGFI.js → chunk-L5OKM2VX.js} +2 -2
- package/dist/chunk-LHF6BVM4.js +83 -0
- package/dist/chunk-LHF6BVM4.js.map +1 -0
- package/dist/chunk-N7S72EXK.js +25 -0
- package/dist/chunk-N7S72EXK.js.map +1 -0
- package/dist/{chunk-CVYCLFMH.js → chunk-PEHTX3V7.js} +19 -1
- package/dist/chunk-PEHTX3V7.js.map +1 -0
- package/dist/chunk-R5R5RQQD.js +51 -0
- package/dist/chunk-R5R5RQQD.js.map +1 -0
- package/dist/chunk-RI42H5IU.js +326 -0
- package/dist/chunk-RI42H5IU.js.map +1 -0
- package/dist/{chunk-JH7V627O.js → chunk-SHLZMAJN.js} +2 -11
- package/dist/chunk-SHLZMAJN.js.map +1 -0
- package/dist/{chunk-EAOSEIXB.js → chunk-TZQSHCZN.js} +692 -1996
- package/dist/chunk-TZQSHCZN.js.map +1 -0
- package/dist/chunk-ZIB4H5LN.js +101 -0
- package/dist/chunk-ZIB4H5LN.js.map +1 -0
- package/dist/client.d.ts +9 -3
- package/dist/clipboard.js +1 -1
- package/dist/components/browser-viewer.d.ts +6 -0
- package/dist/components/composer.d.ts +3 -69
- package/dist/components/human-input-form.d.ts +3 -32
- package/dist/components/markdown.d.ts +8 -0
- package/dist/components/model-policy-picker.d.ts +4 -37
- package/dist/components/opper-mark.d.ts +7 -0
- package/dist/components/provider-recovery-notice.d.ts +15 -0
- package/dist/components/remark-soft-line-breaks.d.ts +38 -0
- package/dist/components/sandbox-workspace.d.ts +13 -0
- package/dist/components/session-chrome.d.ts +5 -3
- package/dist/components/session-conversation.d.ts +15 -3
- package/dist/composer-messages.d.ts +74 -0
- package/dist/composer-messages.js +9 -0
- package/dist/composer.js +15 -10
- package/dist/{computer-viewer-ELNLZGHI.js → computer-viewer-K4CKYRX6.js} +4 -4
- package/dist/connect-setup.d.ts +2 -0
- package/dist/connect.d.ts +1 -1
- package/dist/connect.js +21 -6
- package/dist/connect.js.map +1 -1
- package/dist/{embedded-voice-NIL4RDX3.js → embedded-voice-3ZFWWJ6O.js} +6 -4
- package/dist/{embedded-voice-NIL4RDX3.js.map → embedded-voice-3ZFWWJ6O.js.map} +1 -1
- package/dist/hooks/shared-machines-list.d.ts +4 -0
- package/dist/hooks/use-client-config-flags.d.ts +6 -0
- package/dist/hooks/use-goal.d.ts +2 -1
- package/dist/human-input-model.d.ts +40 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +116 -73
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +5 -5
- package/dist/{latest-question-2POFPYNB.js → latest-question-SWC6QEVN.js} +3 -3
- package/dist/lib/clipboard.d.ts +7 -0
- package/dist/lib/provider-recovery.d.ts +49 -0
- package/dist/machines.js +2 -2
- package/dist/model-mark-paths.d.ts +34 -0
- package/dist/model-mark-paths.js +16 -0
- package/dist/model-picker-model.d.ts +111 -0
- package/dist/model-picker-model.js +25 -0
- package/dist/model-picker-model.js.map +1 -0
- package/dist/{model-policy-picker-menu-WRNSRFQC.js → model-policy-picker-menu-NM4RPTY6.js} +6 -4
- package/dist/model-policy-picker-menu-NM4RPTY6.js.map +1 -0
- package/dist/model-policy.d.ts +10 -0
- package/dist/model-policy.js +5 -1
- package/dist/organization-model.d.ts +32 -0
- package/dist/organization-model.js +65 -0
- package/dist/organization-model.js.map +1 -0
- package/dist/{platform-activity-row-PJJVATTZ.js → platform-activity-row-NJ47YBXW.js} +7 -84
- package/dist/platform-activity-row-NJ47YBXW.js.map +1 -0
- package/dist/queue-presentation-model.d.ts +72 -0
- package/dist/queue-presentation-model.js +38 -0
- package/dist/queue-presentation-model.js.map +1 -0
- package/dist/{queue-surface-implementation-67FS7FUI.js → queue-surface-implementation-YPJFQ3O6.js} +24 -252
- package/dist/queue-surface-implementation-YPJFQ3O6.js.map +1 -0
- package/dist/realtime.js +5 -3
- package/dist/{sandbox-files-34CJJNOY.js → sandbox-files-RCVCMXG7.js} +5 -5
- package/dist/sandbox-files-RCVCMXG7.js.map +1 -0
- package/dist/sandbox-label-model.d.ts +23 -0
- package/dist/sandbox-label-model.js +28 -0
- package/dist/sandbox-label-model.js.map +1 -0
- package/dist/session-attention-model.d.ts +8 -0
- package/dist/session-attention-model.js +46 -0
- package/dist/session-attention-model.js.map +1 -0
- package/dist/session-feedback-model.d.ts +20 -0
- package/dist/session-feedback-model.js +56 -0
- package/dist/session-feedback-model.js.map +1 -0
- package/dist/session-list-model.d.ts +96 -0
- package/dist/session-list-model.js +219 -0
- package/dist/session-list-model.js.map +1 -0
- package/dist/session-status-model.d.ts +19 -0
- package/dist/session-ui.js +26 -17
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +8 -6
- package/dist/testing/session-scenarios.d.ts +10 -0
- package/dist/testing/session-scenarios.js +303 -0
- package/dist/testing/session-scenarios.js.map +1 -0
- package/dist/timeline/genie-copy.d.ts +3 -0
- package/dist/timeline/genie-loading.d.ts +11 -0
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/notice-presentation.d.ts +7 -0
- package/dist/timeline/patch-apply-command.d.ts +18 -0
- package/dist/timeline/platform-activity-presentation.d.ts +7 -0
- package/dist/timeline/presented-image.d.ts +6 -1
- package/dist/timeline/question-nav-model.d.ts +23 -0
- package/dist/timeline/registry.d.ts +10 -1
- package/dist/timeline/retained-loaders.d.ts +1 -1
- package/dist/timeline/tool-presentation.d.ts +172 -0
- package/dist/timeline/turn-summary-model.d.ts +90 -0
- package/dist/timeline/turn-summary.d.ts +4 -74
- package/dist/timeline/work-presentation.d.ts +42 -0
- package/dist/timeline-model.d.ts +24 -0
- package/dist/timeline-model.js +152 -0
- package/dist/timeline-model.js.map +1 -0
- package/package.json +47 -3
- package/src/artifacts.ts +1 -0
- package/src/client.ts +9 -3
- package/src/components/browser-viewer.tsx +23 -2
- package/src/components/claude-mark.tsx +2 -1
- package/src/components/composer.tsx +4 -144
- package/src/components/grok-mark.tsx +2 -1
- package/src/components/human-input-form.tsx +13 -124
- package/src/components/markdown.tsx +19 -1
- package/src/components/message-timeline.tsx +45 -99
- package/src/components/model-mark.tsx +2 -1
- package/src/components/model-policy-picker-menu.tsx +30 -85
- package/src/components/model-policy-picker.tsx +15 -148
- package/src/components/opper-mark.tsx +18 -0
- package/src/components/provider-recovery-notice.tsx +41 -0
- package/src/components/queue-surface-implementation.tsx +20 -349
- package/src/components/remark-soft-line-breaks.ts +166 -0
- package/src/components/sandbox-workspace.tsx +17 -1
- package/src/components/session-chrome.tsx +114 -99
- package/src/components/session-conversation.tsx +55 -6
- package/src/components/session-status.tsx +17 -16
- package/src/composer-messages.ts +150 -0
- package/src/connect-setup.tsx +45 -11
- package/src/connect.ts +1 -1
- package/src/hooks/shared-machines-list.ts +34 -1
- package/src/hooks/use-client-config-flags.ts +8 -0
- package/src/hooks/use-goal.ts +66 -10
- package/src/human-input-model.ts +147 -0
- package/src/index.ts +16 -0
- package/src/lib/clipboard.ts +29 -0
- package/src/lib/format.ts +7 -0
- package/src/lib/provider-recovery.ts +202 -0
- package/src/model-mark-paths.ts +53 -0
- package/src/model-picker-model.ts +330 -0
- package/src/model-policy.ts +43 -2
- package/src/organization-model.ts +118 -0
- package/src/queue-presentation-model.ts +464 -0
- package/src/sandbox-label-model.ts +41 -0
- package/src/session-attention-model.ts +59 -0
- package/src/session-feedback-model.ts +95 -0
- package/src/session-list-model.ts +327 -0
- package/src/session-status-model.ts +62 -0
- package/src/testing/session-scenarios.ts +326 -0
- package/src/timeline/activity-rail.tsx +5 -17
- package/src/timeline/genie-copy.ts +19 -0
- package/src/timeline/genie-loading.tsx +13 -18
- package/src/timeline/index.ts +1 -0
- package/src/timeline/notice-presentation.ts +22 -0
- package/src/timeline/patch-apply-command.tsx +135 -0
- package/src/timeline/platform-activity-presentation.ts +122 -0
- package/src/timeline/platform-activity-row.tsx +6 -96
- package/src/timeline/presented-image.ts +16 -2
- package/src/timeline/question-nav-model.ts +28 -0
- package/src/timeline/registry.ts +11 -1
- package/src/timeline/retained-loaders.ts +9 -4
- package/src/timeline/rolling-activity.tsx +7 -19
- package/src/timeline/tool-presentation.ts +1316 -0
- package/src/timeline/tool-renderers.tsx +226 -1025
- package/src/timeline/turn-summary-model.ts +338 -0
- package/src/timeline/turn-summary.tsx +21 -326
- package/src/timeline/work-presentation.ts +139 -0
- package/src/timeline-model.ts +108 -0
- package/styles/compiled.css +3 -0
- package/styles/connect.css +4 -0
- package/dist/chunk-2OQA3CPB.js.map +0 -1
- package/dist/chunk-CVYCLFMH.js.map +0 -1
- package/dist/chunk-EAOSEIXB.js.map +0 -1
- package/dist/chunk-G6YRCIYJ.js.map +0 -1
- package/dist/chunk-HXYOSSQO.js +0 -117
- package/dist/chunk-HXYOSSQO.js.map +0 -1
- package/dist/chunk-IUHHPMAX.js.map +0 -1
- package/dist/chunk-IYQCWGU5.js.map +0 -1
- package/dist/chunk-JH7V627O.js.map +0 -1
- package/dist/chunk-L2J3PU7A.js.map +0 -1
- package/dist/chunk-LVS3MH5T.js.map +0 -1
- package/dist/chunk-PHBFJCEU.js.map +0 -1
- package/dist/chunk-XS67YG5R.js.map +0 -1
- package/dist/platform-activity-row-PJJVATTZ.js.map +0 -1
- package/dist/queue-surface-implementation-67FS7FUI.js.map +0 -1
- /package/dist/{browser-viewer-5EEAS3IU.js.map → browser-viewer-CV23VIHB.js.map} +0 -0
- /package/dist/{chunk-YVFRBXST.js.map → chunk-4BCVCJSU.js.map} +0 -0
- /package/dist/{chunk-I6OY3MWC.js.map → chunk-6FOTSVWU.js.map} +0 -0
- /package/dist/{chunk-PBCZL4JP.js.map → chunk-INIZSNXT.js.map} +0 -0
- /package/dist/{chunk-C5BF4L26.js.map → chunk-KHF2VRXF.js.map} +0 -0
- /package/dist/{chunk-QMMCJGFI.js.map → chunk-L5OKM2VX.js.map} +0 -0
- /package/dist/{computer-viewer-ELNLZGHI.js.map → composer-messages.js.map} +0 -0
- /package/dist/{model-policy-picker-menu-WRNSRFQC.js.map → computer-viewer-K4CKYRX6.js.map} +0 -0
- /package/dist/{latest-question-2POFPYNB.js.map → latest-question-SWC6QEVN.js.map} +0 -0
- /package/dist/{sandbox-files-34CJJNOY.js.map → model-mark-paths.js.map} +0 -0
package/src/model-policy.ts
CHANGED
|
@@ -55,7 +55,11 @@ const AVAILABILITY_REASON_LABELS: Record<string, string> = {
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
export function billingClassForModel(model: ClientModel): PickerBillingClass {
|
|
58
|
-
if (
|
|
58
|
+
if (
|
|
59
|
+
model.provider === "organization-gateway" ||
|
|
60
|
+
model.provider === "organization-openrouter" ||
|
|
61
|
+
model.provider === "organization-opper"
|
|
62
|
+
) {
|
|
59
63
|
return "organization_byok";
|
|
60
64
|
}
|
|
61
65
|
if (
|
|
@@ -63,7 +67,11 @@ export function billingClassForModel(model: ClientModel): PickerBillingClass {
|
|
|
63
67
|
model.provider?.startsWith("workspace-azure-openai-")
|
|
64
68
|
)
|
|
65
69
|
return "byok";
|
|
66
|
-
if (
|
|
70
|
+
if (
|
|
71
|
+
model.provider === "workspace-gateway" ||
|
|
72
|
+
model.provider === "workspace-openrouter" ||
|
|
73
|
+
model.provider === "workspace-opper"
|
|
74
|
+
) {
|
|
67
75
|
return "byok";
|
|
68
76
|
}
|
|
69
77
|
return modelPickerBillingClassFor(model);
|
|
@@ -116,6 +124,30 @@ export function coerceReasoningEffortForModel(
|
|
|
116
124
|
return defaultEffortForModel(model);
|
|
117
125
|
}
|
|
118
126
|
|
|
127
|
+
/** Whether the composer offers (and therefore labels) a reasoning-effort choice. */
|
|
128
|
+
export function modelOffersEffortChoice(model: ClientModel): boolean {
|
|
129
|
+
return (
|
|
130
|
+
effortOptionsForModel(model).length > 1 && model.capabilities?.reasoning.runnable !== false
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The composer model pill at phone width: the catalog's compact name (falling
|
|
136
|
+
* back to the display name) and the effort label when effort is a choice.
|
|
137
|
+
*/
|
|
138
|
+
export function compactModelPill(
|
|
139
|
+
models: readonly ClientModel[],
|
|
140
|
+
modelId: string,
|
|
141
|
+
effort: ReasoningEffort | null | undefined,
|
|
142
|
+
): { name: string; effort: string | null } {
|
|
143
|
+
const row = findPickerRow(projectClientModelRows([...models]), modelId);
|
|
144
|
+
return {
|
|
145
|
+
name: row?.shortLabel ?? row?.label ?? modelDisplayName(modelId),
|
|
146
|
+
effort:
|
|
147
|
+
row && effort && modelOffersEffortChoice(row.catalog) ? labelReasoningEffort(effort) : null,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
119
151
|
export function runnableLatencyModesForModel(model: ClientModel): LatencyModeId[] {
|
|
120
152
|
const modes = model.capabilities?.latencyModes ?? [];
|
|
121
153
|
return modes.filter((mode) => mode.runnable).map((mode) => mode.id);
|
|
@@ -212,6 +244,9 @@ export function advancedSourceSummary(model: ClientModel): string | null {
|
|
|
212
244
|
if (model.provider === "workspace-openrouter") {
|
|
213
245
|
return "Workspace OpenRouter connection";
|
|
214
246
|
}
|
|
247
|
+
if (model.provider === "workspace-opper") {
|
|
248
|
+
return "Workspace Opper connection";
|
|
249
|
+
}
|
|
215
250
|
if (model.provider === "workspace-gateway" || model.source === "workspace_gateway") {
|
|
216
251
|
return "Workspace Vercel AI Gateway";
|
|
217
252
|
}
|
|
@@ -233,6 +268,9 @@ function workspaceProviderPayerSummary(model: ClientModel): string {
|
|
|
233
268
|
if (model.provider === "workspace-openrouter") {
|
|
234
269
|
return "Billed to the workspace OpenRouter account";
|
|
235
270
|
}
|
|
271
|
+
if (model.provider === "workspace-opper") {
|
|
272
|
+
return "Billed to the workspace Opper account";
|
|
273
|
+
}
|
|
236
274
|
if (model.provider === "workspace-gateway" || model.source === "workspace_gateway") {
|
|
237
275
|
return "Billed to the workspace Vercel account";
|
|
238
276
|
}
|
|
@@ -247,6 +285,9 @@ function organizationProviderPayerSummary(model: ClientModel): string {
|
|
|
247
285
|
if (model.provider === "organization-openrouter") {
|
|
248
286
|
return "Billed to the organization OpenRouter account";
|
|
249
287
|
}
|
|
288
|
+
if (model.provider === "organization-opper") {
|
|
289
|
+
return "Billed to the organization Opper account";
|
|
290
|
+
}
|
|
250
291
|
if (model.provider === "organization-gateway") {
|
|
251
292
|
return "Billed to the organization Vercel account";
|
|
252
293
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Pure organization and workspace selection rules shared by the web rail's
|
|
2
|
+
// switcher and native renderers. The wire model has no account *name*, so a
|
|
3
|
+
// display label is derived from the access grant — preferring the account
|
|
4
|
+
// name, falling back to a short, stable id fragment. No DOM, no React.
|
|
5
|
+
import type { AccessContext, AccountGrant, Workspace } from "@opengeni/sdk";
|
|
6
|
+
|
|
7
|
+
export type OrgOption = {
|
|
8
|
+
accountId: string;
|
|
9
|
+
label: string;
|
|
10
|
+
/** Whether the subject has the owner/admin authority required by organization settings. */
|
|
11
|
+
canManage: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Organizations the subject administers (owner or admin), sorted by id. */
|
|
15
|
+
export function organizationAdministrationAccountIds(accessContext: AccessContext): string[] {
|
|
16
|
+
return accessContext.accountGrants
|
|
17
|
+
.filter(
|
|
18
|
+
(grant) =>
|
|
19
|
+
grant.subjectId === accessContext.subjectId &&
|
|
20
|
+
(grant.role === "owner" || grant.role === "admin"),
|
|
21
|
+
)
|
|
22
|
+
.map((grant) => grant.accountId)
|
|
23
|
+
.sort();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A short, stable id fragment for an account that has no human name. */
|
|
27
|
+
export function shortAccountId(accountId: string): string {
|
|
28
|
+
return accountId.length > 8 ? accountId.slice(0, 8) : accountId;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The organization's real name, or null when the API sent none. */
|
|
32
|
+
export function orgName(accountId: string, grants: AccountGrant[]): string | null {
|
|
33
|
+
const grant = grants.find((candidate) => candidate.accountId === accountId);
|
|
34
|
+
const label =
|
|
35
|
+
grant?.metadata && typeof grant.metadata.accountName === "string"
|
|
36
|
+
? grant.metadata.accountName
|
|
37
|
+
: undefined;
|
|
38
|
+
return label?.trim() || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Human label for an organization: the grant's subjectLabel, else a short id. */
|
|
42
|
+
export function orgLabel(accountId: string, grants: AccountGrant[]): string {
|
|
43
|
+
return orgName(accountId, grants) ?? `Org ${shortAccountId(accountId)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The organizations the subject belongs to, in a stable order (default org
|
|
48
|
+
* first, then by name). Derived from account grants, unioned with the accounts
|
|
49
|
+
* that own the accessible workspaces so an org always shows even without an
|
|
50
|
+
* explicit grant.
|
|
51
|
+
*/
|
|
52
|
+
export function organizationsForSubject(
|
|
53
|
+
context: AccessContext,
|
|
54
|
+
workspaces: Workspace[],
|
|
55
|
+
): OrgOption[] {
|
|
56
|
+
const administeredIds = new Set(organizationAdministrationAccountIds(context));
|
|
57
|
+
const ids = new Set<string>();
|
|
58
|
+
for (const grant of context.accountGrants) {
|
|
59
|
+
ids.add(grant.accountId);
|
|
60
|
+
}
|
|
61
|
+
for (const workspace of workspaces) {
|
|
62
|
+
ids.add(workspace.accountId);
|
|
63
|
+
}
|
|
64
|
+
const options = [...ids].map((accountId) => ({
|
|
65
|
+
accountId,
|
|
66
|
+
label: orgLabel(accountId, context.accountGrants),
|
|
67
|
+
canManage: administeredIds.has(accountId),
|
|
68
|
+
}));
|
|
69
|
+
return options.sort((a, b) => {
|
|
70
|
+
if (a.accountId === context.defaultAccountId) {
|
|
71
|
+
return -1;
|
|
72
|
+
}
|
|
73
|
+
if (b.accountId === context.defaultAccountId) {
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
return a.label.localeCompare(b.label) || a.accountId.localeCompare(b.accountId);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Workspaces that belong to a given organization, ordered by name. */
|
|
81
|
+
export function workspacesInOrg(workspaces: Workspace[], accountId: string): Workspace[] {
|
|
82
|
+
return workspaces
|
|
83
|
+
.filter((workspace) => workspace.accountId === accountId)
|
|
84
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Organization settings remain anchored to an accessible workspace in that organization. */
|
|
88
|
+
export function organizationSettingsWorkspaceId(
|
|
89
|
+
workspaces: Workspace[],
|
|
90
|
+
accountId: string,
|
|
91
|
+
activeWorkspaceId: string,
|
|
92
|
+
): string | null {
|
|
93
|
+
const candidates = workspacesInOrg(workspaces, accountId);
|
|
94
|
+
return (
|
|
95
|
+
candidates.find((workspace) => workspace.id === activeWorkspaceId)?.id ??
|
|
96
|
+
candidates[0]?.id ??
|
|
97
|
+
null
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Where switching to an organization lands: the workspace last used there
|
|
103
|
+
* while it is still open to the person, else its first shared workspace, else
|
|
104
|
+
* their Personal workspace there. Null when nothing in it is open to them.
|
|
105
|
+
*/
|
|
106
|
+
export function organizationLandingWorkspaceId(
|
|
107
|
+
workspaces: Workspace[],
|
|
108
|
+
accountId: string,
|
|
109
|
+
rememberedWorkspaceId?: string | null,
|
|
110
|
+
): string | null {
|
|
111
|
+
const candidates = workspacesInOrg(workspaces, accountId);
|
|
112
|
+
return (
|
|
113
|
+
candidates.find((workspace) => workspace.id === rememberedWorkspaceId)?.id ??
|
|
114
|
+
candidates.find((workspace) => workspace.kind !== "personal")?.id ??
|
|
115
|
+
candidates[0]?.id ??
|
|
116
|
+
null
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue presentation shared by the web queue surface and native docks: the
|
|
3
|
+
* bounded head/tail previews of queued prompts, and the reorder plan and its
|
|
4
|
+
* optimistic projection. No DOM, no React.
|
|
5
|
+
*/
|
|
6
|
+
import type { SessionQueueSnapshot, SessionTurn } from "@opengeni/sdk";
|
|
7
|
+
import { QUEUE_ITEM_CONTENT_UNAVAILABLE, queueItemContent } from "./components/queue-item-content";
|
|
8
|
+
|
|
9
|
+
export { QUEUE_ITEM_CONTENT_UNAVAILABLE, queueItemContent };
|
|
10
|
+
|
|
11
|
+
export const QUEUE_ROW_PREVIEW_CHARACTERS = 360;
|
|
12
|
+
export const QUEUE_COLLAPSED_PREVIEW_CHARACTERS = 180;
|
|
13
|
+
const QUEUE_PREVIEW_GRAPHEME_CONTEXT_CHARACTERS = 32;
|
|
14
|
+
const QUEUE_PREVIEW_REFERENCE_SAMPLE_CHARACTERS = 32;
|
|
15
|
+
const QUEUE_VISIBLE_END_IDENTITY_CHARACTERS = 18;
|
|
16
|
+
const QUEUE_PREVIEW_SEPARATOR = " … ";
|
|
17
|
+
// Hermes (React Native) has no Intl.Segmenter; there, previews fall back to
|
|
18
|
+
// code points, which keeps every boundary rule except grapheme joining.
|
|
19
|
+
let queuePreviewSegmenterCache: Intl.Segmenter | null | undefined;
|
|
20
|
+
function queuePreviewSegments(sample: string): string[] {
|
|
21
|
+
if (queuePreviewSegmenterCache === undefined) {
|
|
22
|
+
queuePreviewSegmenterCache =
|
|
23
|
+
typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"
|
|
24
|
+
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
25
|
+
: null;
|
|
26
|
+
}
|
|
27
|
+
if (!queuePreviewSegmenterCache) return Array.from(sample);
|
|
28
|
+
return Array.from(queuePreviewSegmenterCache.segment(sample), (part) => part.segment);
|
|
29
|
+
}
|
|
30
|
+
const queuePromptNonRenderingCodePoint =
|
|
31
|
+
/[\p{White_Space}\p{Control}\p{Default_Ignorable_Code_Point}]/u;
|
|
32
|
+
|
|
33
|
+
type BoundedPromptSample = {
|
|
34
|
+
value: string;
|
|
35
|
+
characters: number;
|
|
36
|
+
truncated: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type QueuePromptPreview = {
|
|
40
|
+
summary: string;
|
|
41
|
+
collapsedVisual: string;
|
|
42
|
+
visibleStart: string;
|
|
43
|
+
visibleIdentity: string | null;
|
|
44
|
+
visibleIdentityLabel: "End" | "Safe boundary" | null;
|
|
45
|
+
isFallback: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function labelQueuePreview(summary: string): QueuePromptPreview {
|
|
49
|
+
return {
|
|
50
|
+
summary,
|
|
51
|
+
collapsedVisual: summary,
|
|
52
|
+
visibleStart: summary,
|
|
53
|
+
visibleIdentity: null,
|
|
54
|
+
visibleIdentityLabel: null,
|
|
55
|
+
isFallback: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function queueTurnPreview(
|
|
60
|
+
turn: SessionTurn | undefined,
|
|
61
|
+
maxCharacters: number,
|
|
62
|
+
): QueuePromptPreview {
|
|
63
|
+
if (!turn) return queuePromptPreview("", maxCharacters);
|
|
64
|
+
return queueContentPreview(turn.prompt, turn.annotations?.length ?? 0, maxCharacters);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function queueContentPreview(
|
|
68
|
+
prompt: string,
|
|
69
|
+
annotationCount: number,
|
|
70
|
+
maxCharacters: number,
|
|
71
|
+
): QueuePromptPreview {
|
|
72
|
+
switch (queueItemContent(prompt, annotationCount)) {
|
|
73
|
+
case "text":
|
|
74
|
+
return queuePromptPreview(prompt, maxCharacters);
|
|
75
|
+
case "annotations":
|
|
76
|
+
return labelQueuePreview(
|
|
77
|
+
`${annotationCount} timeline ${annotationCount === 1 ? "annotation" : "annotations"}`,
|
|
78
|
+
);
|
|
79
|
+
case "unavailable":
|
|
80
|
+
return labelQueuePreview(QUEUE_ITEM_CONTENT_UNAVAILABLE);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build a bounded head/tail summary of an arbitrary queued prompt. Sampling
|
|
86
|
+
* both ends distinguishes prompts with equal long prefixes. Each sampled edge
|
|
87
|
+
* has a small amount of grapheme context so ordinary emoji/combining sequences
|
|
88
|
+
* can be retained whole. A cluster that exceeds that context is omitted rather
|
|
89
|
+
* than fragmented, and malformed UTF-16 is replaced only in the summary. The
|
|
90
|
+
* durable prompt remains exact and no operation scans or copies it in full.
|
|
91
|
+
*/
|
|
92
|
+
export function queuePromptPreview(prompt: string, maxCharacters: number): QueuePromptPreview {
|
|
93
|
+
const wholePromptProbe = samplePromptStart(prompt, maxCharacters + 1);
|
|
94
|
+
if (!wholePromptProbe.truncated && wholePromptProbe.characters <= maxCharacters) {
|
|
95
|
+
const summary = replaceLoneSurrogates(wholePromptProbe.value);
|
|
96
|
+
return hasVisiblePromptContent(summary)
|
|
97
|
+
? {
|
|
98
|
+
summary,
|
|
99
|
+
collapsedVisual: summary,
|
|
100
|
+
visibleStart: summary,
|
|
101
|
+
visibleIdentity: null,
|
|
102
|
+
visibleIdentityLabel: null,
|
|
103
|
+
isFallback: false,
|
|
104
|
+
}
|
|
105
|
+
: fallbackPromptPreview(prompt.length, wholePromptProbe.value, wholePromptProbe.value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const suffixCharacters = Math.min(Math.floor(maxCharacters / 3), 120);
|
|
109
|
+
const prefixCharacters =
|
|
110
|
+
maxCharacters - codePointLength(QUEUE_PREVIEW_SEPARATOR) - suffixCharacters;
|
|
111
|
+
const prefixSample = samplePromptStart(
|
|
112
|
+
prompt,
|
|
113
|
+
prefixCharacters + QUEUE_PREVIEW_GRAPHEME_CONTEXT_CHARACTERS,
|
|
114
|
+
);
|
|
115
|
+
const suffixSample = samplePromptEnd(
|
|
116
|
+
prompt,
|
|
117
|
+
suffixCharacters + QUEUE_PREVIEW_GRAPHEME_CONTEXT_CHARACTERS,
|
|
118
|
+
);
|
|
119
|
+
const prefixSegments = segmentPromptSample(prefixSample.value);
|
|
120
|
+
const suffixSegments = segmentPromptSample(suffixSample.value);
|
|
121
|
+
|
|
122
|
+
// A cluster at a truncated sampling edge may continue outside the sample.
|
|
123
|
+
// Prefix sampling starts at the true source start, so only its last segment
|
|
124
|
+
// is ambiguous. Suffix sampling ends at the true source end, so its first is.
|
|
125
|
+
if (prefixSample.truncated) prefixSegments.pop();
|
|
126
|
+
if (suffixSample.truncated) {
|
|
127
|
+
let ambiguousSegment = suffixSegments.shift();
|
|
128
|
+
// A locally segmented leading fragment ending in ZWJ can join the next
|
|
129
|
+
// pictographic segment when omitted left context supplies its base. Keep
|
|
130
|
+
// backing off until that uncertainty no longer propagates to the right.
|
|
131
|
+
while (ambiguousSegment?.endsWith("\u200d") && suffixSegments.length > 0) {
|
|
132
|
+
ambiguousSegment = suffixSegments.shift();
|
|
133
|
+
}
|
|
134
|
+
// Regional Indicator pairing depends on the parity of the preceding run.
|
|
135
|
+
// If that run reaches the unknown sample boundary, omit all of its visible
|
|
136
|
+
// leading segments rather than potentially recombining halves of flags.
|
|
137
|
+
while (suffixSegments[0] && startsWithRegionalIndicator(suffixSegments[0])) {
|
|
138
|
+
suffixSegments.shift();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const prefix = takeWholeGraphemesFromStart(prefixSegments, prefixCharacters);
|
|
143
|
+
const suffix = takeWholeGraphemesFromEnd(suffixSegments, suffixCharacters);
|
|
144
|
+
const reference = boundedPromptSampleReference(
|
|
145
|
+
prompt.length,
|
|
146
|
+
prefixSample.value,
|
|
147
|
+
suffixSample.value,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (!hasVisiblePromptContent(prefix) && !hasVisiblePromptContent(suffix)) {
|
|
151
|
+
return fallbackPromptPreview(prompt.length, prefixSample.value, suffixSample.value);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const safeSuffix = hasVisiblePromptContent(suffix)
|
|
155
|
+
? suffix
|
|
156
|
+
: promptPreviewFallbackLabel(reference);
|
|
157
|
+
const summary = `${prefix}${QUEUE_PREVIEW_SEPARATOR}${safeSuffix}`;
|
|
158
|
+
if (!hasVisiblePromptContent(prefix)) {
|
|
159
|
+
return {
|
|
160
|
+
summary,
|
|
161
|
+
collapsedVisual: summary,
|
|
162
|
+
visibleStart: safeSuffix,
|
|
163
|
+
visibleIdentity: null,
|
|
164
|
+
visibleIdentityLabel: null,
|
|
165
|
+
isFallback: false,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const endIdentity = takeWholeGraphemesFromEnd(
|
|
170
|
+
suffixSegments,
|
|
171
|
+
QUEUE_VISIBLE_END_IDENTITY_CHARACTERS,
|
|
172
|
+
);
|
|
173
|
+
return {
|
|
174
|
+
summary,
|
|
175
|
+
collapsedVisual: summary,
|
|
176
|
+
visibleStart: prefix,
|
|
177
|
+
visibleIdentity: hasVisiblePromptContent(endIdentity) ? endIdentity : `ref ${reference}`,
|
|
178
|
+
visibleIdentityLabel: hasVisiblePromptContent(endIdentity) ? "End" : "Safe boundary",
|
|
179
|
+
isFallback: false,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function fallbackPromptPreview(
|
|
184
|
+
promptLength: number,
|
|
185
|
+
startSample: string,
|
|
186
|
+
endSample: string,
|
|
187
|
+
): QueuePromptPreview {
|
|
188
|
+
const reference = boundedPromptSampleReference(promptLength, startSample, endSample);
|
|
189
|
+
const summary = promptPreviewFallbackLabel(reference);
|
|
190
|
+
return {
|
|
191
|
+
summary,
|
|
192
|
+
collapsedVisual: `Omitted · ${reference}`,
|
|
193
|
+
// The complete fallback remains the canonical accessible summary. Narrow
|
|
194
|
+
// collapsed/row layouts paint the bounded reference in a compact truthful
|
|
195
|
+
// form so ellipsis cannot hide the only identifying portion.
|
|
196
|
+
visibleStart: `Omitted · ${reference}`,
|
|
197
|
+
visibleIdentity: null,
|
|
198
|
+
visibleIdentityLabel: null,
|
|
199
|
+
isFallback: true,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function promptPreviewFallbackLabel(reference: string): string {
|
|
204
|
+
return `Content omitted at safe boundary · ref ${reference}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function boundedPromptSampleReference(
|
|
208
|
+
promptLength: number,
|
|
209
|
+
startSample: string,
|
|
210
|
+
endSample: string,
|
|
211
|
+
): string {
|
|
212
|
+
const head = samplePromptStart(startSample, QUEUE_PREVIEW_REFERENCE_SAMPLE_CHARACTERS).value;
|
|
213
|
+
const tail = samplePromptEnd(endSample, QUEUE_PREVIEW_REFERENCE_SAMPLE_CHARACTERS).value;
|
|
214
|
+
let hash = 0x811c9dc5;
|
|
215
|
+
for (const value of [String(promptLength), head, tail]) {
|
|
216
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
217
|
+
hash ^= value.charCodeAt(index);
|
|
218
|
+
hash = Math.imul(hash, 0x01000193);
|
|
219
|
+
}
|
|
220
|
+
hash ^= 0xffff;
|
|
221
|
+
hash = Math.imul(hash, 0x01000193);
|
|
222
|
+
}
|
|
223
|
+
return (hash >>> 0).toString(16).padStart(8, "0").toUpperCase();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function hasVisiblePromptContent(value: string): boolean {
|
|
227
|
+
// Callers pass only the already-bounded whole/head/tail preview samples.
|
|
228
|
+
// Default-ignorables and controls can survive trim() while painting no row
|
|
229
|
+
// identity at all (for example ZWJ, VS16, bidi controls, and tag characters).
|
|
230
|
+
for (const character of value) {
|
|
231
|
+
if (!queuePromptNonRenderingCodePoint.test(character)) return true;
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function samplePromptStart(prompt: string, maxCharacters: number): BoundedPromptSample {
|
|
237
|
+
let end = 0;
|
|
238
|
+
let characters = 0;
|
|
239
|
+
while (end < prompt.length && characters < maxCharacters) {
|
|
240
|
+
const first = prompt.charCodeAt(end);
|
|
241
|
+
end +=
|
|
242
|
+
isHighSurrogate(first) &&
|
|
243
|
+
end + 1 < prompt.length &&
|
|
244
|
+
isLowSurrogate(prompt.charCodeAt(end + 1))
|
|
245
|
+
? 2
|
|
246
|
+
: 1;
|
|
247
|
+
characters += 1;
|
|
248
|
+
}
|
|
249
|
+
return { value: prompt.slice(0, end), characters, truncated: end < prompt.length };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function samplePromptEnd(prompt: string, maxCharacters: number): BoundedPromptSample {
|
|
253
|
+
let start = prompt.length;
|
|
254
|
+
let characters = 0;
|
|
255
|
+
while (start > 0 && characters < maxCharacters) {
|
|
256
|
+
start -= 1;
|
|
257
|
+
if (
|
|
258
|
+
isLowSurrogate(prompt.charCodeAt(start)) &&
|
|
259
|
+
start > 0 &&
|
|
260
|
+
isHighSurrogate(prompt.charCodeAt(start - 1))
|
|
261
|
+
) {
|
|
262
|
+
start -= 1;
|
|
263
|
+
}
|
|
264
|
+
characters += 1;
|
|
265
|
+
}
|
|
266
|
+
return { value: prompt.slice(start), characters, truncated: start > 0 };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function segmentPromptSample(sample: string): string[] {
|
|
270
|
+
return queuePreviewSegments(replaceLoneSurrogates(sample));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function replaceLoneSurrogates(value: string): string {
|
|
274
|
+
let sanitized = "";
|
|
275
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
276
|
+
const current = value.charCodeAt(index);
|
|
277
|
+
if (
|
|
278
|
+
isHighSurrogate(current) &&
|
|
279
|
+
index + 1 < value.length &&
|
|
280
|
+
isLowSurrogate(value.charCodeAt(index + 1))
|
|
281
|
+
) {
|
|
282
|
+
sanitized += value.slice(index, index + 2);
|
|
283
|
+
index += 1;
|
|
284
|
+
} else if (isHighSurrogate(current) || isLowSurrogate(current)) {
|
|
285
|
+
sanitized += "�";
|
|
286
|
+
} else {
|
|
287
|
+
sanitized += value[index];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return sanitized;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function takeWholeGraphemesFromStart(segments: string[], maxCharacters: number): string {
|
|
294
|
+
const selected: string[] = [];
|
|
295
|
+
let characters = 0;
|
|
296
|
+
for (const segment of segments) {
|
|
297
|
+
const segmentCharacters = codePointLength(segment);
|
|
298
|
+
if (characters + segmentCharacters > maxCharacters) break;
|
|
299
|
+
selected.push(segment);
|
|
300
|
+
characters += segmentCharacters;
|
|
301
|
+
}
|
|
302
|
+
while (selected.at(-1)?.trim().length === 0) selected.pop();
|
|
303
|
+
return selected.join("");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function takeWholeGraphemesFromEnd(segments: string[], maxCharacters: number): string {
|
|
307
|
+
const selected: string[] = [];
|
|
308
|
+
let characters = 0;
|
|
309
|
+
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
310
|
+
const segment = segments[index];
|
|
311
|
+
if (!segment) continue;
|
|
312
|
+
const segmentCharacters = codePointLength(segment);
|
|
313
|
+
if (characters + segmentCharacters > maxCharacters) break;
|
|
314
|
+
selected.unshift(segment);
|
|
315
|
+
characters += segmentCharacters;
|
|
316
|
+
}
|
|
317
|
+
while (selected[0]?.trim().length === 0) selected.shift();
|
|
318
|
+
while (selected.at(-1)?.trim().length === 0) selected.pop();
|
|
319
|
+
return selected.join("");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function codePointLength(value: string): number {
|
|
323
|
+
let characters = 0;
|
|
324
|
+
for (const _character of value) characters += 1;
|
|
325
|
+
return characters;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function startsWithRegionalIndicator(value: string): boolean {
|
|
329
|
+
const codePoint = value.codePointAt(0);
|
|
330
|
+
return codePoint !== undefined && codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isHighSurrogate(codeUnit: number): boolean {
|
|
334
|
+
return codeUnit >= 0xd800 && codeUnit <= 0xdbff;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function isLowSurrogate(codeUnit: number): boolean {
|
|
338
|
+
return codeUnit >= 0xdc00 && codeUnit <= 0xdfff;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Move one item, as dnd-kit's arrayMove does. */
|
|
342
|
+
export function moveQueueItem<T>(items: readonly T[], from: number, to: number): T[] {
|
|
343
|
+
const next = items.slice();
|
|
344
|
+
const [item] = next.splice(from, 1);
|
|
345
|
+
if (item === undefined) return next;
|
|
346
|
+
next.splice(to < 0 ? next.length + to : to, 0, item);
|
|
347
|
+
return next;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export type QueueMovePlan = {
|
|
351
|
+
/** The turn ids in their new order. */
|
|
352
|
+
turnIds: string[];
|
|
353
|
+
/** The server anchor: the turn the moved one now precedes, null for last. */
|
|
354
|
+
beforeTurnId: string | null;
|
|
355
|
+
/** The bounded destination index. */
|
|
356
|
+
index: number;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/** The reorder request for moving a queued turn to an index, or null for a no-op. */
|
|
360
|
+
export function queueMovePlan(
|
|
361
|
+
queue: readonly SessionTurn[],
|
|
362
|
+
turnId: string,
|
|
363
|
+
nextIndex: number,
|
|
364
|
+
): QueueMovePlan | null {
|
|
365
|
+
const oldIndex = queue.findIndex((turn) => turn.id === turnId);
|
|
366
|
+
if (oldIndex < 0) return null;
|
|
367
|
+
const index = Math.max(0, Math.min(nextIndex, queue.length - 1));
|
|
368
|
+
if (oldIndex === index) return null;
|
|
369
|
+
const ordered = moveQueueItem(queue, oldIndex, index);
|
|
370
|
+
return {
|
|
371
|
+
turnIds: ordered.map((turn) => turn.id),
|
|
372
|
+
beforeTurnId: ordered[index + 1]?.id ?? null,
|
|
373
|
+
index,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export type PendingQueueMove = { baseVersion: number | null; turnIds: string[] };
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The queue as shown while a move is in flight: the optimistic order, unless
|
|
381
|
+
* any authoritative change arrived. A projection never omits or invents a row.
|
|
382
|
+
*/
|
|
383
|
+
export function projectPendingQueueMove(
|
|
384
|
+
queue: SessionTurn[],
|
|
385
|
+
pendingMove: PendingQueueMove | null,
|
|
386
|
+
snapshot: Pick<SessionQueueSnapshot, "version"> | null,
|
|
387
|
+
): SessionTurn[] {
|
|
388
|
+
if (
|
|
389
|
+
!pendingMove ||
|
|
390
|
+
pendingMove.baseVersion === null ||
|
|
391
|
+
snapshot?.version !== pendingMove.baseVersion
|
|
392
|
+
) {
|
|
393
|
+
return queue;
|
|
394
|
+
}
|
|
395
|
+
const byId = new Map(queue.map((turn) => [turn.id, turn]));
|
|
396
|
+
const projected = pendingMove.turnIds.map((turnId) => byId.get(turnId));
|
|
397
|
+
if (projected.some((turn) => !turn) || projected.length !== queue.length) {
|
|
398
|
+
return queue;
|
|
399
|
+
}
|
|
400
|
+
return projected as SessionTurn[];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Copy for the "replace your draft with this queued prompt" confirmation. */
|
|
404
|
+
export const QUEUE_REPLACE_DRAFT_COPY = {
|
|
405
|
+
title: "Your composer already has a draft. Replace it with this queued prompt?",
|
|
406
|
+
detail:
|
|
407
|
+
"The current draft will be permanently discarded; this queued prompt is preserved until you confirm.",
|
|
408
|
+
keep: "Keep current draft",
|
|
409
|
+
replace: "Replace and edit",
|
|
410
|
+
} as const;
|
|
411
|
+
|
|
412
|
+
export type QueuedTurnPresentation = {
|
|
413
|
+
kind: "prompt" | "realtime_voice" | "realtime_voice_handoff";
|
|
414
|
+
text: string;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
function objectValue(value: unknown): Record<string, unknown> | null {
|
|
418
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
419
|
+
? (value as Record<string, unknown>)
|
|
420
|
+
: null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** What a queued row shows: the prompt, or the transcript of a voice request. */
|
|
424
|
+
export function queuedTurnPresentation(turn: SessionTurn): QueuedTurnPresentation {
|
|
425
|
+
const realtimeDelegation = objectValue(turn.metadata.realtimeDelegation);
|
|
426
|
+
const inputTranscript = realtimeDelegation?.inputTranscript;
|
|
427
|
+
if (typeof inputTranscript === "string" && inputTranscript.trim()) {
|
|
428
|
+
return { kind: "realtime_voice", text: inputTranscript.trim() };
|
|
429
|
+
}
|
|
430
|
+
if (objectValue(turn.metadata.realtimeTailFlush)) {
|
|
431
|
+
return { kind: "realtime_voice_handoff", text: "Remaining voice context" };
|
|
432
|
+
}
|
|
433
|
+
return { kind: "prompt", text: turn.prompt };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function isSteeringTurn(turn: SessionTurn): boolean {
|
|
437
|
+
return turn.metadata.delivery === "steer";
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** A queued turn that is really waiting: not already a steer in flight. */
|
|
441
|
+
export function isAuthoritativeQueuedTurn(
|
|
442
|
+
turn: SessionTurn,
|
|
443
|
+
mutationFor: (turnId: string) => string | null,
|
|
444
|
+
): boolean {
|
|
445
|
+
return !isSteeringTurn(turn) && mutationFor(turn.id) !== "steer";
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function countAuthoritativeQueuedTurns(
|
|
449
|
+
turns: readonly SessionTurn[],
|
|
450
|
+
mutationFor: (turnId: string) => string | null,
|
|
451
|
+
): number {
|
|
452
|
+
return turns.filter((turn) => isAuthoritativeQueuedTurn(turn, mutationFor)).length;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** The move anchors for one row's Move up / Move down. */
|
|
456
|
+
export function queueNeighborAnchors(
|
|
457
|
+
turns: readonly SessionTurn[],
|
|
458
|
+
index: number,
|
|
459
|
+
): { beforeUp: string | null; beforeDown: string | null } {
|
|
460
|
+
return {
|
|
461
|
+
beforeUp: index > 0 ? (turns[index - 1]?.id ?? null) : null,
|
|
462
|
+
beforeDown: index < turns.length - 1 ? (turns[index + 2]?.id ?? null) : null,
|
|
463
|
+
};
|
|
464
|
+
}
|