@agentuity/coder 1.0.39 → 1.0.41
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/dist/client.d.ts +2 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -1
- package/dist/hub-overlay-state.d.ts +30 -0
- package/dist/hub-overlay-state.d.ts.map +1 -0
- package/dist/hub-overlay-state.js +68 -0
- package/dist/hub-overlay-state.js.map +1 -0
- package/dist/hub-overlay.d.ts +41 -2
- package/dist/hub-overlay.d.ts.map +1 -1
- package/dist/hub-overlay.js +667 -115
- package/dist/hub-overlay.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -34
- package/dist/index.js.map +1 -1
- package/dist/native-remote-ui-context.d.ts +5 -0
- package/dist/native-remote-ui-context.d.ts.map +1 -0
- package/dist/native-remote-ui-context.js +30 -0
- package/dist/native-remote-ui-context.js.map +1 -0
- package/dist/protocol.d.ts +210 -38
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +2 -1
- package/dist/protocol.js.map +1 -1
- package/dist/remote-lifecycle.d.ts +61 -0
- package/dist/remote-lifecycle.d.ts.map +1 -0
- package/dist/remote-lifecycle.js +190 -0
- package/dist/remote-lifecycle.js.map +1 -0
- package/dist/remote-session.d.ts +15 -0
- package/dist/remote-session.d.ts.map +1 -1
- package/dist/remote-session.js +240 -35
- package/dist/remote-session.js.map +1 -1
- package/dist/remote-tui.d.ts.map +1 -1
- package/dist/remote-tui.js +95 -12
- package/dist/remote-tui.js.map +1 -1
- package/dist/remote-ui-handler.d.ts +5 -0
- package/dist/remote-ui-handler.d.ts.map +1 -0
- package/dist/remote-ui-handler.js +53 -0
- package/dist/remote-ui-handler.js.map +1 -0
- package/package.json +5 -5
- package/src/client.ts +5 -3
- package/src/hub-overlay-state.ts +117 -0
- package/src/hub-overlay.ts +974 -138
- package/src/index.ts +19 -50
- package/src/native-remote-ui-context.ts +41 -0
- package/src/protocol.ts +270 -56
- package/src/remote-lifecycle.ts +270 -0
- package/src/remote-session.ts +293 -38
- package/src/remote-tui.ts +129 -13
- package/src/remote-ui-handler.ts +86 -0
package/src/hub-overlay.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { type Theme, getMarkdownTheme } from '@mariozechner/pi-coding-agent';
|
|
2
2
|
import { matchesKey, Markdown as MdComponent } from '@mariozechner/pi-tui';
|
|
3
|
+
import {
|
|
4
|
+
buildProjectionFromEntries,
|
|
5
|
+
normalizeStreamProjection,
|
|
6
|
+
shouldReplaceStreamProjection,
|
|
7
|
+
type ConversationEntryLike,
|
|
8
|
+
type StreamBuffer,
|
|
9
|
+
type StreamProjection,
|
|
10
|
+
type StreamProjectionSource,
|
|
11
|
+
} from './hub-overlay-state.ts';
|
|
3
12
|
import { truncateToWidth } from './renderers.ts';
|
|
4
13
|
|
|
5
14
|
interface Component {
|
|
@@ -26,6 +35,20 @@ interface HubSessionSummary {
|
|
|
26
35
|
taskCount: number;
|
|
27
36
|
participantCount: number;
|
|
28
37
|
createdAt: string;
|
|
38
|
+
bucket?: 'running' | 'paused' | 'provisioning' | 'history';
|
|
39
|
+
runtimeAvailable?: boolean;
|
|
40
|
+
controlAvailable?: boolean;
|
|
41
|
+
historyOnly?: boolean;
|
|
42
|
+
tags?: string[];
|
|
43
|
+
skills?: HubSessionSkillRef[];
|
|
44
|
+
defaultAgent?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface HubSessionSkillRef {
|
|
48
|
+
skillId: string;
|
|
49
|
+
repo: string;
|
|
50
|
+
name?: string;
|
|
51
|
+
url?: string;
|
|
29
52
|
}
|
|
30
53
|
|
|
31
54
|
interface HubParticipant {
|
|
@@ -72,16 +95,40 @@ type AgentActivity = Record<
|
|
|
72
95
|
status?: string;
|
|
73
96
|
currentTool?: string;
|
|
74
97
|
toolCallCount?: number;
|
|
75
|
-
lastActivity?: string;
|
|
98
|
+
lastActivity?: string | number;
|
|
99
|
+
currentToolArgs?: string;
|
|
100
|
+
totalElapsed?: number;
|
|
76
101
|
}
|
|
77
102
|
>;
|
|
78
103
|
|
|
104
|
+
interface HubSessionDiagnostics {
|
|
105
|
+
inactiveRunningTasks?: Array<{
|
|
106
|
+
taskId: string;
|
|
107
|
+
agent: string;
|
|
108
|
+
inactivityMs: number;
|
|
109
|
+
startedAt: string;
|
|
110
|
+
lastActivityAt?: string;
|
|
111
|
+
}>;
|
|
112
|
+
}
|
|
113
|
+
|
|
79
114
|
interface HubSessionDetail {
|
|
80
115
|
sessionId: string;
|
|
81
116
|
label?: string;
|
|
82
117
|
status: string;
|
|
83
118
|
createdAt: string;
|
|
84
119
|
mode: string;
|
|
120
|
+
task?: string;
|
|
121
|
+
error?: string;
|
|
122
|
+
streamId?: string | null;
|
|
123
|
+
streamUrl?: string | null;
|
|
124
|
+
tags?: string[];
|
|
125
|
+
skills?: HubSessionSkillRef[];
|
|
126
|
+
defaultAgent?: string;
|
|
127
|
+
bucket?: 'running' | 'paused' | 'provisioning' | 'history';
|
|
128
|
+
runtimeAvailable?: boolean;
|
|
129
|
+
controlAvailable?: boolean;
|
|
130
|
+
historyOnly?: boolean;
|
|
131
|
+
diagnostics?: HubSessionDiagnostics;
|
|
85
132
|
context?: {
|
|
86
133
|
branch?: string;
|
|
87
134
|
workingDirectory?: string;
|
|
@@ -92,11 +139,7 @@ interface HubSessionDetail {
|
|
|
92
139
|
todoSummary?: HubTodoSummary;
|
|
93
140
|
todosUnavailable?: string;
|
|
94
141
|
agentActivity?: AgentActivity;
|
|
95
|
-
stream?:
|
|
96
|
-
output?: string;
|
|
97
|
-
thinking?: string;
|
|
98
|
-
tasks?: Record<string, { output?: string; thinking?: string }>;
|
|
99
|
-
};
|
|
142
|
+
stream?: StreamProjection;
|
|
100
143
|
}
|
|
101
144
|
|
|
102
145
|
interface HubTodoListResponse {
|
|
@@ -114,6 +157,27 @@ interface HubListResponse {
|
|
|
114
157
|
};
|
|
115
158
|
}
|
|
116
159
|
|
|
160
|
+
interface HubReplayResponse {
|
|
161
|
+
sessionId: string;
|
|
162
|
+
entries?: ConversationEntryLike[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface HubEventHistoryItem {
|
|
166
|
+
id: number;
|
|
167
|
+
event: string;
|
|
168
|
+
category?: string;
|
|
169
|
+
agent?: string;
|
|
170
|
+
taskId?: string;
|
|
171
|
+
payload?: unknown;
|
|
172
|
+
occurredAt: string;
|
|
173
|
+
ingestedAt?: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface HubEventHistoryResponse {
|
|
177
|
+
sessionId: string;
|
|
178
|
+
events?: HubEventHistoryItem[];
|
|
179
|
+
}
|
|
180
|
+
|
|
117
181
|
interface FeedEntry {
|
|
118
182
|
at: number;
|
|
119
183
|
sessionId?: string;
|
|
@@ -132,11 +196,6 @@ interface SessionStreamState {
|
|
|
132
196
|
mode: 'summary' | 'full';
|
|
133
197
|
}
|
|
134
198
|
|
|
135
|
-
interface StreamBuffer {
|
|
136
|
-
output: string;
|
|
137
|
-
thinking: string;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
199
|
interface HubOverlayOptions {
|
|
141
200
|
baseUrl: string;
|
|
142
201
|
currentSessionId?: string;
|
|
@@ -150,9 +209,46 @@ type ScreenMode = 'list' | 'detail' | 'feed' | 'task';
|
|
|
150
209
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
151
210
|
const POLL_MS = 4_000;
|
|
152
211
|
const REQUEST_TIMEOUT_MS = 5_000;
|
|
212
|
+
const HISTORY_FETCH_FAILURE_COOLDOWN_MS = 15_000;
|
|
153
213
|
const MAX_FEED_ITEMS = 80;
|
|
154
214
|
const STREAM_SESSION_LIMIT = 8;
|
|
155
215
|
|
|
216
|
+
class HubRequestError extends Error {
|
|
217
|
+
public readonly _tag = 'HubRequestError';
|
|
218
|
+
public readonly path: string;
|
|
219
|
+
public readonly status: number;
|
|
220
|
+
public readonly statusText?: string;
|
|
221
|
+
public readonly body?: string;
|
|
222
|
+
public readonly plainArgs: {
|
|
223
|
+
path: string;
|
|
224
|
+
status: number;
|
|
225
|
+
statusText?: string;
|
|
226
|
+
body?: string;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
constructor(args: {
|
|
230
|
+
message: string;
|
|
231
|
+
path: string;
|
|
232
|
+
status: number;
|
|
233
|
+
statusText?: string;
|
|
234
|
+
body?: string;
|
|
235
|
+
cause?: unknown;
|
|
236
|
+
}) {
|
|
237
|
+
super(args.message, args.cause === undefined ? undefined : { cause: args.cause });
|
|
238
|
+
this.name = 'HubRequestError';
|
|
239
|
+
this.path = args.path;
|
|
240
|
+
this.status = args.status;
|
|
241
|
+
this.statusText = args.statusText;
|
|
242
|
+
this.body = args.body;
|
|
243
|
+
this.plainArgs = {
|
|
244
|
+
path: args.path,
|
|
245
|
+
status: args.status,
|
|
246
|
+
statusText: args.statusText,
|
|
247
|
+
body: args.body,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
156
252
|
function visibleWidth(text: string): number {
|
|
157
253
|
return text.replace(ANSI_RE, '').replace(/\t/g, ' ').length;
|
|
158
254
|
}
|
|
@@ -195,8 +291,8 @@ function formatClock(ms: number): string {
|
|
|
195
291
|
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
196
292
|
}
|
|
197
293
|
|
|
198
|
-
function formatRelative(
|
|
199
|
-
const ts = Date.parse(
|
|
294
|
+
function formatRelative(value: string | number): string {
|
|
295
|
+
const ts = typeof value === 'number' ? value : Date.parse(value);
|
|
200
296
|
if (Number.isNaN(ts)) return '-';
|
|
201
297
|
const seconds = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
202
298
|
if (seconds < 60) return `${seconds}s ago`;
|
|
@@ -208,6 +304,13 @@ function formatRelative(isoDate: string): string {
|
|
|
208
304
|
return `${days}d ago`;
|
|
209
305
|
}
|
|
210
306
|
|
|
307
|
+
function formatElapsedCompact(ms: number): string {
|
|
308
|
+
if (ms < 1_000) return `${ms}ms`;
|
|
309
|
+
if (ms < 60_000) return `${Math.round(ms / 1_000)}s`;
|
|
310
|
+
if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m`;
|
|
311
|
+
return `${Math.round(ms / 3_600_000)}h`;
|
|
312
|
+
}
|
|
313
|
+
|
|
211
314
|
function shortId(id: string): string {
|
|
212
315
|
if (id.length <= 12) return id;
|
|
213
316
|
return id.slice(0, 12);
|
|
@@ -411,15 +514,28 @@ export class HubOverlay implements Component, Focusable {
|
|
|
411
514
|
private cachedTodos: { todos: any[]; summary: any; sessionId: string } | null = null;
|
|
412
515
|
private feed: FeedEntry[] = [];
|
|
413
516
|
private sessionFeed = new Map<string, FeedEntry[]>();
|
|
517
|
+
private sessionHistoryFeed = new Map<string, FeedEntry[]>();
|
|
414
518
|
private sessionBuffers = new Map<string, StreamBuffer>();
|
|
415
519
|
private taskBuffers = new Map<string, StreamBuffer>();
|
|
416
|
-
private
|
|
520
|
+
private streamSources = new Map<string, StreamProjectionSource>();
|
|
521
|
+
private replayLoadedSessions = new Set<string>();
|
|
522
|
+
private replayInFlight = new Set<string>();
|
|
523
|
+
private replayRequestControllers = new Map<string, AbortController>();
|
|
524
|
+
private replayRequestGenerations = new Map<string, number>();
|
|
525
|
+
private replayFailureUntil = new Map<string, number>();
|
|
526
|
+
private eventHistoryLoadedSessions = new Set<string>();
|
|
527
|
+
private eventHistoryInFlight = new Set<string>();
|
|
528
|
+
private eventHistoryRequestControllers = new Map<string, AbortController>();
|
|
529
|
+
private eventHistoryRequestGenerations = new Map<string, number>();
|
|
530
|
+
private eventHistoryFailureUntil = new Map<string, number>();
|
|
417
531
|
private previousDigests = new Map<string, SessionDigest>();
|
|
418
532
|
|
|
419
533
|
private loadingList = true;
|
|
420
534
|
private loadingDetail = false;
|
|
421
535
|
private listError = '';
|
|
422
536
|
private detailError = '';
|
|
537
|
+
private detailTodoRequestId = 0;
|
|
538
|
+
private resumeBusySessionId: string | null = null;
|
|
423
539
|
private lastUpdatedAt = 0;
|
|
424
540
|
private listInFlight = false;
|
|
425
541
|
private detailInFlight = false;
|
|
@@ -590,6 +706,14 @@ export class HubOverlay implements Component, Focusable {
|
|
|
590
706
|
stream.controller.abort();
|
|
591
707
|
}
|
|
592
708
|
this.sseControllers.clear();
|
|
709
|
+
for (const controller of this.replayRequestControllers.values()) {
|
|
710
|
+
controller.abort();
|
|
711
|
+
}
|
|
712
|
+
this.replayRequestControllers.clear();
|
|
713
|
+
for (const controller of this.eventHistoryRequestControllers.values()) {
|
|
714
|
+
controller.abort();
|
|
715
|
+
}
|
|
716
|
+
this.eventHistoryRequestControllers.clear();
|
|
593
717
|
}
|
|
594
718
|
|
|
595
719
|
private requestRender(): void {
|
|
@@ -600,6 +724,81 @@ export class HubOverlay implements Component, Focusable {
|
|
|
600
724
|
}
|
|
601
725
|
}
|
|
602
726
|
|
|
727
|
+
private nextRequestGeneration(generations: Map<string, number>, sessionId: string): number {
|
|
728
|
+
const next = (generations.get(sessionId) ?? 0) + 1;
|
|
729
|
+
generations.set(sessionId, next);
|
|
730
|
+
return next;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private invalidateSessionRequest(
|
|
734
|
+
generations: Map<string, number>,
|
|
735
|
+
controllers: Map<string, AbortController>,
|
|
736
|
+
inFlight: Set<string>,
|
|
737
|
+
sessionId: string
|
|
738
|
+
): void {
|
|
739
|
+
this.nextRequestGeneration(generations, sessionId);
|
|
740
|
+
inFlight.delete(sessionId);
|
|
741
|
+
const controller = controllers.get(sessionId);
|
|
742
|
+
if (controller) controller.abort();
|
|
743
|
+
controllers.delete(sessionId);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
private isCurrentSessionRequest(
|
|
747
|
+
generations: Map<string, number>,
|
|
748
|
+
controllers: Map<string, AbortController>,
|
|
749
|
+
sessionId: string,
|
|
750
|
+
generation: number,
|
|
751
|
+
controller: AbortController
|
|
752
|
+
): boolean {
|
|
753
|
+
return (
|
|
754
|
+
!controller.signal.aborted &&
|
|
755
|
+
generations.get(sessionId) === generation &&
|
|
756
|
+
controllers.get(sessionId) === controller
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
private isFailureCoolingDown(failureUntil: Map<string, number>, sessionId: string): boolean {
|
|
761
|
+
const until = failureUntil.get(sessionId);
|
|
762
|
+
if (!until) return false;
|
|
763
|
+
if (until <= Date.now()) {
|
|
764
|
+
failureUntil.delete(sessionId);
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private markFailureCooldown(failureUntil: Map<string, number>, sessionId: string): void {
|
|
771
|
+
failureUntil.set(sessionId, Date.now() + HISTORY_FETCH_FAILURE_COOLDOWN_MS);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
private clearReplayFailureCooldown(sessionId: string): void {
|
|
775
|
+
this.replayFailureUntil.delete(sessionId);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
private clearEventHistoryFailureCooldown(sessionId: string): void {
|
|
779
|
+
this.eventHistoryFailureUntil.delete(sessionId);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private resetHistoricalSessionState(sessionId: string): void {
|
|
783
|
+
this.invalidateSessionRequest(
|
|
784
|
+
this.replayRequestGenerations,
|
|
785
|
+
this.replayRequestControllers,
|
|
786
|
+
this.replayInFlight,
|
|
787
|
+
sessionId
|
|
788
|
+
);
|
|
789
|
+
this.invalidateSessionRequest(
|
|
790
|
+
this.eventHistoryRequestGenerations,
|
|
791
|
+
this.eventHistoryRequestControllers,
|
|
792
|
+
this.eventHistoryInFlight,
|
|
793
|
+
sessionId
|
|
794
|
+
);
|
|
795
|
+
this.replayLoadedSessions.delete(sessionId);
|
|
796
|
+
this.eventHistoryLoadedSessions.delete(sessionId);
|
|
797
|
+
this.sessionHistoryFeed.delete(sessionId);
|
|
798
|
+
this.clearReplayFailureCooldown(sessionId);
|
|
799
|
+
this.clearEventHistoryFailureCooldown(sessionId);
|
|
800
|
+
}
|
|
801
|
+
|
|
603
802
|
private close(): void {
|
|
604
803
|
this.dispose();
|
|
605
804
|
this.done(undefined);
|
|
@@ -650,6 +849,13 @@ export class HubOverlay implements Component, Focusable {
|
|
|
650
849
|
private handleDetailInput(data: string): void {
|
|
651
850
|
const tasks = this.getDetailTasks();
|
|
652
851
|
|
|
852
|
+
if ((matchesKey(data, 'w') || data.toLowerCase() === 'w') && this.detailSessionId) {
|
|
853
|
+
if (this.canResumeSession(this.detail)) {
|
|
854
|
+
void this.resumeSession(this.detailSessionId);
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
653
859
|
if (matchesKey(data, 'up')) {
|
|
654
860
|
if (tasks.length > 0) {
|
|
655
861
|
this.selectedTaskIndex = (this.selectedTaskIndex - 1 + tasks.length) % tasks.length;
|
|
@@ -935,6 +1141,55 @@ export class HubOverlay implements Component, Focusable {
|
|
|
935
1141
|
return buffer;
|
|
936
1142
|
}
|
|
937
1143
|
|
|
1144
|
+
private getStreamSource(sessionId: string): StreamProjectionSource {
|
|
1145
|
+
return this.streamSources.get(sessionId) ?? 'none';
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
private setStreamSource(sessionId: string, source: StreamProjectionSource): void {
|
|
1149
|
+
this.streamSources.set(sessionId, source);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
private clearTaskBuffers(sessionId: string): void {
|
|
1153
|
+
for (const key of this.taskBuffers.keys()) {
|
|
1154
|
+
if (key.startsWith(`${sessionId}:`)) {
|
|
1155
|
+
this.taskBuffers.delete(key);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
private replaceStreamProjection(
|
|
1161
|
+
sessionId: string,
|
|
1162
|
+
projectionRaw: StreamProjection | null | undefined,
|
|
1163
|
+
source: Exclude<StreamProjectionSource, 'live'>
|
|
1164
|
+
): boolean {
|
|
1165
|
+
if (!projectionRaw) return false;
|
|
1166
|
+
const currentSource = this.getStreamSource(sessionId);
|
|
1167
|
+
if (!shouldReplaceStreamProjection(currentSource, source)) return false;
|
|
1168
|
+
|
|
1169
|
+
const projection = normalizeStreamProjection(projectionRaw);
|
|
1170
|
+
const sessionBuffer = this.getSessionBuffer(sessionId);
|
|
1171
|
+
sessionBuffer.output = projection.output;
|
|
1172
|
+
sessionBuffer.thinking = projection.thinking;
|
|
1173
|
+
|
|
1174
|
+
this.clearTaskBuffers(sessionId);
|
|
1175
|
+
for (const [taskId, block] of Object.entries(projection.tasks)) {
|
|
1176
|
+
const taskBuffer = this.getTaskBuffer(sessionId, taskId);
|
|
1177
|
+
taskBuffer.output = block.output;
|
|
1178
|
+
taskBuffer.thinking = block.thinking;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
this.setStreamSource(sessionId, source);
|
|
1182
|
+
return true;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
private applyReplayEntries(sessionId: string, entries: ConversationEntryLike[]): void {
|
|
1186
|
+
const projection = buildProjectionFromEntries(entries);
|
|
1187
|
+
this.clearReplayFailureCooldown(sessionId);
|
|
1188
|
+
if (this.replaceStreamProjection(sessionId, projection, 'replay')) {
|
|
1189
|
+
this.replayLoadedSessions.add(sessionId);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
938
1193
|
private appendBufferText(
|
|
939
1194
|
sessionId: string,
|
|
940
1195
|
kind: 'output' | 'thinking',
|
|
@@ -942,6 +1197,16 @@ export class HubOverlay implements Component, Focusable {
|
|
|
942
1197
|
taskId?: string
|
|
943
1198
|
): void {
|
|
944
1199
|
if (!chunk) return;
|
|
1200
|
+
this.clearReplayFailureCooldown(sessionId);
|
|
1201
|
+
if (this.replayInFlight.has(sessionId)) {
|
|
1202
|
+
this.invalidateSessionRequest(
|
|
1203
|
+
this.replayRequestGenerations,
|
|
1204
|
+
this.replayRequestControllers,
|
|
1205
|
+
this.replayInFlight,
|
|
1206
|
+
sessionId
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
this.setStreamSource(sessionId, 'live');
|
|
945
1210
|
|
|
946
1211
|
// Session stream is lead/top-level only; task-scoped output lives in task buffers.
|
|
947
1212
|
if (!taskId) {
|
|
@@ -1013,20 +1278,61 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1013
1278
|
return ` ${tab('1', 'List', active === 'list')}${divider}${tab('2', 'Feed', active === 'feed')}`;
|
|
1014
1279
|
}
|
|
1015
1280
|
|
|
1016
|
-
private async fetchJson<T>(
|
|
1281
|
+
private async fetchJson<T>(
|
|
1282
|
+
path: string,
|
|
1283
|
+
timeoutMs = REQUEST_TIMEOUT_MS,
|
|
1284
|
+
init?: RequestInit
|
|
1285
|
+
): Promise<T> {
|
|
1017
1286
|
const controller = new AbortController();
|
|
1018
1287
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1019
1288
|
try {
|
|
1020
1289
|
// TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
|
|
1021
1290
|
const apiKey = process.env.AGENTUITY_CODER_API_KEY;
|
|
1022
|
-
const headers: Record<string, string> = {
|
|
1291
|
+
const headers: Record<string, string> = {
|
|
1292
|
+
accept: 'application/json',
|
|
1293
|
+
...(init?.headers && typeof init.headers === 'object'
|
|
1294
|
+
? (init.headers as Record<string, string>)
|
|
1295
|
+
: {}),
|
|
1296
|
+
};
|
|
1023
1297
|
if (apiKey) headers['x-agentuity-auth-api-key'] = apiKey;
|
|
1298
|
+
const signal = init?.signal
|
|
1299
|
+
? AbortSignal.any([controller.signal, init.signal])
|
|
1300
|
+
: controller.signal;
|
|
1024
1301
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
1302
|
+
...init,
|
|
1025
1303
|
headers,
|
|
1026
|
-
signal
|
|
1304
|
+
signal,
|
|
1027
1305
|
});
|
|
1028
1306
|
if (!response.ok) {
|
|
1029
|
-
|
|
1307
|
+
let message = `Hub returned ${response.status}`;
|
|
1308
|
+
let rawBody = '';
|
|
1309
|
+
try {
|
|
1310
|
+
const text = await response.text();
|
|
1311
|
+
if (text) {
|
|
1312
|
+
rawBody = text;
|
|
1313
|
+
try {
|
|
1314
|
+
const parsed = JSON.parse(text) as { error?: unknown; message?: unknown };
|
|
1315
|
+
if (typeof parsed.error === 'string' && parsed.error.trim()) {
|
|
1316
|
+
message = parsed.error;
|
|
1317
|
+
} else if (typeof parsed.message === 'string' && parsed.message.trim()) {
|
|
1318
|
+
message = parsed.message;
|
|
1319
|
+
} else {
|
|
1320
|
+
message = text;
|
|
1321
|
+
}
|
|
1322
|
+
} catch {
|
|
1323
|
+
message = text;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
} catch {
|
|
1327
|
+
// Fall back to the status-only message.
|
|
1328
|
+
}
|
|
1329
|
+
throw new HubRequestError({
|
|
1330
|
+
message,
|
|
1331
|
+
path,
|
|
1332
|
+
status: response.status,
|
|
1333
|
+
statusText: response.statusText || undefined,
|
|
1334
|
+
body: rawBody || undefined,
|
|
1335
|
+
});
|
|
1030
1336
|
}
|
|
1031
1337
|
return (await response.json()) as T;
|
|
1032
1338
|
} finally {
|
|
@@ -1034,6 +1340,171 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1034
1340
|
}
|
|
1035
1341
|
}
|
|
1036
1342
|
|
|
1343
|
+
private getSessionSummary(sessionId: string): HubSessionSummary | undefined {
|
|
1344
|
+
return this.sessions.find((item) => item.sessionId === sessionId);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
private isRuntimeAvailable(sessionId: string): boolean {
|
|
1348
|
+
const detailRuntime =
|
|
1349
|
+
this.detail?.sessionId === sessionId ? this.detail.runtimeAvailable : undefined;
|
|
1350
|
+
if (typeof detailRuntime === 'boolean') return detailRuntime;
|
|
1351
|
+
|
|
1352
|
+
const session = this.getSessionSummary(sessionId);
|
|
1353
|
+
if (typeof session?.runtimeAvailable === 'boolean') return session.runtimeAvailable;
|
|
1354
|
+
if (session?.historyOnly) return false;
|
|
1355
|
+
if (session?.bucket === 'history') return false;
|
|
1356
|
+
return true;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
private isHistoryOnly(sessionId: string): boolean {
|
|
1360
|
+
if (this.detail?.sessionId === sessionId && typeof this.detail.historyOnly === 'boolean') {
|
|
1361
|
+
return this.detail.historyOnly;
|
|
1362
|
+
}
|
|
1363
|
+
return this.getSessionSummary(sessionId)?.historyOnly === true;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
private canResumeSession(detail: HubSessionDetail | null): boolean {
|
|
1367
|
+
if (!detail) return false;
|
|
1368
|
+
return (
|
|
1369
|
+
detail.mode === 'sandbox' &&
|
|
1370
|
+
detail.bucket === 'paused' &&
|
|
1371
|
+
detail.historyOnly !== true &&
|
|
1372
|
+
detail.runtimeAvailable === false
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
private isArchivedStatus(status: string): boolean {
|
|
1377
|
+
return status === 'archived' || status === 'shutdown' || status === 'stopped';
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
private getStatusTone(status: string): 'success' | 'warning' | 'error' | 'muted' {
|
|
1381
|
+
if (status === 'active' || status === 'running' || status === 'connected') {
|
|
1382
|
+
return 'success';
|
|
1383
|
+
}
|
|
1384
|
+
if (status === 'error' || status === 'failed' || status === 'stopped') {
|
|
1385
|
+
return 'error';
|
|
1386
|
+
}
|
|
1387
|
+
if (status === 'archived' || status === 'shutdown') {
|
|
1388
|
+
return 'muted';
|
|
1389
|
+
}
|
|
1390
|
+
return 'warning';
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
private getConnectionState(input: {
|
|
1394
|
+
sessionId: string;
|
|
1395
|
+
mode: string;
|
|
1396
|
+
status: string;
|
|
1397
|
+
bucket?: 'running' | 'paused' | 'provisioning' | 'history';
|
|
1398
|
+
runtimeAvailable?: boolean;
|
|
1399
|
+
controlAvailable?: boolean;
|
|
1400
|
+
historyOnly?: boolean;
|
|
1401
|
+
}): {
|
|
1402
|
+
label: string;
|
|
1403
|
+
tone: 'success' | 'warning' | 'error' | 'accent' | 'muted';
|
|
1404
|
+
controlAvailable: boolean;
|
|
1405
|
+
} {
|
|
1406
|
+
const isLocalSession = input.mode === 'tui';
|
|
1407
|
+
const isArchived = this.isArchivedStatus(input.status);
|
|
1408
|
+
const bucket = input.bucket ?? (input.historyOnly ? 'history' : 'running');
|
|
1409
|
+
const runtimeAvailable = input.runtimeAvailable !== false;
|
|
1410
|
+
const controlAvailable = input.controlAvailable ?? runtimeAvailable;
|
|
1411
|
+
const historyOnly = input.historyOnly === true || bucket === 'history';
|
|
1412
|
+
const canWake = !isLocalSession && !isArchived && bucket === 'paused' && historyOnly !== true;
|
|
1413
|
+
|
|
1414
|
+
if (historyOnly) return { label: 'History only', tone: 'warning', controlAvailable };
|
|
1415
|
+
if (isLocalSession && bucket === 'provisioning')
|
|
1416
|
+
return { label: 'Starting', tone: 'warning', controlAvailable };
|
|
1417
|
+
if (isLocalSession && !isArchived)
|
|
1418
|
+
return { label: 'View only', tone: 'muted', controlAvailable };
|
|
1419
|
+
if (bucket === 'provisioning')
|
|
1420
|
+
return { label: 'Starting', tone: 'warning', controlAvailable };
|
|
1421
|
+
if (canWake) {
|
|
1422
|
+
return {
|
|
1423
|
+
label: this.resumeBusySessionId === input.sessionId ? 'Waking' : 'Paused',
|
|
1424
|
+
tone: this.resumeBusySessionId === input.sessionId ? 'accent' : 'warning',
|
|
1425
|
+
controlAvailable,
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
if (!isArchived && runtimeAvailable && !controlAvailable) {
|
|
1429
|
+
return { label: 'Read only', tone: 'muted', controlAvailable };
|
|
1430
|
+
}
|
|
1431
|
+
if (!isArchived && runtimeAvailable)
|
|
1432
|
+
return { label: 'Live', tone: 'success', controlAvailable };
|
|
1433
|
+
if (isArchived) return { label: 'Archived', tone: 'muted', controlAvailable };
|
|
1434
|
+
return { label: 'Disconnected', tone: 'warning', controlAvailable };
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
private formatTagSummary(tags: string[] | undefined, maxCount = 4): string | null {
|
|
1438
|
+
if (!tags || tags.length === 0) return null;
|
|
1439
|
+
const visible = tags.slice(0, maxCount);
|
|
1440
|
+
const remainder = tags.length - visible.length;
|
|
1441
|
+
return remainder > 0 ? `${visible.join(', ')} +${remainder}` : visible.join(', ');
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
private getSessionEventEntries(sessionId: string): FeedEntry[] {
|
|
1445
|
+
if (!this.isRuntimeAvailable(sessionId) || this.isHistoryOnly(sessionId)) {
|
|
1446
|
+
return this.sessionHistoryFeed.get(sessionId) ?? [];
|
|
1447
|
+
}
|
|
1448
|
+
return this.sessionFeed.get(sessionId) ?? [];
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
private async refreshTodos(sessionId: string): Promise<void> {
|
|
1452
|
+
const requestId = ++this.detailTodoRequestId;
|
|
1453
|
+
try {
|
|
1454
|
+
const todosResponse = await this.fetchJson<HubTodoListResponse>(
|
|
1455
|
+
`/api/hub/session/${encodeURIComponent(sessionId)}/todos?includeTerminal=true&includeSync=true&limit=30`,
|
|
1456
|
+
15_000
|
|
1457
|
+
).catch((err: any) => {
|
|
1458
|
+
const isAbort = err?.name === 'AbortError' || err?.message?.includes('aborted');
|
|
1459
|
+
return {
|
|
1460
|
+
_fetchError: true,
|
|
1461
|
+
message: isAbort ? 'Todos loading...' : err?.message || 'Failed to load todos',
|
|
1462
|
+
} as HubTodoListResponse & { _fetchError: true };
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
if (
|
|
1466
|
+
this.disposed ||
|
|
1467
|
+
this.detail?.sessionId !== sessionId ||
|
|
1468
|
+
requestId !== this.detailTodoRequestId
|
|
1469
|
+
) {
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
if ((todosResponse as HubTodoListResponse & { _fetchError?: boolean })._fetchError) {
|
|
1474
|
+
if (this.cachedTodos && this.cachedTodos.sessionId === sessionId) {
|
|
1475
|
+
this.detail.todos = this.cachedTodos.todos;
|
|
1476
|
+
this.detail.todoSummary = this.cachedTodos.summary;
|
|
1477
|
+
}
|
|
1478
|
+
this.detail.todosUnavailable = todosResponse.message;
|
|
1479
|
+
this.requestRender();
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
this.detail.todos = Array.isArray(todosResponse.todos) ? todosResponse.todos : [];
|
|
1484
|
+
this.detail.todoSummary =
|
|
1485
|
+
todosResponse.summary && typeof todosResponse.summary === 'object'
|
|
1486
|
+
? todosResponse.summary
|
|
1487
|
+
: undefined;
|
|
1488
|
+
this.detail.todosUnavailable = todosResponse.unavailable
|
|
1489
|
+
? typeof todosResponse.message === 'string'
|
|
1490
|
+
? todosResponse.message
|
|
1491
|
+
: 'Task service unavailable'
|
|
1492
|
+
: undefined;
|
|
1493
|
+
|
|
1494
|
+
if (this.detail.todos.length > 0) {
|
|
1495
|
+
this.cachedTodos = {
|
|
1496
|
+
todos: this.detail.todos,
|
|
1497
|
+
summary: this.detail.todoSummary,
|
|
1498
|
+
sessionId,
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
this.requestRender();
|
|
1503
|
+
} catch {
|
|
1504
|
+
// Best-effort todos loading.
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1037
1508
|
private async refreshList(initial = false): Promise<void> {
|
|
1038
1509
|
if (this.disposed || this.listInFlight) return;
|
|
1039
1510
|
this.listInFlight = true;
|
|
@@ -1078,47 +1549,24 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1078
1549
|
}
|
|
1079
1550
|
|
|
1080
1551
|
try {
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
} as any;
|
|
1092
|
-
}),
|
|
1093
|
-
]);
|
|
1094
|
-
if (todosResponse?._fetchError) {
|
|
1095
|
-
// Use cached todos if available, show loading indicator
|
|
1096
|
-
if (this.cachedTodos && this.cachedTodos.sessionId === sessionId) {
|
|
1097
|
-
detail.todos = this.cachedTodos.todos;
|
|
1098
|
-
detail.todoSummary = this.cachedTodos.summary;
|
|
1099
|
-
detail.todosUnavailable = todosResponse.message;
|
|
1100
|
-
} else {
|
|
1101
|
-
detail.todosUnavailable = todosResponse.message;
|
|
1102
|
-
}
|
|
1103
|
-
} else if (todosResponse) {
|
|
1104
|
-
detail.todos = Array.isArray(todosResponse.todos) ? todosResponse.todos : [];
|
|
1105
|
-
detail.todoSummary =
|
|
1106
|
-
todosResponse.summary && typeof todosResponse.summary === 'object'
|
|
1107
|
-
? todosResponse.summary
|
|
1108
|
-
: undefined;
|
|
1109
|
-
detail.todosUnavailable = todosResponse.unavailable
|
|
1110
|
-
? typeof todosResponse.message === 'string'
|
|
1111
|
-
? todosResponse.message
|
|
1112
|
-
: 'Task service unavailable'
|
|
1113
|
-
: undefined;
|
|
1114
|
-
// Cache successful todo data
|
|
1115
|
-
if (detail.todos && detail.todos.length > 0) {
|
|
1116
|
-
this.cachedTodos = { todos: detail.todos, summary: detail.todoSummary, sessionId };
|
|
1117
|
-
}
|
|
1552
|
+
const detail = await this.fetchJson<HubSessionDetail>(
|
|
1553
|
+
`/api/hub/session/${encodeURIComponent(sessionId)}`
|
|
1554
|
+
);
|
|
1555
|
+
|
|
1556
|
+
if (this.cachedTodos && this.cachedTodos.sessionId === sessionId) {
|
|
1557
|
+
detail.todos = this.cachedTodos.todos;
|
|
1558
|
+
detail.todoSummary = this.cachedTodos.summary;
|
|
1559
|
+
detail.todosUnavailable = 'Loading todos...';
|
|
1560
|
+
} else {
|
|
1561
|
+
detail.todosUnavailable = 'Loading todos...';
|
|
1118
1562
|
}
|
|
1563
|
+
|
|
1119
1564
|
this.detail = detail;
|
|
1120
1565
|
this.detailSessionId = sessionId;
|
|
1121
|
-
this.
|
|
1566
|
+
this.replaceStreamProjection(sessionId, detail.stream, 'snapshot');
|
|
1567
|
+
if (detail.runtimeAvailable !== false && detail.historyOnly !== true) {
|
|
1568
|
+
this.resetHistoricalSessionState(sessionId);
|
|
1569
|
+
}
|
|
1122
1570
|
const taskCount = detail.tasks?.length ?? 0;
|
|
1123
1571
|
if (taskCount === 0) {
|
|
1124
1572
|
this.selectedTaskIndex = 0;
|
|
@@ -1129,6 +1577,11 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1129
1577
|
this.detailError = '';
|
|
1130
1578
|
this.lastUpdatedAt = Date.now();
|
|
1131
1579
|
this.requestRender();
|
|
1580
|
+
void this.refreshTodos(sessionId);
|
|
1581
|
+
if (detail.runtimeAvailable === false || detail.historyOnly === true) {
|
|
1582
|
+
void this.refreshReplay(sessionId);
|
|
1583
|
+
void this.refreshEventHistory(sessionId);
|
|
1584
|
+
}
|
|
1132
1585
|
} catch (err) {
|
|
1133
1586
|
this.loadingDetail = false;
|
|
1134
1587
|
this.detailError = err instanceof Error ? err.message : String(err);
|
|
@@ -1138,6 +1591,193 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1138
1591
|
}
|
|
1139
1592
|
}
|
|
1140
1593
|
|
|
1594
|
+
private async refreshReplay(sessionId: string): Promise<void> {
|
|
1595
|
+
if (this.isFailureCoolingDown(this.replayFailureUntil, sessionId)) return;
|
|
1596
|
+
if (this.replayLoadedSessions.has(sessionId) || this.replayInFlight.has(sessionId)) return;
|
|
1597
|
+
const controller = new AbortController();
|
|
1598
|
+
const generation = this.nextRequestGeneration(this.replayRequestGenerations, sessionId);
|
|
1599
|
+
this.replayInFlight.add(sessionId);
|
|
1600
|
+
this.replayRequestControllers.set(sessionId, controller);
|
|
1601
|
+
try {
|
|
1602
|
+
const data = await this.fetchJson<HubReplayResponse>(
|
|
1603
|
+
`/api/hub/session/${encodeURIComponent(sessionId)}/replay`,
|
|
1604
|
+
15_000,
|
|
1605
|
+
{ signal: controller.signal }
|
|
1606
|
+
);
|
|
1607
|
+
if (
|
|
1608
|
+
!this.isCurrentSessionRequest(
|
|
1609
|
+
this.replayRequestGenerations,
|
|
1610
|
+
this.replayRequestControllers,
|
|
1611
|
+
sessionId,
|
|
1612
|
+
generation,
|
|
1613
|
+
controller
|
|
1614
|
+
)
|
|
1615
|
+
) {
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
this.applyReplayEntries(sessionId, Array.isArray(data.entries) ? data.entries : []);
|
|
1619
|
+
this.replayLoadedSessions.add(sessionId);
|
|
1620
|
+
this.requestRender();
|
|
1621
|
+
} catch (err) {
|
|
1622
|
+
if (
|
|
1623
|
+
controller.signal.aborted ||
|
|
1624
|
+
!this.isCurrentSessionRequest(
|
|
1625
|
+
this.replayRequestGenerations,
|
|
1626
|
+
this.replayRequestControllers,
|
|
1627
|
+
sessionId,
|
|
1628
|
+
generation,
|
|
1629
|
+
controller
|
|
1630
|
+
)
|
|
1631
|
+
) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
this.markFailureCooldown(this.replayFailureUntil, sessionId);
|
|
1635
|
+
this.pushFeed(
|
|
1636
|
+
`${this.getSessionLabel(sessionId)}: replay unavailable (${err instanceof Error ? err.message : String(err)})`,
|
|
1637
|
+
sessionId
|
|
1638
|
+
);
|
|
1639
|
+
this.requestRender();
|
|
1640
|
+
} finally {
|
|
1641
|
+
if (
|
|
1642
|
+
this.isCurrentSessionRequest(
|
|
1643
|
+
this.replayRequestGenerations,
|
|
1644
|
+
this.replayRequestControllers,
|
|
1645
|
+
sessionId,
|
|
1646
|
+
generation,
|
|
1647
|
+
controller
|
|
1648
|
+
)
|
|
1649
|
+
) {
|
|
1650
|
+
this.replayInFlight.delete(sessionId);
|
|
1651
|
+
this.replayRequestControllers.delete(sessionId);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
private formatHistoryEventLine(sessionId: string, event: HubEventHistoryItem): string | null {
|
|
1657
|
+
const payload =
|
|
1658
|
+
event.payload && typeof event.payload === 'object'
|
|
1659
|
+
? (event.payload as Record<string, unknown>)
|
|
1660
|
+
: undefined;
|
|
1661
|
+
return (
|
|
1662
|
+
this.formatEventFeedLine(sessionId, event.event, payload) ??
|
|
1663
|
+
this.formatStreamLine(event.event, payload) ??
|
|
1664
|
+
`${event.event}${event.agent ? ` ${event.agent}` : ''}`
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
private async refreshEventHistory(sessionId: string): Promise<void> {
|
|
1669
|
+
if (this.isFailureCoolingDown(this.eventHistoryFailureUntil, sessionId)) {
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (
|
|
1673
|
+
this.eventHistoryLoadedSessions.has(sessionId) ||
|
|
1674
|
+
this.eventHistoryInFlight.has(sessionId)
|
|
1675
|
+
) {
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
const controller = new AbortController();
|
|
1679
|
+
const generation = this.nextRequestGeneration(this.eventHistoryRequestGenerations, sessionId);
|
|
1680
|
+
this.eventHistoryInFlight.add(sessionId);
|
|
1681
|
+
this.eventHistoryRequestControllers.set(sessionId, controller);
|
|
1682
|
+
try {
|
|
1683
|
+
const data = await this.fetchJson<HubEventHistoryResponse>(
|
|
1684
|
+
`/api/hub/session/${encodeURIComponent(sessionId)}/events/history?limit=100`,
|
|
1685
|
+
15_000,
|
|
1686
|
+
{ signal: controller.signal }
|
|
1687
|
+
);
|
|
1688
|
+
if (
|
|
1689
|
+
!this.isCurrentSessionRequest(
|
|
1690
|
+
this.eventHistoryRequestGenerations,
|
|
1691
|
+
this.eventHistoryRequestControllers,
|
|
1692
|
+
sessionId,
|
|
1693
|
+
generation,
|
|
1694
|
+
controller
|
|
1695
|
+
)
|
|
1696
|
+
) {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
const entries = (data.events ?? [])
|
|
1700
|
+
.map((event): FeedEntry | null => {
|
|
1701
|
+
const text = this.formatHistoryEventLine(sessionId, event);
|
|
1702
|
+
if (!text) return null;
|
|
1703
|
+
return {
|
|
1704
|
+
at: Number.isNaN(Date.parse(event.occurredAt))
|
|
1705
|
+
? Date.now()
|
|
1706
|
+
: Date.parse(event.occurredAt),
|
|
1707
|
+
sessionId,
|
|
1708
|
+
text,
|
|
1709
|
+
};
|
|
1710
|
+
})
|
|
1711
|
+
.filter((entry): entry is FeedEntry => entry !== null)
|
|
1712
|
+
.sort((a, b) => b.at - a.at);
|
|
1713
|
+
this.sessionHistoryFeed.set(sessionId, entries);
|
|
1714
|
+
this.clearEventHistoryFailureCooldown(sessionId);
|
|
1715
|
+
this.eventHistoryLoadedSessions.add(sessionId);
|
|
1716
|
+
this.requestRender();
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
if (
|
|
1719
|
+
controller.signal.aborted ||
|
|
1720
|
+
!this.isCurrentSessionRequest(
|
|
1721
|
+
this.eventHistoryRequestGenerations,
|
|
1722
|
+
this.eventHistoryRequestControllers,
|
|
1723
|
+
sessionId,
|
|
1724
|
+
generation,
|
|
1725
|
+
controller
|
|
1726
|
+
)
|
|
1727
|
+
) {
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
this.markFailureCooldown(this.eventHistoryFailureUntil, sessionId);
|
|
1731
|
+
this.pushFeed(
|
|
1732
|
+
`${this.getSessionLabel(sessionId)}: event history unavailable (${err instanceof Error ? err.message : String(err)})`,
|
|
1733
|
+
sessionId
|
|
1734
|
+
);
|
|
1735
|
+
this.requestRender();
|
|
1736
|
+
} finally {
|
|
1737
|
+
if (
|
|
1738
|
+
this.isCurrentSessionRequest(
|
|
1739
|
+
this.eventHistoryRequestGenerations,
|
|
1740
|
+
this.eventHistoryRequestControllers,
|
|
1741
|
+
sessionId,
|
|
1742
|
+
generation,
|
|
1743
|
+
controller
|
|
1744
|
+
)
|
|
1745
|
+
) {
|
|
1746
|
+
this.eventHistoryInFlight.delete(sessionId);
|
|
1747
|
+
this.eventHistoryRequestControllers.delete(sessionId);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
private async resumeSession(sessionId: string): Promise<void> {
|
|
1753
|
+
if (this.resumeBusySessionId) return;
|
|
1754
|
+
this.resumeBusySessionId = sessionId;
|
|
1755
|
+
this.pushFeed(`${this.getSessionLabel(sessionId)}: resume requested`, sessionId);
|
|
1756
|
+
this.requestRender();
|
|
1757
|
+
|
|
1758
|
+
try {
|
|
1759
|
+
const response = await this.fetchJson<{ resumed?: boolean; error?: string }>(
|
|
1760
|
+
`/api/hub/session/${encodeURIComponent(sessionId)}/resume`,
|
|
1761
|
+
15_000,
|
|
1762
|
+
{ method: 'POST' }
|
|
1763
|
+
);
|
|
1764
|
+
if (response.resumed !== true) {
|
|
1765
|
+
throw new Error(response.error || 'Hub declined to resume the session');
|
|
1766
|
+
}
|
|
1767
|
+
this.resetHistoricalSessionState(sessionId);
|
|
1768
|
+
await this.refreshList();
|
|
1769
|
+
await this.refreshDetail(sessionId);
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1772
|
+
this.detailError = message;
|
|
1773
|
+
this.pushFeed(`${this.getSessionLabel(sessionId)}: resume failed (${message})`, sessionId);
|
|
1774
|
+
this.requestRender();
|
|
1775
|
+
} finally {
|
|
1776
|
+
this.resumeBusySessionId = null;
|
|
1777
|
+
this.requestRender();
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1141
1781
|
private updateFeedFromList(sessions: HubSessionSummary[]): void {
|
|
1142
1782
|
if (this.previousDigests.size === 0) {
|
|
1143
1783
|
if (sessions.length > 0) {
|
|
@@ -1209,9 +1849,15 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1209
1849
|
if (this.disposed) return;
|
|
1210
1850
|
const desiredModes = new Map<string, 'summary' | 'full'>();
|
|
1211
1851
|
for (const session of sessions.slice(0, STREAM_SESSION_LIMIT)) {
|
|
1852
|
+
if (session.runtimeAvailable === false || session.historyOnly === true) continue;
|
|
1212
1853
|
desiredModes.set(session.sessionId, 'summary');
|
|
1213
1854
|
}
|
|
1214
|
-
if (
|
|
1855
|
+
if (
|
|
1856
|
+
this.detailSessionId &&
|
|
1857
|
+
this.isSessionContext() &&
|
|
1858
|
+
this.isRuntimeAvailable(this.detailSessionId) &&
|
|
1859
|
+
!this.isHistoryOnly(this.detailSessionId)
|
|
1860
|
+
) {
|
|
1215
1861
|
desiredModes.set(this.detailSessionId, 'full');
|
|
1216
1862
|
}
|
|
1217
1863
|
|
|
@@ -1395,50 +2041,22 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1395
2041
|
}
|
|
1396
2042
|
|
|
1397
2043
|
private applyHydration(sessionId: string, eventData: unknown): void {
|
|
1398
|
-
if (this.hydratedSessions.has(sessionId)) return;
|
|
1399
2044
|
if (!eventData || typeof eventData !== 'object') return;
|
|
1400
2045
|
const payload = eventData as Record<string, unknown>;
|
|
1401
|
-
const loadedFromProjection = this.
|
|
2046
|
+
const loadedFromProjection = this.replaceStreamProjection(
|
|
2047
|
+
sessionId,
|
|
2048
|
+
payload.stream as StreamProjection | undefined,
|
|
2049
|
+
'hydration'
|
|
2050
|
+
);
|
|
1402
2051
|
|
|
1403
2052
|
if (!loadedFromProjection) {
|
|
1404
|
-
const entries = Array.isArray(payload.entries)
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
const taskId = typeof entry.taskId === 'string' ? entry.taskId : undefined;
|
|
1411
|
-
|
|
1412
|
-
if (!content) continue;
|
|
1413
|
-
if (type === 'message' || type === 'task_result') {
|
|
1414
|
-
this.appendBufferText(sessionId, 'output', content + '\n\n', taskId);
|
|
1415
|
-
} else if (type === 'thinking') {
|
|
1416
|
-
this.appendBufferText(sessionId, 'thinking', content + '\n\n', taskId);
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
this.hydratedSessions.add(sessionId);
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
private applyStreamProjection(sessionId: string, streamRaw: unknown): boolean {
|
|
1425
|
-
if (!streamRaw || typeof streamRaw !== 'object') return false;
|
|
1426
|
-
const stream = streamRaw as Record<string, unknown>;
|
|
1427
|
-
const sessionBuffer = this.getSessionBuffer(sessionId);
|
|
1428
|
-
sessionBuffer.output = typeof stream.output === 'string' ? stream.output : '';
|
|
1429
|
-
sessionBuffer.thinking = typeof stream.thinking === 'string' ? stream.thinking : '';
|
|
1430
|
-
const taskStreams =
|
|
1431
|
-
stream.tasks && typeof stream.tasks === 'object'
|
|
1432
|
-
? (stream.tasks as Record<string, unknown>)
|
|
1433
|
-
: {};
|
|
1434
|
-
for (const [taskId, rawBlock] of Object.entries(taskStreams)) {
|
|
1435
|
-
if (!rawBlock || typeof rawBlock !== 'object') continue;
|
|
1436
|
-
const block = rawBlock as Record<string, unknown>;
|
|
1437
|
-
const taskBuffer = this.getTaskBuffer(sessionId, taskId);
|
|
1438
|
-
taskBuffer.output = typeof block.output === 'string' ? block.output : '';
|
|
1439
|
-
taskBuffer.thinking = typeof block.thinking === 'string' ? block.thinking : '';
|
|
2053
|
+
const entries = Array.isArray(payload.entries)
|
|
2054
|
+
? payload.entries.filter(
|
|
2055
|
+
(entry): entry is ConversationEntryLike => !!entry && typeof entry === 'object'
|
|
2056
|
+
)
|
|
2057
|
+
: [];
|
|
2058
|
+
this.replaceStreamProjection(sessionId, buildProjectionFromEntries(entries), 'hydration');
|
|
1440
2059
|
}
|
|
1441
|
-
return true;
|
|
1442
2060
|
}
|
|
1443
2061
|
|
|
1444
2062
|
private captureStreamContent(
|
|
@@ -1570,7 +2188,7 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1570
2188
|
|
|
1571
2189
|
private getFeedEntries(scope: 'global' | 'session'): FeedEntry[] {
|
|
1572
2190
|
if (scope === 'session' && this.detailSessionId) {
|
|
1573
|
-
return this.
|
|
2191
|
+
return this.getSessionEventEntries(this.detailSessionId);
|
|
1574
2192
|
}
|
|
1575
2193
|
return this.feed;
|
|
1576
2194
|
}
|
|
@@ -1777,9 +2395,30 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1777
2395
|
const updated = this.lastUpdatedAt
|
|
1778
2396
|
? `${formatClock(this.lastUpdatedAt)} updated`
|
|
1779
2397
|
: 'not updated';
|
|
2398
|
+
const liveCount = this.sessions.filter((session) => {
|
|
2399
|
+
const connection = this.getConnectionState({
|
|
2400
|
+
sessionId: session.sessionId,
|
|
2401
|
+
mode: session.mode,
|
|
2402
|
+
status: session.status,
|
|
2403
|
+
bucket: session.bucket,
|
|
2404
|
+
runtimeAvailable: session.runtimeAvailable,
|
|
2405
|
+
controlAvailable: session.controlAvailable,
|
|
2406
|
+
historyOnly: session.historyOnly,
|
|
2407
|
+
});
|
|
2408
|
+
return connection.label === 'Live';
|
|
2409
|
+
}).length;
|
|
2410
|
+
const pausedCount = this.sessions.filter(
|
|
2411
|
+
(session) => session.bucket === 'paused' && session.historyOnly !== true
|
|
2412
|
+
).length;
|
|
2413
|
+
const historyCount = this.sessions.filter(
|
|
2414
|
+
(session) => session.historyOnly === true || session.bucket === 'history'
|
|
2415
|
+
).length;
|
|
1780
2416
|
body.push(
|
|
1781
2417
|
this.contentLine(
|
|
1782
|
-
this.theme.fg(
|
|
2418
|
+
this.theme.fg(
|
|
2419
|
+
'muted',
|
|
2420
|
+
` Active: ${this.sessions.length} sessions Live:${liveCount} Paused:${pausedCount} History:${historyCount} ${updated}`
|
|
2421
|
+
),
|
|
1783
2422
|
inner
|
|
1784
2423
|
)
|
|
1785
2424
|
);
|
|
@@ -1803,27 +2442,34 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1803
2442
|
const marker = selected ? this.theme.fg('accent', '›') : ' ';
|
|
1804
2443
|
const label = session.label || shortId(session.sessionId);
|
|
1805
2444
|
const name = selected ? this.theme.bold(label) : label;
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
session.
|
|
1815
|
-
);
|
|
2445
|
+
const status = this.theme.fg(this.getStatusTone(session.status), session.status);
|
|
2446
|
+
const connection = this.getConnectionState({
|
|
2447
|
+
sessionId: session.sessionId,
|
|
2448
|
+
mode: session.mode,
|
|
2449
|
+
status: session.status,
|
|
2450
|
+
bucket: session.bucket,
|
|
2451
|
+
runtimeAvailable: session.runtimeAvailable,
|
|
2452
|
+
controlAvailable: session.controlAvailable,
|
|
2453
|
+
historyOnly: session.historyOnly,
|
|
2454
|
+
});
|
|
2455
|
+
const connectionLabel = this.theme.fg(connection.tone, connection.label);
|
|
1816
2456
|
const self =
|
|
1817
2457
|
this.currentSessionId === session.sessionId
|
|
1818
2458
|
? this.theme.fg('accent', ' (this)')
|
|
1819
2459
|
: '';
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1822
|
-
`obs:${session.observerCount}
|
|
1823
|
-
|
|
2460
|
+
const tagSummary = this.formatTagSummary(session.tags, 2);
|
|
2461
|
+
const metricsParts = [
|
|
2462
|
+
`obs:${session.observerCount}`,
|
|
2463
|
+
`agents:${session.subAgentCount}`,
|
|
2464
|
+
`tasks:${session.taskCount}`,
|
|
2465
|
+
formatRelative(session.createdAt),
|
|
2466
|
+
session.defaultAgent ? `agent:${session.defaultAgent}` : undefined,
|
|
2467
|
+
tagSummary ? `tags:${tagSummary}` : undefined,
|
|
2468
|
+
].filter((part): part is string => !!part);
|
|
2469
|
+
const metrics = this.theme.fg('muted', metricsParts.join(' '));
|
|
1824
2470
|
body.push(
|
|
1825
2471
|
this.contentLine(
|
|
1826
|
-
` ${marker} ${name}${self} ${status} ${session.mode} ${metrics}`,
|
|
2472
|
+
` ${marker} ${name}${self} ${status} ${session.mode} ${connectionLabel} ${metrics}`,
|
|
1827
2473
|
inner
|
|
1828
2474
|
)
|
|
1829
2475
|
);
|
|
@@ -1857,12 +2503,14 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1857
2503
|
private renderDetailScreen(width: number, maxLines: number): string[] {
|
|
1858
2504
|
const inner = Math.max(0, width - 2);
|
|
1859
2505
|
const lines: string[] = [];
|
|
1860
|
-
const title =
|
|
2506
|
+
const title =
|
|
2507
|
+
this.detail?.label ||
|
|
2508
|
+
(this.detailSessionId ? shortId(this.detailSessionId) : 'Hub Session');
|
|
1861
2509
|
const headerRows = 3;
|
|
1862
2510
|
const footerRows = 2;
|
|
1863
2511
|
const contentBudget = Math.max(5, maxLines - headerRows - footerRows);
|
|
1864
2512
|
|
|
1865
|
-
lines.push(buildTopBorder(width, `Session ${
|
|
2513
|
+
lines.push(buildTopBorder(width, `Session ${title}`));
|
|
1866
2514
|
lines.push(this.contentLine('', inner));
|
|
1867
2515
|
lines.push(this.contentLine(this.buildTopTabs('detail', true), inner));
|
|
1868
2516
|
|
|
@@ -1878,12 +2526,41 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1878
2526
|
const participants = session.participants ?? [];
|
|
1879
2527
|
const tasks = session.tasks ?? [];
|
|
1880
2528
|
const activityEntries = Object.entries(session.agentActivity ?? {});
|
|
2529
|
+
const connection = this.getConnectionState({
|
|
2530
|
+
sessionId: session.sessionId,
|
|
2531
|
+
mode: session.mode,
|
|
2532
|
+
status: session.status,
|
|
2533
|
+
bucket: session.bucket,
|
|
2534
|
+
runtimeAvailable: session.runtimeAvailable,
|
|
2535
|
+
controlAvailable: session.controlAvailable,
|
|
2536
|
+
historyOnly: session.historyOnly,
|
|
2537
|
+
});
|
|
2538
|
+
const inactiveTasks = session.diagnostics?.inactiveRunningTasks ?? [];
|
|
2539
|
+
const inactiveTaskById = new Map(
|
|
2540
|
+
inactiveTasks.map((item) => [item.taskId, item] as const)
|
|
2541
|
+
);
|
|
2542
|
+
const pushWrappedLine = (
|
|
2543
|
+
text: string,
|
|
2544
|
+
tone: 'muted' | 'warning' | 'error' | 'dim' = 'muted'
|
|
2545
|
+
): void => {
|
|
2546
|
+
for (const wrapped of wrapText(text, Math.max(12, inner - 4))) {
|
|
2547
|
+
body.push(this.contentLine(this.theme.fg(tone, ` ${wrapped}`), inner));
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
1881
2550
|
|
|
1882
2551
|
body.push(this.contentLine(this.theme.bold(' Overview'), inner));
|
|
1883
2552
|
body.push(this.contentLine(this.theme.fg('muted', ` ID: ${session.sessionId}`), inner));
|
|
1884
2553
|
body.push(
|
|
1885
2554
|
this.contentLine(
|
|
1886
|
-
this.theme.fg(
|
|
2555
|
+
` Status: ${this.theme.fg(this.getStatusTone(session.status), session.status)} Mode: ${session.mode} Bucket: ${session.bucket ?? '-'}`,
|
|
2556
|
+
inner
|
|
2557
|
+
)
|
|
2558
|
+
);
|
|
2559
|
+
body.push(
|
|
2560
|
+
this.contentLine(
|
|
2561
|
+
` Connection: ${this.theme.fg(connection.tone, connection.label)} Runtime: ${
|
|
2562
|
+
session.runtimeAvailable === false ? 'offline' : 'ready'
|
|
2563
|
+
} Control: ${connection.controlAvailable ? 'enabled' : 'read-only'}`,
|
|
1887
2564
|
inner
|
|
1888
2565
|
)
|
|
1889
2566
|
);
|
|
@@ -1902,6 +2579,26 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1902
2579
|
inner
|
|
1903
2580
|
)
|
|
1904
2581
|
);
|
|
2582
|
+
if (session.defaultAgent) {
|
|
2583
|
+
body.push(
|
|
2584
|
+
this.contentLine(
|
|
2585
|
+
this.theme.fg('muted', ` Default agent: ${session.defaultAgent}`),
|
|
2586
|
+
inner
|
|
2587
|
+
)
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
const tagSummary = this.formatTagSummary(session.tags);
|
|
2591
|
+
if (tagSummary) {
|
|
2592
|
+
pushWrappedLine(`Tags: ${tagSummary}`);
|
|
2593
|
+
}
|
|
2594
|
+
if (session.streamId || session.streamUrl) {
|
|
2595
|
+
const streamState = session.streamId
|
|
2596
|
+
? shortId(session.streamId)
|
|
2597
|
+
: session.streamUrl
|
|
2598
|
+
? 'attached'
|
|
2599
|
+
: 'none';
|
|
2600
|
+
body.push(this.contentLine(this.theme.fg('muted', ` Stream: ${streamState}`), inner));
|
|
2601
|
+
}
|
|
1905
2602
|
if (session.context?.branch) {
|
|
1906
2603
|
body.push(
|
|
1907
2604
|
this.contentLine(
|
|
@@ -1918,6 +2615,32 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1918
2615
|
)
|
|
1919
2616
|
);
|
|
1920
2617
|
}
|
|
2618
|
+
if (session.task) {
|
|
2619
|
+
body.push(
|
|
2620
|
+
this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner)
|
|
2621
|
+
);
|
|
2622
|
+
body.push(this.contentLine(this.theme.bold(' Root Task'), inner));
|
|
2623
|
+
for (const wrapped of wrapText(session.task, Math.max(12, inner - 4))) {
|
|
2624
|
+
body.push(this.contentLine(` ${wrapped}`, inner));
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
if (session.error) {
|
|
2628
|
+
body.push(
|
|
2629
|
+
this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner)
|
|
2630
|
+
);
|
|
2631
|
+
body.push(this.contentLine(this.theme.bold(' Error'), inner));
|
|
2632
|
+
pushWrappedLine(session.error, 'error');
|
|
2633
|
+
}
|
|
2634
|
+
if (this.canResumeSession(session)) {
|
|
2635
|
+
const resumeMessage =
|
|
2636
|
+
this.resumeBusySessionId === session.sessionId
|
|
2637
|
+
? 'Waking sandbox session...'
|
|
2638
|
+
: 'Press [w] to wake this paused sandbox session.';
|
|
2639
|
+
pushWrappedLine(
|
|
2640
|
+
resumeMessage,
|
|
2641
|
+
this.resumeBusySessionId === session.sessionId ? 'muted' : 'warning'
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
1921
2644
|
body.push(
|
|
1922
2645
|
this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner)
|
|
1923
2646
|
);
|
|
@@ -1942,19 +2665,28 @@ export class HubOverlay implements Component, Focusable {
|
|
|
1942
2665
|
? 'success'
|
|
1943
2666
|
: task.status === 'failed'
|
|
1944
2667
|
? 'error'
|
|
1945
|
-
: '
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
);
|
|
2668
|
+
: task.status === 'running'
|
|
2669
|
+
? 'accent'
|
|
2670
|
+
: 'warning';
|
|
2671
|
+
const status = this.theme.fg(statusColor, task.status);
|
|
2672
|
+
const inactive = inactiveTaskById.get(task.taskId);
|
|
1950
2673
|
const prompt = task.prompt
|
|
1951
2674
|
? truncateToWidth(toSingleLine(task.prompt), Math.max(16, inner - 34))
|
|
1952
2675
|
: '';
|
|
1953
|
-
const duration =
|
|
2676
|
+
const duration =
|
|
2677
|
+
typeof task.duration === 'number'
|
|
2678
|
+
? ` ${formatElapsedCompact(task.duration)}`
|
|
2679
|
+
: '';
|
|
2680
|
+
const idle = inactive
|
|
2681
|
+
? ` ${this.theme.fg(
|
|
2682
|
+
'warning',
|
|
2683
|
+
`idle ${formatElapsedCompact(inactive.inactivityMs)}`
|
|
2684
|
+
)}`
|
|
2685
|
+
: '';
|
|
1954
2686
|
const marker = selected ? this.theme.fg('accent', '›') : ' ';
|
|
1955
2687
|
body.push(
|
|
1956
2688
|
this.contentLine(
|
|
1957
|
-
`${marker} ${shortId(task.taskId).padEnd(12)} ${task.agent.padEnd(9)} ${status}${duration} ${prompt}`,
|
|
2689
|
+
`${marker} ${shortId(task.taskId).padEnd(12)} ${task.agent.padEnd(9)} ${status}${duration}${idle} ${prompt}`,
|
|
1958
2690
|
inner
|
|
1959
2691
|
)
|
|
1960
2692
|
);
|
|
@@ -2058,6 +2790,36 @@ export class HubOverlay implements Component, Focusable {
|
|
|
2058
2790
|
}
|
|
2059
2791
|
}
|
|
2060
2792
|
|
|
2793
|
+
if (inactiveTasks.length > 0) {
|
|
2794
|
+
body.push(
|
|
2795
|
+
this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner)
|
|
2796
|
+
);
|
|
2797
|
+
body.push(this.contentLine(this.theme.bold(' Diagnostics'), inner));
|
|
2798
|
+
body.push(
|
|
2799
|
+
this.contentLine(
|
|
2800
|
+
this.theme.fg(
|
|
2801
|
+
'warning',
|
|
2802
|
+
` ${inactiveTasks.length} running task${inactiveTasks.length === 1 ? '' : 's'} without recent activity`
|
|
2803
|
+
),
|
|
2804
|
+
inner
|
|
2805
|
+
)
|
|
2806
|
+
);
|
|
2807
|
+
for (const item of inactiveTasks.slice(0, 8)) {
|
|
2808
|
+
const lastSeen = item.lastActivityAt
|
|
2809
|
+
? ` last ${formatRelative(item.lastActivityAt)}`
|
|
2810
|
+
: '';
|
|
2811
|
+
body.push(
|
|
2812
|
+
this.contentLine(
|
|
2813
|
+
this.theme.fg(
|
|
2814
|
+
'warning',
|
|
2815
|
+
` ${shortId(item.taskId).padEnd(12)} ${item.agent.padEnd(10)} idle ${formatElapsedCompact(item.inactivityMs)} started ${formatRelative(item.startedAt)}${lastSeen}`
|
|
2816
|
+
),
|
|
2817
|
+
inner
|
|
2818
|
+
)
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2061
2823
|
body.push(
|
|
2062
2824
|
this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner)
|
|
2063
2825
|
);
|
|
@@ -2066,13 +2828,43 @@ export class HubOverlay implements Component, Focusable {
|
|
|
2066
2828
|
body.push(this.contentLine(this.theme.fg('dim', ' (none)'), inner));
|
|
2067
2829
|
} else {
|
|
2068
2830
|
for (const [agent, info] of activityEntries.slice(0, 15)) {
|
|
2069
|
-
const
|
|
2831
|
+
const status = info.status || 'idle';
|
|
2832
|
+
const statusTone =
|
|
2833
|
+
status === 'completed'
|
|
2834
|
+
? 'success'
|
|
2835
|
+
: status === 'failed'
|
|
2836
|
+
? 'error'
|
|
2837
|
+
: status === 'tool_start' || status === 'running'
|
|
2838
|
+
? 'accent'
|
|
2839
|
+
: 'warning';
|
|
2840
|
+
const tool = info.currentTool ? ` tool:${info.currentTool}` : '';
|
|
2070
2841
|
const calls =
|
|
2071
|
-
typeof info.toolCallCount === 'number'
|
|
2072
|
-
|
|
2842
|
+
typeof info.toolCallCount === 'number' ? ` calls:${info.toolCallCount}` : '';
|
|
2843
|
+
const lastSeen =
|
|
2844
|
+
info.lastActivity !== undefined && info.lastActivity !== null
|
|
2845
|
+
? ` last:${formatRelative(info.lastActivity)}`
|
|
2073
2846
|
: '';
|
|
2074
|
-
const
|
|
2075
|
-
|
|
2847
|
+
const totalElapsed =
|
|
2848
|
+
typeof info.totalElapsed === 'number'
|
|
2849
|
+
? ` total:${formatElapsedCompact(info.totalElapsed)}`
|
|
2850
|
+
: '';
|
|
2851
|
+
body.push(
|
|
2852
|
+
this.contentLine(
|
|
2853
|
+
` ${agent.padEnd(12)} ${this.theme.fg(statusTone, status)}${this.theme.fg(
|
|
2854
|
+
'dim',
|
|
2855
|
+
`${tool}${calls}${lastSeen}${totalElapsed}`
|
|
2856
|
+
)}`,
|
|
2857
|
+
inner
|
|
2858
|
+
)
|
|
2859
|
+
);
|
|
2860
|
+
if (info.currentToolArgs) {
|
|
2861
|
+
for (const wrapped of wrapText(
|
|
2862
|
+
toSingleLine(info.currentToolArgs),
|
|
2863
|
+
Math.max(12, inner - 6)
|
|
2864
|
+
)) {
|
|
2865
|
+
body.push(this.contentLine(this.theme.fg('dim', ` ${wrapped}`), inner));
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2076
2868
|
}
|
|
2077
2869
|
}
|
|
2078
2870
|
}
|
|
@@ -2094,9 +2886,10 @@ export class HubOverlay implements Component, Focusable {
|
|
|
2094
2886
|
this.detailMaxScroll > 0
|
|
2095
2887
|
? this.theme.fg('dim', ` scroll ${this.detailScrollOffset}/${this.detailMaxScroll}`)
|
|
2096
2888
|
: this.theme.fg('dim', ' scroll 0/0');
|
|
2889
|
+
const wakeHint = this.canResumeSession(this.detail) ? ' [w] Wake' : '';
|
|
2097
2890
|
lines.push(
|
|
2098
2891
|
this.contentLine(
|
|
2099
|
-
`${scrollInfo} ${this.theme.fg('dim',
|
|
2892
|
+
`${scrollInfo} ${this.theme.fg('dim', `[↑↓] Task [j/k] Scroll [Enter] Open${wakeHint} [r] Refresh [Esc] Back`)}`,
|
|
2100
2893
|
inner
|
|
2101
2894
|
)
|
|
2102
2895
|
);
|
|
@@ -2111,12 +2904,15 @@ export class HubOverlay implements Component, Focusable {
|
|
|
2111
2904
|
const footerRows = 2;
|
|
2112
2905
|
const contentBudget = Math.max(5, maxLines - headerRows - footerRows);
|
|
2113
2906
|
const scoped = this.feedScope === 'session' && !!this.detailSessionId;
|
|
2907
|
+
const sessionId = scoped ? this.detailSessionId! : null;
|
|
2908
|
+
const historySession = sessionId
|
|
2909
|
+
? !this.isRuntimeAvailable(sessionId) || this.isHistoryOnly(sessionId)
|
|
2910
|
+
: false;
|
|
2114
2911
|
const title = scoped
|
|
2115
|
-
? `${this.feedViewMode === 'events' ? 'Session Events' : 'Session Feed'} ${
|
|
2912
|
+
? `${this.feedViewMode === 'events' ? 'Session Events' : 'Session Feed'} ${this.getSessionLabel(sessionId!)}`
|
|
2116
2913
|
: 'Global Feed';
|
|
2117
2914
|
const entryBudget = Math.max(1, contentBudget - 2);
|
|
2118
|
-
const sessionBuffer =
|
|
2119
|
-
scoped && this.detailSessionId ? this.sessionBuffers.get(this.detailSessionId) : undefined;
|
|
2915
|
+
const sessionBuffer = sessionId ? this.sessionBuffers.get(sessionId) : undefined;
|
|
2120
2916
|
|
|
2121
2917
|
lines.push(buildTopBorder(width, title));
|
|
2122
2918
|
lines.push(this.contentLine('', inner));
|
|
@@ -2135,8 +2931,12 @@ export class HubOverlay implements Component, Focusable {
|
|
|
2135
2931
|
'muted',
|
|
2136
2932
|
scoped
|
|
2137
2933
|
? this.feedViewMode === 'stream'
|
|
2138
|
-
?
|
|
2139
|
-
|
|
2934
|
+
? historySession
|
|
2935
|
+
? ' Rehydrated session output from stored replay — [v] task stream'
|
|
2936
|
+
: ' Live rendered session output (sub-agent style) — [v] task stream'
|
|
2937
|
+
: historySession
|
|
2938
|
+
? ' Stored session events from history'
|
|
2939
|
+
: ' Live full session events'
|
|
2140
2940
|
: ' Streaming event summaries across all sessions'
|
|
2141
2941
|
),
|
|
2142
2942
|
inner
|
|
@@ -2155,7 +2955,13 @@ export class HubOverlay implements Component, Focusable {
|
|
|
2155
2955
|
Math.max(12, inner - 4)
|
|
2156
2956
|
);
|
|
2157
2957
|
if (contentLines.length === 0) {
|
|
2158
|
-
|
|
2958
|
+
if (sessionId && historySession && this.replayInFlight.has(sessionId)) {
|
|
2959
|
+
contentLines = [this.theme.fg('dim', '(loading replay...)')];
|
|
2960
|
+
} else if (sessionId && historySession && this.replayLoadedSessions.has(sessionId)) {
|
|
2961
|
+
contentLines = [this.theme.fg('dim', '(no replay output available)')];
|
|
2962
|
+
} else {
|
|
2963
|
+
contentLines = [this.theme.fg('dim', '(no streamed output yet)')];
|
|
2964
|
+
}
|
|
2159
2965
|
}
|
|
2160
2966
|
} else {
|
|
2161
2967
|
const entries = this.getFeedEntries(scoped ? 'session' : 'global');
|
|
@@ -2163,7 +2969,22 @@ export class HubOverlay implements Component, Focusable {
|
|
|
2163
2969
|
.reverse()
|
|
2164
2970
|
.map((entry) => `${this.theme.fg('dim', formatClock(entry.at))} ${entry.text}`);
|
|
2165
2971
|
if (contentLines.length === 0) {
|
|
2166
|
-
|
|
2972
|
+
if (sessionId && historySession && this.eventHistoryInFlight.has(sessionId)) {
|
|
2973
|
+
contentLines = [this.theme.fg('dim', '(loading event history...)')];
|
|
2974
|
+
} else if (
|
|
2975
|
+
sessionId &&
|
|
2976
|
+
historySession &&
|
|
2977
|
+
this.eventHistoryLoadedSessions.has(sessionId)
|
|
2978
|
+
) {
|
|
2979
|
+
contentLines = [this.theme.fg('dim', '(no stored events available)')];
|
|
2980
|
+
} else {
|
|
2981
|
+
contentLines = [
|
|
2982
|
+
this.theme.fg(
|
|
2983
|
+
'dim',
|
|
2984
|
+
scoped ? '(no session feed items yet)' : '(no feed items yet)'
|
|
2985
|
+
),
|
|
2986
|
+
];
|
|
2987
|
+
}
|
|
2167
2988
|
}
|
|
2168
2989
|
}
|
|
2169
2990
|
|
|
@@ -2223,6 +3044,10 @@ export class HubOverlay implements Component, Focusable {
|
|
|
2223
3044
|
if (!selected) {
|
|
2224
3045
|
body.push(this.contentLine(this.theme.fg('dim', ' No task selected'), inner));
|
|
2225
3046
|
} else {
|
|
3047
|
+
const historySession = this.detailSessionId
|
|
3048
|
+
? !this.isRuntimeAvailable(this.detailSessionId) ||
|
|
3049
|
+
this.isHistoryOnly(this.detailSessionId)
|
|
3050
|
+
: false;
|
|
2226
3051
|
body.push(this.contentLine(this.theme.bold(' Task Overview'), inner));
|
|
2227
3052
|
body.push(
|
|
2228
3053
|
this.contentLine(this.theme.fg('muted', ` Task ID: ${selected.taskId}`), inner)
|
|
@@ -2231,7 +3056,10 @@ export class HubOverlay implements Component, Focusable {
|
|
|
2231
3056
|
body.push(this.contentLine(this.theme.fg('muted', ` Status: ${selected.status}`), inner));
|
|
2232
3057
|
if (typeof selected.duration === 'number') {
|
|
2233
3058
|
body.push(
|
|
2234
|
-
this.contentLine(
|
|
3059
|
+
this.contentLine(
|
|
3060
|
+
this.theme.fg('muted', ` Duration: ${formatElapsedCompact(selected.duration)}`),
|
|
3061
|
+
inner
|
|
3062
|
+
)
|
|
2235
3063
|
);
|
|
2236
3064
|
}
|
|
2237
3065
|
if (selected.startedAt) {
|
|
@@ -2275,7 +3103,15 @@ export class HubOverlay implements Component, Focusable {
|
|
|
2275
3103
|
Math.max(12, inner - 4)
|
|
2276
3104
|
);
|
|
2277
3105
|
if (rendered.length === 0) {
|
|
2278
|
-
|
|
3106
|
+
if (
|
|
3107
|
+
historySession &&
|
|
3108
|
+
this.detailSessionId &&
|
|
3109
|
+
this.replayInFlight.has(this.detailSessionId)
|
|
3110
|
+
) {
|
|
3111
|
+
body.push(this.contentLine(this.theme.fg('dim', ' (loading replay...)'), inner));
|
|
3112
|
+
} else {
|
|
3113
|
+
body.push(this.contentLine(this.theme.fg('dim', ' (no task output yet)'), inner));
|
|
3114
|
+
}
|
|
2279
3115
|
} else {
|
|
2280
3116
|
for (const line of rendered) {
|
|
2281
3117
|
body.push(this.contentLine(` ${line}`, inner));
|