@opengeni/react 1.4.4 → 1.4.5-canary.37485542607001
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-XS67YG5R.js → chunk-4O7OE7ON.js} +45 -4
- package/dist/chunk-4O7OE7ON.js.map +1 -0
- 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-QMMCJGFI.js → chunk-6BAMPGBV.js} +6 -3
- package/dist/chunk-6BAMPGBV.js.map +1 -0
- package/dist/{chunk-I6OY3MWC.js → chunk-6FOTSVWU.js} +2 -2
- 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-YVFRBXST.js → chunk-GMNGUPMX.js} +2 -2
- 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-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-LHF6BVM4.js +83 -0
- package/dist/chunk-LHF6BVM4.js.map +1 -0
- package/dist/{chunk-C5BF4L26.js → chunk-LSMWZSCJ.js} +7 -7
- package/dist/chunk-LTKGTXUQ.js +174 -0
- package/dist/chunk-LTKGTXUQ.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-IYQCWGU5.js → chunk-RGTZ2VDR.js} +35 -62
- package/dist/chunk-RGTZ2VDR.js.map +1 -0
- package/dist/chunk-RI42H5IU.js +326 -0
- package/dist/chunk-RI42H5IU.js.map +1 -0
- package/dist/{chunk-EAOSEIXB.js → chunk-RRRS5D5T.js} +727 -1983
- package/dist/chunk-RRRS5D5T.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-IUHHPMAX.js → chunk-SZOCY5VG.js} +18 -87
- package/dist/chunk-SZOCY5VG.js.map +1 -0
- package/dist/{chunk-L2J3PU7A.js → chunk-VYIPNDUZ.js} +29 -100
- package/dist/chunk-VYIPNDUZ.js.map +1 -0
- package/dist/{chunk-G6YRCIYJ.js → chunk-YK6QMGVL.js} +55 -60
- package/dist/chunk-YK6QMGVL.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-K7SOZVVZ.js} +6 -4
- package/dist/{embedded-voice-NIL4RDX3.js.map → embedded-voice-K7SOZVVZ.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/hooks/use-new-session-draft.d.ts +74 -0
- 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-27HGRPVF.js} +3 -3
- package/dist/lib/clipboard.d.ts +7 -0
- package/dist/lib/provider-recovery.d.ts +49 -0
- package/dist/lib/transient-retry.d.ts +25 -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-I5KPC3D6.js} +6 -4
- package/dist/model-policy-picker-menu-I5KPC3D6.js.map +1 -0
- package/dist/model-policy.d.ts +10 -0
- package/dist/model-policy.js +5 -1
- package/dist/new-session-draft.d.ts +7 -0
- package/dist/new-session-draft.js +639 -0
- package/dist/new-session-draft.js.map +1 -0
- 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-2DGDO6NI.js} +5 -5
- package/dist/sandbox-files-2DGDO6NI.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-agents-model.d.ts +10 -0
- package/dist/session-agents-model.js +25 -0
- package/dist/session-agents-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 +119 -0
- package/dist/session-list-model.js +244 -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 +55 -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 +8 -146
- 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 +20 -150
- 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/hooks/use-new-session-draft.ts +813 -0
- 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/lib/transient-retry.ts +99 -0
- package/src/model-mark-paths.ts +53 -0
- package/src/model-picker-model.ts +330 -0
- package/src/model-policy.ts +75 -5
- package/src/new-session-draft.ts +22 -0
- 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-agents-model.ts +33 -0
- package/src/session-attention-model.ts +59 -0
- package/src/session-feedback-model.ts +95 -0
- package/src/session-list-model.ts +377 -0
- package/src/session-status-model.ts +62 -0
- package/src/testing/session-scenarios.ts +326 -0
- package/src/timeline/activity-rail.tsx +54 -24
- package/src/timeline/genie-copy.ts +19 -0
- package/src/timeline/genie-loading.tsx +15 -19
- 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/projection.ts +12 -1
- 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 +254 -1024
- 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 +17 -4
- package/styles/connect.css +4 -0
- package/styles/responsive.css +11 -2
- 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-QMMCJGFI.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-I6OY3MWC.js.map → chunk-6FOTSVWU.js.map} +0 -0
- /package/dist/{chunk-YVFRBXST.js.map → chunk-GMNGUPMX.js.map} +0 -0
- /package/dist/{chunk-PBCZL4JP.js.map → chunk-INIZSNXT.js.map} +0 -0
- /package/dist/{chunk-C5BF4L26.js.map → chunk-LSMWZSCJ.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-27HGRPVF.js.map} +0 -0
- /package/dist/{sandbox-files-34CJJNOY.js.map → model-mark-paths.js.map} +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// Pure session-list presentation shared by the web app (rail and recent
|
|
2
|
+
// sessions) and native renderers: relative-time labels, recency bucketing
|
|
3
|
+
// (Today / Yesterday / Previous 7 days / Older), the running-first ordering
|
|
4
|
+
// rule, status tones and the compact row metadata. No DOM, no React.
|
|
5
|
+
import {
|
|
6
|
+
deriveAutomaticSessionTitlePreview,
|
|
7
|
+
deriveSessionDisplayTitle,
|
|
8
|
+
sessionTitleIsPending,
|
|
9
|
+
type ClientModel,
|
|
10
|
+
type Session,
|
|
11
|
+
type SessionListEntry,
|
|
12
|
+
type SessionStatus,
|
|
13
|
+
} from "@opengeni/sdk";
|
|
14
|
+
import { modelDisplayName } from "@opengeni/sdk/model-display";
|
|
15
|
+
import { findPickerRow, type PickerModelRow } from "./model-policy";
|
|
16
|
+
|
|
17
|
+
/** A list row may be a compact page entry or a complete session response. */
|
|
18
|
+
export type SessionListRow = Session | SessionListEntry;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The title shown for a session: durable agent/user metadata once available,
|
|
22
|
+
* otherwise a bounded opening-prompt preview while automatic naming is still
|
|
23
|
+
* pending. Every list, header and row reads identically.
|
|
24
|
+
*/
|
|
25
|
+
export function sessionDisplayTitle(session: SessionListRow): string {
|
|
26
|
+
return "displayTitle" in session ? session.displayTitle : deriveSessionDisplayTitle(session);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The maximum length a session title may be renamed to. */
|
|
30
|
+
export const SESSION_TITLE_MAX_LENGTH = 200;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The value a rename editor seeds from. A safe provisional prompt preview is
|
|
34
|
+
* editable because it is also what the user sees; the UUID-derived reference
|
|
35
|
+
* remains an empty draft rather than becoming an accidental rename.
|
|
36
|
+
*/
|
|
37
|
+
export function sessionRenameSeed(session: SessionListRow): string {
|
|
38
|
+
if ("renameSeed" in session) return session.renameSeed;
|
|
39
|
+
if (!sessionTitleIsPending(session)) {
|
|
40
|
+
return session.title?.trim() || "";
|
|
41
|
+
}
|
|
42
|
+
return deriveAutomaticSessionTitlePreview(session.initialMessage) ?? "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a submitted draft against the current display title. Returns the
|
|
47
|
+
* trimmed title to persist, or `null` when the edit is a no-op (empty,
|
|
48
|
+
* unchanged from what is already shown, or still equal to the value that
|
|
49
|
+
* seeded this edit) and should simply cancel.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveRenameSubmission(
|
|
52
|
+
draft: string,
|
|
53
|
+
display: string,
|
|
54
|
+
editSeed?: string,
|
|
55
|
+
): string | null {
|
|
56
|
+
const next = draft.trim();
|
|
57
|
+
if (!next || next === display || (editSeed !== undefined && next === editSeed.trim())) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type SessionRecencyGroup = "today" | "yesterday" | "previous7" | "older";
|
|
64
|
+
|
|
65
|
+
export const SESSION_GROUP_LABELS: Record<SessionRecencyGroup, string> = {
|
|
66
|
+
today: "Today",
|
|
67
|
+
yesterday: "Yesterday",
|
|
68
|
+
previous7: "Previous 7 days",
|
|
69
|
+
older: "Older",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The render order of recency groups, top → bottom. */
|
|
73
|
+
export const SESSION_GROUP_ORDER: SessionRecencyGroup[] = [
|
|
74
|
+
"today",
|
|
75
|
+
"yesterday",
|
|
76
|
+
"previous7",
|
|
77
|
+
"older",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/** Live states that earn the pinned-to-top, breathing-dot treatment. */
|
|
81
|
+
const RUNNING_STATUSES = new Set<SessionStatus>([
|
|
82
|
+
"running",
|
|
83
|
+
"queued",
|
|
84
|
+
"waiting_capacity",
|
|
85
|
+
"recovering",
|
|
86
|
+
"requires_action",
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
export function isRunningStatus(status: SessionStatus): boolean {
|
|
90
|
+
return RUNNING_STATUSES.has(status);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** An idle session waiting on its own timer, while its control is active. */
|
|
94
|
+
export function sessionInputWait(
|
|
95
|
+
session: Pick<SessionListRow, "status" | "effectiveControl" | "inputWait">,
|
|
96
|
+
) {
|
|
97
|
+
return session.status === "idle" && session.effectiveControl?.state === "active"
|
|
98
|
+
? (session.inputWait ?? null)
|
|
99
|
+
: null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function hasActiveEffectiveControl(session: SessionListRow): boolean {
|
|
103
|
+
return (session.effectiveControl?.state ?? "active") === "active";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isEffectivelyRunning(session: SessionListRow): boolean {
|
|
107
|
+
// Background commands have their own chat indicator, not agent working status.
|
|
108
|
+
return (
|
|
109
|
+
hasActiveEffectiveControl(session) &&
|
|
110
|
+
(isRunningStatus(session.status) || Boolean(sessionInputWait(session)))
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Most-recent activity timestamp for a session (updatedAt, then createdAt). */
|
|
115
|
+
export function sessionActivityTime(session: SessionListRow): number {
|
|
116
|
+
const updated = Date.parse(session.updatedAt);
|
|
117
|
+
if (!Number.isNaN(updated)) {
|
|
118
|
+
return updated;
|
|
119
|
+
}
|
|
120
|
+
const created = Date.parse(session.createdAt);
|
|
121
|
+
return Number.isNaN(created) ? 0 : created;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Deterministic newest-first ordering for every flat or forest session list. */
|
|
125
|
+
export function compareSessionActivity(left: SessionListRow, right: SessionListRow): number {
|
|
126
|
+
return sessionActivityTime(right) - sessionActivityTime(left) || right.id.localeCompare(left.id);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Deterministic personal-pin order: newest pin first, then descending id. */
|
|
130
|
+
export function compareSessionPins(left: SessionListRow, right: SessionListRow): number {
|
|
131
|
+
const leftPinnedAt = Date.parse(left.pinnedAt ?? "");
|
|
132
|
+
const rightPinnedAt = Date.parse(right.pinnedAt ?? "");
|
|
133
|
+
const leftTime = Number.isNaN(leftPinnedAt) ? 0 : leftPinnedAt;
|
|
134
|
+
const rightTime = Number.isNaN(rightPinnedAt) ? 0 : rightPinnedAt;
|
|
135
|
+
return rightTime - leftTime || right.id.localeCompare(left.id);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Split explicit personal pins from ordinary rows without changing the input. */
|
|
139
|
+
export function partitionPinnedSessions<T extends SessionListRow>(
|
|
140
|
+
sessions: T[],
|
|
141
|
+
): {
|
|
142
|
+
pinned: T[];
|
|
143
|
+
ordinary: T[];
|
|
144
|
+
} {
|
|
145
|
+
const pinned: T[] = [];
|
|
146
|
+
const ordinary: T[] = [];
|
|
147
|
+
for (const session of sessions) {
|
|
148
|
+
(session.pinned ? pinned : ordinary).push(session);
|
|
149
|
+
}
|
|
150
|
+
return { pinned: pinned.sort(compareSessionPins), ordinary };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Which recency bucket a timestamp falls into, relative to `now`. "Today" and
|
|
155
|
+
* "Yesterday" are calendar-local; "Previous 7 days" is the rest of the trailing
|
|
156
|
+
* week; everything earlier is "Older".
|
|
157
|
+
*/
|
|
158
|
+
export function recencyGroupFor(timestampMs: number, now: Date = new Date()): SessionRecencyGroup {
|
|
159
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
160
|
+
const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000;
|
|
161
|
+
const startOfWeekWindow = startOfToday - 7 * 24 * 60 * 60 * 1000;
|
|
162
|
+
if (timestampMs >= startOfToday) {
|
|
163
|
+
return "today";
|
|
164
|
+
}
|
|
165
|
+
if (timestampMs >= startOfYesterday) {
|
|
166
|
+
return "yesterday";
|
|
167
|
+
}
|
|
168
|
+
if (timestampMs >= startOfWeekWindow) {
|
|
169
|
+
return "previous7";
|
|
170
|
+
}
|
|
171
|
+
return "older";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type SessionRecencyBucket<T extends SessionListRow = SessionListRow> = {
|
|
175
|
+
group: SessionRecencyGroup;
|
|
176
|
+
label: string;
|
|
177
|
+
sessions: T[];
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export type GroupedSessions<T extends SessionListRow = SessionListRow> = {
|
|
181
|
+
/** Running sessions, pinned above every recency group, most-recent first. */
|
|
182
|
+
running: T[];
|
|
183
|
+
/** Non-running sessions bucketed by recency (empty buckets dropped). */
|
|
184
|
+
grouped: SessionRecencyBucket<T>[];
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Order + bucket the sessions for the rail. Running sessions are lifted into a
|
|
189
|
+
* synthetic, always-first position regardless of recency (rendered with a
|
|
190
|
+
* "running" marker); the remainder are bucketed by recency, most-recent first
|
|
191
|
+
* within each bucket. Empty groups are dropped.
|
|
192
|
+
*/
|
|
193
|
+
export function groupSessionsForRail<T extends SessionListRow>(
|
|
194
|
+
sessions: T[],
|
|
195
|
+
now: Date = new Date(),
|
|
196
|
+
): GroupedSessions<T> {
|
|
197
|
+
const running = sessions.filter(isEffectivelyRunning).sort(compareSessionActivity);
|
|
198
|
+
const rest = sessions
|
|
199
|
+
.filter((session) => !isEffectivelyRunning(session))
|
|
200
|
+
.sort(compareSessionActivity);
|
|
201
|
+
|
|
202
|
+
const buckets = new Map<SessionRecencyGroup, T[]>();
|
|
203
|
+
for (const session of rest) {
|
|
204
|
+
const group = recencyGroupFor(sessionActivityTime(session), now);
|
|
205
|
+
const list = buckets.get(group) ?? [];
|
|
206
|
+
list.push(session);
|
|
207
|
+
buckets.set(group, list);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const grouped: SessionRecencyBucket<T>[] = [];
|
|
211
|
+
for (const group of SESSION_GROUP_ORDER) {
|
|
212
|
+
const list = buckets.get(group);
|
|
213
|
+
if (list && list.length > 0) {
|
|
214
|
+
grouped.push({
|
|
215
|
+
group,
|
|
216
|
+
label: SESSION_GROUP_LABELS[group],
|
|
217
|
+
sessions: list,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { running, grouped };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function relativeTimeLabel(value: string, now: Date = new Date()): string {
|
|
225
|
+
const timestamp = Date.parse(value);
|
|
226
|
+
if (Number.isNaN(timestamp)) {
|
|
227
|
+
return "";
|
|
228
|
+
}
|
|
229
|
+
const diffSeconds = Math.max(0, Math.floor((now.getTime() - timestamp) / 1000));
|
|
230
|
+
if (diffSeconds < 45) {
|
|
231
|
+
return "now";
|
|
232
|
+
}
|
|
233
|
+
const minutes = Math.floor(diffSeconds / 60);
|
|
234
|
+
if (minutes < 60) {
|
|
235
|
+
return `${minutes}m`;
|
|
236
|
+
}
|
|
237
|
+
const hours = Math.floor(minutes / 60);
|
|
238
|
+
if (hours < 24) {
|
|
239
|
+
return `${hours}h`;
|
|
240
|
+
}
|
|
241
|
+
const days = Math.floor(hours / 24);
|
|
242
|
+
if (days < 7) {
|
|
243
|
+
return `${days}d`;
|
|
244
|
+
}
|
|
245
|
+
return new Date(timestamp).toLocaleDateString(undefined, {
|
|
246
|
+
month: "short",
|
|
247
|
+
day: "numeric",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Codex subscription product ids are prefixed `codex/`. */
|
|
252
|
+
export function isCodexProductModel(modelId: string): boolean {
|
|
253
|
+
return modelId.startsWith("codex/");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export type SessionStatusTone = "queued" | "running" | "waiting" | "idle" | "failed" | "cancelled";
|
|
257
|
+
|
|
258
|
+
export const SESSION_STATUS_TONE: Record<SessionStatus, SessionStatusTone> = {
|
|
259
|
+
queued: "queued",
|
|
260
|
+
running: "running",
|
|
261
|
+
recovering: "running",
|
|
262
|
+
waiting_capacity: "waiting",
|
|
263
|
+
requires_action: "waiting",
|
|
264
|
+
idle: "idle",
|
|
265
|
+
failed: "failed",
|
|
266
|
+
cancelled: "cancelled",
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/** The status dot of a recent-session row: a background command reads as running. */
|
|
270
|
+
export function recentSessionStatus(
|
|
271
|
+
session: Pick<SessionListRow, "status" | "backgroundCommandActivity">,
|
|
272
|
+
): { tone: SessionStatusTone; pulse: boolean } {
|
|
273
|
+
const hasBackgroundCommand = session.backgroundCommandActivity !== undefined;
|
|
274
|
+
return {
|
|
275
|
+
tone: hasBackgroundCommand ? "running" : SESSION_STATUS_TONE[session.status],
|
|
276
|
+
pulse: hasBackgroundCommand || session.status === "running",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** A short `owner/repo` label from the session's first repository resource. */
|
|
281
|
+
export function sessionRepoLabel(session: Pick<Session, "resources">): string | null {
|
|
282
|
+
const repo = session.resources.find((resource) => resource.kind === "repository");
|
|
283
|
+
if (!repo || repo.kind !== "repository") {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const parts = repo.uri
|
|
287
|
+
.replace(/\.git$/, "")
|
|
288
|
+
.split("/")
|
|
289
|
+
.filter(Boolean);
|
|
290
|
+
return parts.length >= 2 ? parts.slice(-2).join("/") : (parts.at(-1) ?? null);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function recentSessionModelPresentation<TCatalog extends ClientModel>(
|
|
294
|
+
modelId: string,
|
|
295
|
+
catalogRows: readonly PickerModelRow<TCatalog>[],
|
|
296
|
+
): {
|
|
297
|
+
label: string;
|
|
298
|
+
billingClass: PickerModelRow["billingClass"];
|
|
299
|
+
logoUrl?: string | undefined;
|
|
300
|
+
} {
|
|
301
|
+
const row = findPickerRow([...catalogRows], modelId);
|
|
302
|
+
return {
|
|
303
|
+
label: row?.label ?? modelDisplayName(modelId),
|
|
304
|
+
logoUrl: row?.catalog.logoUrl,
|
|
305
|
+
billingClass:
|
|
306
|
+
row?.billingClass ??
|
|
307
|
+
(isCodexProductModel(modelId) ? "codex_subscription" : "opengeni_credits"),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The home screen's recent sessions: server pins first, then running, then
|
|
313
|
+
* recency buckets, capped. `sessions` keeps the all-visible-row contract, so
|
|
314
|
+
* its pins are removed before recombining the explicit pinned section.
|
|
315
|
+
*/
|
|
316
|
+
export function recentSessionsForHome<T extends SessionListRow>(
|
|
317
|
+
sessions: T[],
|
|
318
|
+
pinned: T[],
|
|
319
|
+
limit = 6,
|
|
320
|
+
now: Date = new Date(),
|
|
321
|
+
): T[] {
|
|
322
|
+
const ordinary = sessions.filter((session) => !session.pinned);
|
|
323
|
+
const { running, grouped } = groupSessionsForRail(ordinary, now);
|
|
324
|
+
return [...pinned, ...running, ...grouped.flatMap((bucket) => bucket.sessions)].slice(0, limit);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** One project (workspace channel) and its sessions, as the web rail groups them. */
|
|
328
|
+
export type SessionProjectSection<T extends SessionListRow = SessionListRow> = {
|
|
329
|
+
/** The channel id, or "default" for unfiled sessions. */
|
|
330
|
+
key: string;
|
|
331
|
+
channelId: string | null;
|
|
332
|
+
name: string;
|
|
333
|
+
sessions: T[];
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Group top-level sessions by project in the server's project order (pinned
|
|
338
|
+
* projects first), then "Default" for unfiled ones and those whose project no
|
|
339
|
+
* longer exists. Running sessions lead each project, then the most recent.
|
|
340
|
+
* Sub-agent sessions are reached from their parent, so they are left out.
|
|
341
|
+
* Projects without sessions are kept only when `keepEmpty` is set.
|
|
342
|
+
*/
|
|
343
|
+
export function groupSessionsByProject<T extends SessionListRow & { channelId?: string | null }>(
|
|
344
|
+
sessions: readonly T[],
|
|
345
|
+
projects: readonly { id: string; name: string }[],
|
|
346
|
+
options: { keepEmpty?: boolean } = {},
|
|
347
|
+
): SessionProjectSection<T>[] {
|
|
348
|
+
const known = new Set(projects.map((project) => project.id));
|
|
349
|
+
const byProject = new Map<string | null, T[]>();
|
|
350
|
+
for (const session of sessions) {
|
|
351
|
+
if ("parentSessionId" in session && session.parentSessionId) continue;
|
|
352
|
+
const channelId = session.channelId ?? null;
|
|
353
|
+
const key = channelId !== null && known.has(channelId) ? channelId : null;
|
|
354
|
+
byProject.set(key, [...(byProject.get(key) ?? []), session]);
|
|
355
|
+
}
|
|
356
|
+
const order = (list: T[]) =>
|
|
357
|
+
[...list].sort(
|
|
358
|
+
(left, right) =>
|
|
359
|
+
Number(isEffectivelyRunning(right)) - Number(isEffectivelyRunning(left)) ||
|
|
360
|
+
compareSessionActivity(left, right),
|
|
361
|
+
);
|
|
362
|
+
const sections: SessionProjectSection<T>[] = projects
|
|
363
|
+
.map((project) => ({
|
|
364
|
+
key: project.id,
|
|
365
|
+
channelId: project.id,
|
|
366
|
+
name: project.name,
|
|
367
|
+
sessions: order(byProject.get(project.id) ?? []),
|
|
368
|
+
}))
|
|
369
|
+
.filter((section) => options.keepEmpty || section.sessions.length > 0);
|
|
370
|
+
const unfiled = byProject.get(null) ?? [];
|
|
371
|
+
if (unfiled.length > 0) {
|
|
372
|
+
sections.push({ key: "default", channelId: null, name: "Default", sessions: order(unfiled) });
|
|
373
|
+
}
|
|
374
|
+
return sections;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export { findPickerRow };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SessionStatus } from "@opengeni/sdk";
|
|
2
|
+
|
|
3
|
+
/** Status token family (`--og-color-status-<tone>`) for a session status. */
|
|
4
|
+
export type SessionStatusTone = "queued" | "running" | "waiting" | "idle" | "failed" | "cancelled";
|
|
5
|
+
|
|
6
|
+
/** Renderer-neutral session status presentation: label, color family and liveness. */
|
|
7
|
+
export const SESSION_STATUS_PRESENTATION: Record<
|
|
8
|
+
SessionStatus,
|
|
9
|
+
{ label: string; tone: SessionStatusTone; pulse: boolean }
|
|
10
|
+
> = {
|
|
11
|
+
queued: { label: "Queued", tone: "queued", pulse: false },
|
|
12
|
+
running: { label: "Running", tone: "running", pulse: true },
|
|
13
|
+
recovering: { label: "Recovering", tone: "running", pulse: true },
|
|
14
|
+
waiting_capacity: { label: "Waiting for capacity", tone: "waiting", pulse: true },
|
|
15
|
+
idle: { label: "Idle", tone: "idle", pulse: false },
|
|
16
|
+
requires_action: { label: "Waiting on you", tone: "waiting", pulse: true },
|
|
17
|
+
failed: { label: "Failed", tone: "failed", pulse: false },
|
|
18
|
+
cancelled: { label: "Cancelled", tone: "cancelled", pulse: false },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The status badge's color tokens (text, border and its alpha); the fill is the
|
|
23
|
+
* status color at 10%. Mirrors the web badge classes, for non-DOM renderers.
|
|
24
|
+
*/
|
|
25
|
+
export const SESSION_STATUS_BADGE: Record<
|
|
26
|
+
SessionStatus,
|
|
27
|
+
{ text: string; border: string; borderAlpha: number; fill: string }
|
|
28
|
+
> = {
|
|
29
|
+
queued: { text: "fg-muted", border: "border", borderAlpha: 1, fill: "status-queued" },
|
|
30
|
+
running: {
|
|
31
|
+
text: "status-running",
|
|
32
|
+
border: "status-running",
|
|
33
|
+
borderAlpha: 0.3,
|
|
34
|
+
fill: "status-running",
|
|
35
|
+
},
|
|
36
|
+
recovering: {
|
|
37
|
+
text: "status-running",
|
|
38
|
+
border: "status-running",
|
|
39
|
+
borderAlpha: 0.3,
|
|
40
|
+
fill: "status-running",
|
|
41
|
+
},
|
|
42
|
+
waiting_capacity: {
|
|
43
|
+
text: "status-waiting",
|
|
44
|
+
border: "status-waiting",
|
|
45
|
+
borderAlpha: 0.35,
|
|
46
|
+
fill: "status-waiting",
|
|
47
|
+
},
|
|
48
|
+
idle: { text: "status-idle", border: "status-idle", borderAlpha: 0.3, fill: "status-idle" },
|
|
49
|
+
requires_action: {
|
|
50
|
+
text: "status-waiting",
|
|
51
|
+
border: "status-waiting",
|
|
52
|
+
borderAlpha: 0.35,
|
|
53
|
+
fill: "status-waiting",
|
|
54
|
+
},
|
|
55
|
+
failed: {
|
|
56
|
+
text: "status-failed",
|
|
57
|
+
border: "status-failed",
|
|
58
|
+
borderAlpha: 0.35,
|
|
59
|
+
fill: "status-failed",
|
|
60
|
+
},
|
|
61
|
+
cancelled: { text: "fg-subtle", border: "border", borderAlpha: 1, fill: "status-cancelled" },
|
|
62
|
+
};
|