@opengeni/react 0.5.0 → 0.6.1
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 +109 -2
- package/dist/chunk-DEW2ZNF2.js +1238 -0
- package/dist/chunk-DEW2ZNF2.js.map +1 -0
- package/dist/index.d.ts +173 -50
- package/dist/index.js +1422 -982
- package/dist/index.js.map +1 -1
- package/dist/machines-BD6h9P_s.d.ts +329 -0
- package/dist/machines.d.ts +4 -0
- package/dist/machines.js +33 -0
- package/dist/machines.js.map +1 -0
- package/package.json +8 -2
- package/src/components/desktop-viewer.tsx +11 -1
- package/src/components/enrollment-consent.tsx +245 -0
- package/src/components/enrollment-device-flow.tsx +182 -0
- package/src/components/machine-card.tsx +131 -0
- package/src/components/machine-dock-bar.tsx +84 -0
- package/src/components/machine-metrics.tsx +184 -0
- package/src/components/machine-status-pill.tsx +157 -0
- package/src/components/machines-dashboard.tsx +151 -0
- package/src/components/message-timeline.tsx +106 -8
- package/src/components/workspace-dock.tsx +44 -10
- package/src/hooks/use-codex-accounts.ts +179 -0
- package/src/hooks/use-desktop-stream.ts +28 -2
- package/src/hooks/use-goal.ts +10 -2
- package/src/hooks/use-machines.ts +157 -0
- package/src/hooks/use-relay-frame-stream.ts +335 -0
- package/src/index.ts +15 -0
- package/src/lib/relay-wire.ts +116 -0
- package/src/machines.ts +50 -0
- package/src/timeline/activity-rail.tsx +13 -7
- package/src/timeline/index.ts +1 -0
- package/src/timeline/projection.ts +214 -16
- package/src/timeline/turn-summary.tsx +32 -10
- package/src/timeline/types.ts +29 -3
- package/src/types/machines.ts +67 -0
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
SessionStatusItem,
|
|
9
9
|
TimelineGroup,
|
|
10
10
|
TimelineItem,
|
|
11
|
+
TurnEndItem,
|
|
11
12
|
ToolCallItem,
|
|
12
13
|
WorkerItem,
|
|
13
14
|
} from "./types";
|
|
@@ -29,6 +30,7 @@ import type {
|
|
|
29
30
|
/** Tool names on the first-party OpenGeni MCP server that operate on sessions. */
|
|
30
31
|
const WORKER_SPAWN_TOOL = "session_create";
|
|
31
32
|
const WORKER_MESSAGE_TOOL = "session_send_message";
|
|
33
|
+
const WORKER_INTERRUPT_TOOL = "session_interrupt";
|
|
32
34
|
|
|
33
35
|
export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
34
36
|
const items: TimelineItem[] = [];
|
|
@@ -159,6 +161,23 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
159
161
|
const callId = typeof payload.id === "string" ? payload.id : null;
|
|
160
162
|
const args = payload.arguments ?? null;
|
|
161
163
|
closeStreamingTail();
|
|
164
|
+
if (name === WORKER_INTERRUPT_TOOL) {
|
|
165
|
+
// stop (default) vs steer — the target keeps its goal on steer and
|
|
166
|
+
// picks up its next queued turn (pair with session_send_message).
|
|
167
|
+
items.push({
|
|
168
|
+
kind: "worker",
|
|
169
|
+
id: event.id,
|
|
170
|
+
turnId,
|
|
171
|
+
callId,
|
|
172
|
+
action: "interrupt",
|
|
173
|
+
prompt: null,
|
|
174
|
+
workerSessionId: extractSessionRef(args),
|
|
175
|
+
mode: workerInterruptMode(args),
|
|
176
|
+
status: "running",
|
|
177
|
+
occurredAt: event.occurredAt,
|
|
178
|
+
});
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
162
181
|
if (name === WORKER_SPAWN_TOOL || name === WORKER_MESSAGE_TOOL) {
|
|
163
182
|
items.push({
|
|
164
183
|
kind: "worker",
|
|
@@ -261,6 +280,14 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
261
280
|
if (!isSessionStatus(status)) {
|
|
262
281
|
break;
|
|
263
282
|
}
|
|
283
|
+
// Only attention-worthy statuses earn a timeline divider. queued /
|
|
284
|
+
// running / idle are machinery telemetry: the header pill carries the
|
|
285
|
+
// live status, the shimmer says "running", and the turn chip's duration
|
|
286
|
+
// facet says how long — a stale "idle · 27s" row is pure noise,
|
|
287
|
+
// especially in historical traces.
|
|
288
|
+
if (!ATTENTION_STATUSES.has(status)) {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
264
291
|
const previous = [...items].reverse().find((item): item is SessionStatusItem => item.kind === "session-status");
|
|
265
292
|
if (previous?.status === status) {
|
|
266
293
|
break;
|
|
@@ -283,30 +310,40 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
283
310
|
|
|
284
311
|
case "turn.completed": {
|
|
285
312
|
finalizeOpen(turnId);
|
|
313
|
+
items.push(turnEndItem(event, "complete", null));
|
|
286
314
|
break;
|
|
287
315
|
}
|
|
288
316
|
|
|
289
317
|
case "turn.failed": {
|
|
318
|
+
const hadActivity = hasTurnActivity(items, turnId);
|
|
319
|
+
const failureText = failureMessage(payload);
|
|
290
320
|
finalizeOpen(turnId, "failed");
|
|
291
|
-
items.push(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
321
|
+
items.push(turnEndItem(event, "failed", failureText));
|
|
322
|
+
if (!hadActivity) {
|
|
323
|
+
items.push({
|
|
324
|
+
kind: "notice",
|
|
325
|
+
id: event.id,
|
|
326
|
+
tone: "failed",
|
|
327
|
+
text: failureText ?? "The turn failed.",
|
|
328
|
+
occurredAt: event.occurredAt,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
298
331
|
break;
|
|
299
332
|
}
|
|
300
333
|
|
|
301
334
|
case "turn.cancelled": {
|
|
335
|
+
const hadActivity = hasTurnActivity(items, turnId);
|
|
302
336
|
finalizeOpen(turnId, "cancelled");
|
|
303
|
-
items.push(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
337
|
+
items.push(turnEndItem(event, "cancelled", null));
|
|
338
|
+
if (!hadActivity) {
|
|
339
|
+
items.push({
|
|
340
|
+
kind: "notice",
|
|
341
|
+
id: event.id,
|
|
342
|
+
tone: "cancelled",
|
|
343
|
+
text: "Interrupted.",
|
|
344
|
+
occurredAt: event.occurredAt,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
310
347
|
break;
|
|
311
348
|
}
|
|
312
349
|
|
|
@@ -351,7 +388,8 @@ export function sessionStatusFromEvents(events: SessionEvent[]): SessionStatus |
|
|
|
351
388
|
|
|
352
389
|
/* ----------------------------------------------------------------------------
|
|
353
390
|
Visual grouping: consecutive activity items (reasoning / tools / workers /
|
|
354
|
-
sandbox) cluster into
|
|
391
|
+
sandbox) cluster into collapsible blocks. Once a turn settles, the full
|
|
392
|
+
non-user span folds behind a turn group, with activity blocks nested inside.
|
|
355
393
|
-------------------------------------------------------------------------- */
|
|
356
394
|
|
|
357
395
|
/**
|
|
@@ -376,13 +414,18 @@ export function groupTimeline(items: TimelineItem[]): TimelineGroup[] {
|
|
|
376
414
|
for (const item of items) {
|
|
377
415
|
if (isActivityItem(item)) {
|
|
378
416
|
const open = groups[groups.length - 1];
|
|
379
|
-
if (open?.kind === "activity") {
|
|
417
|
+
if (open?.kind === "activity" && open.outcome === undefined) {
|
|
380
418
|
open.items.push(item);
|
|
381
419
|
} else {
|
|
382
420
|
groups.push({ kind: "activity", id: `activity-${item.id}`, items: [item] });
|
|
383
421
|
}
|
|
384
422
|
continue;
|
|
385
423
|
}
|
|
424
|
+
if (item.kind === "turn-end") {
|
|
425
|
+
stampTurnOutcome(groups, item);
|
|
426
|
+
foldSettledTurn(groups, item);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
386
429
|
groups.push({ kind: "item", item });
|
|
387
430
|
}
|
|
388
431
|
return groups;
|
|
@@ -390,12 +433,161 @@ export function groupTimeline(items: TimelineItem[]): TimelineGroup[] {
|
|
|
390
433
|
|
|
391
434
|
/* --- helpers ---------------------------------------------------------------- */
|
|
392
435
|
|
|
436
|
+
function turnEndItem(
|
|
437
|
+
event: SessionEvent,
|
|
438
|
+
outcome: TurnEndItem["outcome"],
|
|
439
|
+
failureText: string | null,
|
|
440
|
+
): TurnEndItem {
|
|
441
|
+
return {
|
|
442
|
+
kind: "turn-end",
|
|
443
|
+
id: `${event.id}-turn-end`,
|
|
444
|
+
turnId: event.turnId ?? null,
|
|
445
|
+
outcome,
|
|
446
|
+
failureText,
|
|
447
|
+
occurredAt: event.occurredAt,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function hasTurnActivity(items: TimelineItem[], turnId: string | null): boolean {
|
|
452
|
+
if (turnId) {
|
|
453
|
+
return items.some((item) => isActivityItem(item) && item.turnId === turnId);
|
|
454
|
+
}
|
|
455
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
456
|
+
const item = items[index];
|
|
457
|
+
if (!item || item.kind === "turn-end" || item.kind === "user-message") {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
if (isActivityItem(item)) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function stampTurnOutcome(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
|
|
468
|
+
if (turnEnd.turnId === null) {
|
|
469
|
+
const trailing = groups[groups.length - 1];
|
|
470
|
+
if (trailing?.kind === "activity" && trailing.outcome === undefined) {
|
|
471
|
+
applyTurnOutcome(trailing, turnEnd);
|
|
472
|
+
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
for (const group of groups) {
|
|
476
|
+
if (group.kind !== "activity" || group.outcome !== undefined) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (group.items.some((activity) => activity.turnId === turnEnd.turnId)) {
|
|
480
|
+
applyTurnOutcome(group, turnEnd);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function applyTurnOutcome(group: Extract<TimelineGroup, { kind: "activity" }>, turnEnd: TurnEndItem): void {
|
|
486
|
+
group.outcome = turnEnd.outcome;
|
|
487
|
+
if (turnEnd.failureText) {
|
|
488
|
+
group.failureText = turnEnd.failureText;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function foldSettledTurn(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
|
|
493
|
+
let startIndex = groups.length;
|
|
494
|
+
let stoppedAtForeignTurn = false;
|
|
495
|
+
while (startIndex > 0) {
|
|
496
|
+
const previous = groups[startIndex - 1];
|
|
497
|
+
if (isTurnBoundary(previous)) {
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
if (belongsToDifferentTurn(previous, turnEnd.turnId)) {
|
|
501
|
+
stoppedAtForeignTurn = true;
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
startIndex -= 1;
|
|
505
|
+
}
|
|
506
|
+
if (stoppedAtForeignTurn) {
|
|
507
|
+
while (startIndex < groups.length && isBetweenTurnDivider(groups[startIndex])) {
|
|
508
|
+
startIndex += 1;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const collected = groups.slice(startIndex);
|
|
513
|
+
if (collected.length === 0) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const finalMessage = extractFinalAgentMessage(collected, turnEnd);
|
|
518
|
+
const body = finalMessage ? collected.slice(0, -1) : collected;
|
|
519
|
+
if (body.length === 0) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const firstOccurredAt = groupStartedAt(body[0]) ?? turnEnd.occurredAt;
|
|
524
|
+
const turnGroup: TimelineGroup = {
|
|
525
|
+
kind: "turn",
|
|
526
|
+
id: `turn-${turnEnd.turnId ?? turnEnd.id}`,
|
|
527
|
+
outcome: turnEnd.outcome,
|
|
528
|
+
startedAt: firstOccurredAt,
|
|
529
|
+
endedAt: turnEnd.occurredAt,
|
|
530
|
+
groups: body,
|
|
531
|
+
};
|
|
532
|
+
if (turnEnd.failureText) {
|
|
533
|
+
turnGroup.failureText = turnEnd.failureText;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
groups.splice(startIndex, collected.length, ...(finalMessage ? [turnGroup, finalMessage] : [turnGroup]));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function isTurnBoundary(group: TimelineGroup | undefined): boolean {
|
|
540
|
+
return group?.kind === "turn" || (group?.kind === "item" && group.item.kind === "user-message");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function belongsToDifferentTurn(group: TimelineGroup | undefined, turnId: string | null): boolean {
|
|
544
|
+
if (!group || !turnId) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
if (group.kind === "activity") {
|
|
548
|
+
return group.items.length > 0 && group.items.every((item) => item.turnId !== null && item.turnId !== turnId);
|
|
549
|
+
}
|
|
550
|
+
return group.kind === "item" && group.item.kind === "agent-message" && group.item.turnId !== null && group.item.turnId !== turnId;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function isBetweenTurnDivider(group: TimelineGroup | undefined): boolean {
|
|
554
|
+
return group?.kind === "item" && group.item.kind === "session-status" && group.item.status !== "running";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function extractFinalAgentMessage(groups: TimelineGroup[], turnEnd: TurnEndItem): Extract<TimelineGroup, { kind: "item" }> | null {
|
|
558
|
+
const tail = groups[groups.length - 1];
|
|
559
|
+
if (tail?.kind !== "item" || tail.item.kind !== "agent-message" || tail.item.streaming) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
if (tail.item.turnId && turnEnd.turnId && tail.item.turnId !== turnEnd.turnId) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
return tail;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function groupStartedAt(group: TimelineGroup | undefined): string | undefined {
|
|
569
|
+
if (!group) {
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
switch (group.kind) {
|
|
573
|
+
case "item":
|
|
574
|
+
return group.item.occurredAt;
|
|
575
|
+
case "activity":
|
|
576
|
+
return group.items[0]?.occurredAt;
|
|
577
|
+
case "turn":
|
|
578
|
+
return group.startedAt;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
393
582
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
394
583
|
return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
395
584
|
}
|
|
396
585
|
|
|
397
586
|
const SESSION_STATUSES: readonly SessionStatus[] = ["queued", "running", "idle", "requires_action", "failed", "cancelled"];
|
|
398
587
|
|
|
588
|
+
/** Statuses that demand the reader's attention and so earn a timeline divider. */
|
|
589
|
+
const ATTENTION_STATUSES: ReadonlySet<SessionStatus> = new Set(["requires_action", "failed", "cancelled"]);
|
|
590
|
+
|
|
399
591
|
/** Keep only entries that match the wire shapes; user payloads are untyped. */
|
|
400
592
|
function resourceRefs(value: unknown): import("@opengeni/sdk").ResourceRef[] {
|
|
401
593
|
if (!Array.isArray(value)) {
|
|
@@ -502,6 +694,12 @@ function workerPrompt(args: unknown): string | null {
|
|
|
502
694
|
return null;
|
|
503
695
|
}
|
|
504
696
|
|
|
697
|
+
/** The interrupt mode from `session_interrupt` args; defaults to "stop". */
|
|
698
|
+
function workerInterruptMode(args: unknown): "stop" | "steer" {
|
|
699
|
+
const record = asRecord(typeof args === "string" ? tryParseJson(args) : args);
|
|
700
|
+
return record.mode === "steer" ? "steer" : "stop";
|
|
701
|
+
}
|
|
702
|
+
|
|
505
703
|
/**
|
|
506
704
|
* Find a session id in orchestration tool arguments or output. Handles raw
|
|
507
705
|
* objects, JSON strings, and MCP tool results (`{ content: [{ type: "text",
|
|
@@ -5,40 +5,41 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import { useForcedDefaultOpen } from "./disclosure-context";
|
|
6
6
|
import { applyPatchOps, isApplyPatch } from "./parsers";
|
|
7
7
|
import { rawTypeOf } from "./registry";
|
|
8
|
-
import type { ActivityItem } from "./types";
|
|
8
|
+
import type { ActivityItem, TurnOutcome } from "./types";
|
|
9
|
+
export type { TurnOutcome } from "./types";
|
|
9
10
|
|
|
10
11
|
/* ----------------------------------------------------------------------------
|
|
11
12
|
Turn summary
|
|
12
13
|
|
|
13
|
-
A completed (or failed/cancelled) turn
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
A completed (or failed/cancelled) turn folds behind one quiet summary chip:
|
|
15
|
+
"N steps · M files · K commands · 1 screenshot · 4m". The chip is the default
|
|
16
|
+
surface; expanding it reveals the full settled turn body. A live turn never
|
|
17
|
+
folds — render its rows directly.
|
|
17
18
|
|
|
18
19
|
This keeps the timeline calm: a finished turn is a single line until the
|
|
19
20
|
reader chooses to look inside it.
|
|
20
21
|
-------------------------------------------------------------------------- */
|
|
21
22
|
|
|
22
|
-
export type TurnOutcome = "complete" | "failed" | "cancelled";
|
|
23
|
-
|
|
24
23
|
export type TurnSummaryProps = {
|
|
25
24
|
/** The activity items in the turn (used only to compute the facet counts). */
|
|
26
25
|
items: ActivityItem[];
|
|
27
26
|
outcome: TurnOutcome;
|
|
28
27
|
/** A short failure reason shown inline on a failed chip (never hidden). */
|
|
29
28
|
failureText?: string | undefined;
|
|
29
|
+
/** Elapsed turn duration; shown as a trailing facet when at least 1s. */
|
|
30
|
+
durationMs?: number | undefined;
|
|
30
31
|
/** Start expanded. */
|
|
31
32
|
defaultOpen?: boolean | undefined;
|
|
32
33
|
/** The rendered activity rail revealed on expand. */
|
|
33
34
|
children: React.ReactNode;
|
|
34
35
|
};
|
|
35
36
|
|
|
36
|
-
export function TurnSummary({ items, outcome, failureText, defaultOpen, children }: TurnSummaryProps) {
|
|
37
|
+
export function TurnSummary({ items, outcome, failureText, durationMs, defaultOpen, children }: TurnSummaryProps) {
|
|
37
38
|
// An explicit `defaultOpen` always wins; otherwise an ancestor may seed it
|
|
38
39
|
// (screenshot instrumentation); otherwise the turn starts folded.
|
|
39
40
|
const forcedDefaultOpen = useForcedDefaultOpen();
|
|
40
41
|
const [open, setOpen] = useState(defaultOpen ?? forcedDefaultOpen ?? false);
|
|
41
|
-
const facets = summarizeTurn(items);
|
|
42
|
+
const facets = summarizeTurn(items, durationMs);
|
|
42
43
|
|
|
43
44
|
return (
|
|
44
45
|
<Collapsible.Root open={open} onOpenChange={setOpen} className="animate-og-enter">
|
|
@@ -92,7 +93,7 @@ export function TurnSummary({ items, outcome, failureText, defaultOpen, children
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
/** Compose the facet summary line ("14 steps · 3 files · 2 commands · 1 screenshot · 4m"). */
|
|
95
|
-
function summarizeTurn(items: ActivityItem[]): string {
|
|
96
|
+
function summarizeTurn(items: ActivityItem[], durationMs?: number): string {
|
|
96
97
|
let files = 0;
|
|
97
98
|
let commands = 0;
|
|
98
99
|
let screenshots = 0;
|
|
@@ -121,5 +122,26 @@ function summarizeTurn(items: ActivityItem[]): string {
|
|
|
121
122
|
if (screenshots) {
|
|
122
123
|
parts.push(`${screenshots} ${screenshots === 1 ? "screenshot" : "screenshots"}`);
|
|
123
124
|
}
|
|
125
|
+
const duration = formatDurationFacet(durationMs);
|
|
126
|
+
if (duration) {
|
|
127
|
+
parts.push(duration);
|
|
128
|
+
}
|
|
124
129
|
return parts.join(" · ");
|
|
125
130
|
}
|
|
131
|
+
|
|
132
|
+
function formatDurationFacet(durationMs: number | undefined): string | null {
|
|
133
|
+
if (durationMs === undefined || !Number.isFinite(durationMs) || durationMs < 1000) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const totalSeconds = Math.floor(durationMs / 1000);
|
|
137
|
+
if (totalSeconds < 60) {
|
|
138
|
+
return `${totalSeconds}s`;
|
|
139
|
+
}
|
|
140
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
141
|
+
if (totalMinutes < 60) {
|
|
142
|
+
return `${totalMinutes}m`;
|
|
143
|
+
}
|
|
144
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
145
|
+
const minutes = totalMinutes % 60;
|
|
146
|
+
return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
147
|
+
}
|
package/src/timeline/types.ts
CHANGED
|
@@ -69,11 +69,16 @@ export type WorkerItem = {
|
|
|
69
69
|
id: string;
|
|
70
70
|
turnId: string | null;
|
|
71
71
|
callId: string | null;
|
|
72
|
-
action: "spawn" | "message";
|
|
72
|
+
action: "spawn" | "message" | "interrupt";
|
|
73
73
|
/** The worker's initial message / the message sent to it, when parseable. */
|
|
74
74
|
prompt: string | null;
|
|
75
75
|
/** The target/spawned worker session id, when parseable from args/output. */
|
|
76
76
|
workerSessionId: string | null;
|
|
77
|
+
/**
|
|
78
|
+
* For an `interrupt` action, whether it stops the target (default) or steers
|
|
79
|
+
* it (cancel the current turn, keep the goal). Absent on spawn/message.
|
|
80
|
+
*/
|
|
81
|
+
mode?: "stop" | "steer";
|
|
77
82
|
status: "running" | "complete" | "failed" | "cancelled";
|
|
78
83
|
occurredAt: string;
|
|
79
84
|
};
|
|
@@ -112,6 +117,17 @@ export type NoticeItem = {
|
|
|
112
117
|
occurredAt: string;
|
|
113
118
|
};
|
|
114
119
|
|
|
120
|
+
export type TurnOutcome = "complete" | "failed" | "cancelled";
|
|
121
|
+
|
|
122
|
+
export type TurnEndItem = {
|
|
123
|
+
kind: "turn-end";
|
|
124
|
+
id: string;
|
|
125
|
+
turnId: string | null;
|
|
126
|
+
outcome: TurnOutcome;
|
|
127
|
+
failureText: string | null;
|
|
128
|
+
occurredAt: string;
|
|
129
|
+
};
|
|
130
|
+
|
|
115
131
|
export type TimelineItem =
|
|
116
132
|
| UserMessageItem
|
|
117
133
|
| AgentMessageItem
|
|
@@ -121,11 +137,21 @@ export type TimelineItem =
|
|
|
121
137
|
| SandboxItem
|
|
122
138
|
| SessionStatusItem
|
|
123
139
|
| GoalItem
|
|
124
|
-
| NoticeItem
|
|
140
|
+
| NoticeItem
|
|
141
|
+
| TurnEndItem;
|
|
125
142
|
|
|
126
143
|
/** Activity items cluster between chat messages (reasoning, tools, workers, sandbox). */
|
|
127
144
|
export type ActivityItem = ReasoningItem | ToolCallItem | WorkerItem | SandboxItem;
|
|
128
145
|
|
|
129
146
|
export type TimelineGroup =
|
|
130
147
|
| { kind: "item"; item: TimelineItem }
|
|
131
|
-
| { kind: "activity"; id: string; items: ActivityItem[] }
|
|
148
|
+
| { kind: "activity"; id: string; items: ActivityItem[]; outcome?: TurnOutcome; failureText?: string }
|
|
149
|
+
| {
|
|
150
|
+
kind: "turn";
|
|
151
|
+
id: string;
|
|
152
|
+
outcome: TurnOutcome;
|
|
153
|
+
failureText?: string;
|
|
154
|
+
startedAt: string;
|
|
155
|
+
endedAt: string;
|
|
156
|
+
groups: TimelineGroup[];
|
|
157
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// ----------------------------------------------------------------------------
|
|
2
|
+
// Bring-your-own-compute — Machines view-model.
|
|
3
|
+
//
|
|
4
|
+
// The data-contract types (MachineView / MetricSample / MachinesResponse /
|
|
5
|
+
// MachineMetricsSeriesResponse / MachineKind / MachineState) are the
|
|
6
|
+
// orchestrator-owned SHARED DATA CONTRACT that M10 ships in `@opengeni/sdk`
|
|
7
|
+
// (hand-written mirrors of `@opengeni/contracts`, pinned by contract-parity).
|
|
8
|
+
// M9 RE-EXPORTS them here so the dashboard UI imports a single, stable name and
|
|
9
|
+
// never drifts from the API — exactly as the contract intends.
|
|
10
|
+
//
|
|
11
|
+
// (During concurrent M9/M10 development this module briefly carried a local
|
|
12
|
+
// view-model with the same field names; once M10 landed the SDK types we
|
|
13
|
+
// reconciled to re-export them — the field names were identical by design.)
|
|
14
|
+
//
|
|
15
|
+
// Endpoints these model (M10 owns the SDK client methods):
|
|
16
|
+
// GET /v1/workspaces/:ws/machines -> MachinesResponse
|
|
17
|
+
// GET /v1/workspaces/:ws/machines/:enrollmentId/metrics/series?window=1h
|
|
18
|
+
// -> { samples: MetricSample[] }
|
|
19
|
+
// ----------------------------------------------------------------------------
|
|
20
|
+
import type {
|
|
21
|
+
MachineKind,
|
|
22
|
+
MachineMetricsSeriesResponse,
|
|
23
|
+
MachinesResponse,
|
|
24
|
+
MachineState,
|
|
25
|
+
MachineView,
|
|
26
|
+
MetricSample,
|
|
27
|
+
} from "@opengeni/sdk";
|
|
28
|
+
|
|
29
|
+
export type {
|
|
30
|
+
MachineKind,
|
|
31
|
+
MachineMetricsSeriesResponse,
|
|
32
|
+
MachinesResponse,
|
|
33
|
+
MachineState,
|
|
34
|
+
MachineView,
|
|
35
|
+
MetricSample,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// --- connection-status grouping (a PURE UI projection — not a contract type) --
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The three connection-status pill values the UI surfaces across the dashboard,
|
|
42
|
+
* the dock, and the timeline. `consent_required` / `display_unavailable` /
|
|
43
|
+
* `enrolling` are NOT connection states — they map onto a connection state for
|
|
44
|
+
* the pill and carry their own badge separately.
|
|
45
|
+
*/
|
|
46
|
+
export type ConnectionStatus = "online" | "reconnecting" | "offline";
|
|
47
|
+
|
|
48
|
+
/** Project a machine `state` onto its connection-status pill value. */
|
|
49
|
+
export function connectionStatusForState(state: MachineState): ConnectionStatus {
|
|
50
|
+
switch (state) {
|
|
51
|
+
case "online":
|
|
52
|
+
case "consent_required":
|
|
53
|
+
case "display_unavailable":
|
|
54
|
+
// The control plane is reachable; the limitation is consent / no display.
|
|
55
|
+
return "online";
|
|
56
|
+
case "reconnecting":
|
|
57
|
+
case "enrolling":
|
|
58
|
+
return "reconnecting";
|
|
59
|
+
case "offline":
|
|
60
|
+
return "offline";
|
|
61
|
+
default: {
|
|
62
|
+
// Exhaustiveness guard — a new state must be classified explicitly.
|
|
63
|
+
const _never: never = state;
|
|
64
|
+
return _never;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|