@opengeni/react 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -2
- package/dist/chunk-DEW2ZNF2.js +1238 -0
- package/dist/chunk-DEW2ZNF2.js.map +1 -0
- package/dist/index.d.ts +191 -61
- package/dist/index.js +1501 -1009
- 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/client.ts +1 -0
- 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/fleet-tile.tsx +5 -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/hooks/use-session.ts +80 -12
- package/src/index.ts +16 -1
- package/src/lib/git-patch.ts +10 -4
- 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/parsers.ts +6 -1
- package/src/timeline/projection.ts +203 -16
- package/src/timeline/turn-summary.tsx +32 -10
- package/src/timeline/types.ts +29 -3
- package/src/types/machines.ts +67 -0
|
@@ -145,13 +145,19 @@ function WorkerRow({ item, onOpenSession }: { item: WorkerItem; onOpenSession?:
|
|
|
145
145
|
: cancelled
|
|
146
146
|
? "Worker interrupted"
|
|
147
147
|
: "Worker spawned"
|
|
148
|
-
:
|
|
149
|
-
?
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
: item.action === "interrupt"
|
|
149
|
+
? (() => {
|
|
150
|
+
const verb = item.mode === "steer" ? "Steering" : "Stopping";
|
|
151
|
+
const done = item.mode === "steer" ? "Worker steered" : "Worker stopped";
|
|
152
|
+
return running ? `${verb} worker` : failed ? "Worker interrupt failed" : cancelled ? "Worker interrupted" : done;
|
|
153
|
+
})()
|
|
154
|
+
: running
|
|
155
|
+
? "Messaging worker"
|
|
156
|
+
: failed
|
|
157
|
+
? "Worker message failed"
|
|
158
|
+
: cancelled
|
|
159
|
+
? "Worker interrupted"
|
|
160
|
+
: "Worker messaged";
|
|
155
161
|
return (
|
|
156
162
|
<div
|
|
157
163
|
className={cn(
|
package/src/timeline/index.ts
CHANGED
package/src/timeline/parsers.ts
CHANGED
|
@@ -131,7 +131,12 @@ export function v4aToGitFileDiff(op: ApplyPatchOperation): GitFileDiff {
|
|
|
131
131
|
hunks.push(cur);
|
|
132
132
|
} else if (cur || op.type === "create_file") {
|
|
133
133
|
if (!cur) {
|
|
134
|
-
|
|
134
|
+
// No `@@` anchor on a create_file body: synthesize an add-only hunk.
|
|
135
|
+
// Leave `header` empty so `gitFileDiffToPatch` regenerates a valid
|
|
136
|
+
// `@@ -0,0 +1,N @@` from the range fields once newLines is counted — a
|
|
137
|
+
// pre-baked partial header (e.g. `@@ +1 @@`) renders zero lines in a
|
|
138
|
+
// generic unified-diff parser.
|
|
139
|
+
cur = { oldStart: 0, oldLines: 0, newStart: 1, newLines: 0, header: "", lines: [] };
|
|
135
140
|
hunks.push(cur);
|
|
136
141
|
oldNo = 0;
|
|
137
142
|
newNo = 1;
|
|
@@ -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",
|
|
@@ -283,30 +302,40 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
283
302
|
|
|
284
303
|
case "turn.completed": {
|
|
285
304
|
finalizeOpen(turnId);
|
|
305
|
+
items.push(turnEndItem(event, "complete", null));
|
|
286
306
|
break;
|
|
287
307
|
}
|
|
288
308
|
|
|
289
309
|
case "turn.failed": {
|
|
310
|
+
const hadActivity = hasTurnActivity(items, turnId);
|
|
311
|
+
const failureText = failureMessage(payload);
|
|
290
312
|
finalizeOpen(turnId, "failed");
|
|
291
|
-
items.push(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
313
|
+
items.push(turnEndItem(event, "failed", failureText));
|
|
314
|
+
if (!hadActivity) {
|
|
315
|
+
items.push({
|
|
316
|
+
kind: "notice",
|
|
317
|
+
id: event.id,
|
|
318
|
+
tone: "failed",
|
|
319
|
+
text: failureText ?? "The turn failed.",
|
|
320
|
+
occurredAt: event.occurredAt,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
298
323
|
break;
|
|
299
324
|
}
|
|
300
325
|
|
|
301
326
|
case "turn.cancelled": {
|
|
327
|
+
const hadActivity = hasTurnActivity(items, turnId);
|
|
302
328
|
finalizeOpen(turnId, "cancelled");
|
|
303
|
-
items.push(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
329
|
+
items.push(turnEndItem(event, "cancelled", null));
|
|
330
|
+
if (!hadActivity) {
|
|
331
|
+
items.push({
|
|
332
|
+
kind: "notice",
|
|
333
|
+
id: event.id,
|
|
334
|
+
tone: "cancelled",
|
|
335
|
+
text: "Interrupted.",
|
|
336
|
+
occurredAt: event.occurredAt,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
310
339
|
break;
|
|
311
340
|
}
|
|
312
341
|
|
|
@@ -351,7 +380,8 @@ export function sessionStatusFromEvents(events: SessionEvent[]): SessionStatus |
|
|
|
351
380
|
|
|
352
381
|
/* ----------------------------------------------------------------------------
|
|
353
382
|
Visual grouping: consecutive activity items (reasoning / tools / workers /
|
|
354
|
-
sandbox) cluster into
|
|
383
|
+
sandbox) cluster into collapsible blocks. Once a turn settles, the full
|
|
384
|
+
non-user span folds behind a turn group, with activity blocks nested inside.
|
|
355
385
|
-------------------------------------------------------------------------- */
|
|
356
386
|
|
|
357
387
|
/**
|
|
@@ -376,13 +406,18 @@ export function groupTimeline(items: TimelineItem[]): TimelineGroup[] {
|
|
|
376
406
|
for (const item of items) {
|
|
377
407
|
if (isActivityItem(item)) {
|
|
378
408
|
const open = groups[groups.length - 1];
|
|
379
|
-
if (open?.kind === "activity") {
|
|
409
|
+
if (open?.kind === "activity" && open.outcome === undefined) {
|
|
380
410
|
open.items.push(item);
|
|
381
411
|
} else {
|
|
382
412
|
groups.push({ kind: "activity", id: `activity-${item.id}`, items: [item] });
|
|
383
413
|
}
|
|
384
414
|
continue;
|
|
385
415
|
}
|
|
416
|
+
if (item.kind === "turn-end") {
|
|
417
|
+
stampTurnOutcome(groups, item);
|
|
418
|
+
foldSettledTurn(groups, item);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
386
421
|
groups.push({ kind: "item", item });
|
|
387
422
|
}
|
|
388
423
|
return groups;
|
|
@@ -390,6 +425,152 @@ export function groupTimeline(items: TimelineItem[]): TimelineGroup[] {
|
|
|
390
425
|
|
|
391
426
|
/* --- helpers ---------------------------------------------------------------- */
|
|
392
427
|
|
|
428
|
+
function turnEndItem(
|
|
429
|
+
event: SessionEvent,
|
|
430
|
+
outcome: TurnEndItem["outcome"],
|
|
431
|
+
failureText: string | null,
|
|
432
|
+
): TurnEndItem {
|
|
433
|
+
return {
|
|
434
|
+
kind: "turn-end",
|
|
435
|
+
id: `${event.id}-turn-end`,
|
|
436
|
+
turnId: event.turnId ?? null,
|
|
437
|
+
outcome,
|
|
438
|
+
failureText,
|
|
439
|
+
occurredAt: event.occurredAt,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function hasTurnActivity(items: TimelineItem[], turnId: string | null): boolean {
|
|
444
|
+
if (turnId) {
|
|
445
|
+
return items.some((item) => isActivityItem(item) && item.turnId === turnId);
|
|
446
|
+
}
|
|
447
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
448
|
+
const item = items[index];
|
|
449
|
+
if (!item || item.kind === "turn-end" || item.kind === "user-message") {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
if (isActivityItem(item)) {
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function stampTurnOutcome(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
|
|
460
|
+
if (turnEnd.turnId === null) {
|
|
461
|
+
const trailing = groups[groups.length - 1];
|
|
462
|
+
if (trailing?.kind === "activity" && trailing.outcome === undefined) {
|
|
463
|
+
applyTurnOutcome(trailing, turnEnd);
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
for (const group of groups) {
|
|
468
|
+
if (group.kind !== "activity" || group.outcome !== undefined) {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (group.items.some((activity) => activity.turnId === turnEnd.turnId)) {
|
|
472
|
+
applyTurnOutcome(group, turnEnd);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function applyTurnOutcome(group: Extract<TimelineGroup, { kind: "activity" }>, turnEnd: TurnEndItem): void {
|
|
478
|
+
group.outcome = turnEnd.outcome;
|
|
479
|
+
if (turnEnd.failureText) {
|
|
480
|
+
group.failureText = turnEnd.failureText;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function foldSettledTurn(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
|
|
485
|
+
let startIndex = groups.length;
|
|
486
|
+
let stoppedAtForeignTurn = false;
|
|
487
|
+
while (startIndex > 0) {
|
|
488
|
+
const previous = groups[startIndex - 1];
|
|
489
|
+
if (isTurnBoundary(previous)) {
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
if (belongsToDifferentTurn(previous, turnEnd.turnId)) {
|
|
493
|
+
stoppedAtForeignTurn = true;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
startIndex -= 1;
|
|
497
|
+
}
|
|
498
|
+
if (stoppedAtForeignTurn) {
|
|
499
|
+
while (startIndex < groups.length && isBetweenTurnDivider(groups[startIndex])) {
|
|
500
|
+
startIndex += 1;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const collected = groups.slice(startIndex);
|
|
505
|
+
if (collected.length === 0) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const finalMessage = extractFinalAgentMessage(collected, turnEnd);
|
|
510
|
+
const body = finalMessage ? collected.slice(0, -1) : collected;
|
|
511
|
+
if (body.length === 0) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const firstOccurredAt = groupStartedAt(body[0]) ?? turnEnd.occurredAt;
|
|
516
|
+
const turnGroup: TimelineGroup = {
|
|
517
|
+
kind: "turn",
|
|
518
|
+
id: `turn-${turnEnd.turnId ?? turnEnd.id}`,
|
|
519
|
+
outcome: turnEnd.outcome,
|
|
520
|
+
startedAt: firstOccurredAt,
|
|
521
|
+
endedAt: turnEnd.occurredAt,
|
|
522
|
+
groups: body,
|
|
523
|
+
};
|
|
524
|
+
if (turnEnd.failureText) {
|
|
525
|
+
turnGroup.failureText = turnEnd.failureText;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
groups.splice(startIndex, collected.length, ...(finalMessage ? [turnGroup, finalMessage] : [turnGroup]));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function isTurnBoundary(group: TimelineGroup | undefined): boolean {
|
|
532
|
+
return group?.kind === "turn" || (group?.kind === "item" && group.item.kind === "user-message");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function belongsToDifferentTurn(group: TimelineGroup | undefined, turnId: string | null): boolean {
|
|
536
|
+
if (!group || !turnId) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
if (group.kind === "activity") {
|
|
540
|
+
return group.items.length > 0 && group.items.every((item) => item.turnId !== null && item.turnId !== turnId);
|
|
541
|
+
}
|
|
542
|
+
return group.kind === "item" && group.item.kind === "agent-message" && group.item.turnId !== null && group.item.turnId !== turnId;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function isBetweenTurnDivider(group: TimelineGroup | undefined): boolean {
|
|
546
|
+
return group?.kind === "item" && group.item.kind === "session-status" && group.item.status !== "running";
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function extractFinalAgentMessage(groups: TimelineGroup[], turnEnd: TurnEndItem): Extract<TimelineGroup, { kind: "item" }> | null {
|
|
550
|
+
const tail = groups[groups.length - 1];
|
|
551
|
+
if (tail?.kind !== "item" || tail.item.kind !== "agent-message" || tail.item.streaming) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
if (tail.item.turnId && turnEnd.turnId && tail.item.turnId !== turnEnd.turnId) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
return tail;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function groupStartedAt(group: TimelineGroup | undefined): string | undefined {
|
|
561
|
+
if (!group) {
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
switch (group.kind) {
|
|
565
|
+
case "item":
|
|
566
|
+
return group.item.occurredAt;
|
|
567
|
+
case "activity":
|
|
568
|
+
return group.items[0]?.occurredAt;
|
|
569
|
+
case "turn":
|
|
570
|
+
return group.startedAt;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
393
574
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
394
575
|
return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
395
576
|
}
|
|
@@ -502,6 +683,12 @@ function workerPrompt(args: unknown): string | null {
|
|
|
502
683
|
return null;
|
|
503
684
|
}
|
|
504
685
|
|
|
686
|
+
/** The interrupt mode from `session_interrupt` args; defaults to "stop". */
|
|
687
|
+
function workerInterruptMode(args: unknown): "stop" | "steer" {
|
|
688
|
+
const record = asRecord(typeof args === "string" ? tryParseJson(args) : args);
|
|
689
|
+
return record.mode === "steer" ? "steer" : "stop";
|
|
690
|
+
}
|
|
691
|
+
|
|
505
692
|
/**
|
|
506
693
|
* Find a session id in orchestration tool arguments or output. Handles raw
|
|
507
694
|
* 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
|
+
}
|