@agentuity/coder 1.0.40 → 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/protocol.d.ts +209 -37
- 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 +78 -6
- package/dist/remote-tui.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +5 -3
- package/src/hub-overlay-state.ts +117 -0
- package/src/hub-overlay.ts +974 -138
- package/src/protocol.ts +269 -55
- package/src/remote-lifecycle.ts +270 -0
- package/src/remote-session.ts +293 -38
- package/src/remote-tui.ts +103 -8
package/dist/hub-overlay.js
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import { getMarkdownTheme } from '@mariozechner/pi-coding-agent';
|
|
2
2
|
import { matchesKey, Markdown as MdComponent } from '@mariozechner/pi-tui';
|
|
3
|
+
import { buildProjectionFromEntries, normalizeStreamProjection, shouldReplaceStreamProjection, } from "./hub-overlay-state.js";
|
|
3
4
|
import { truncateToWidth } from "./renderers.js";
|
|
4
5
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
5
6
|
const POLL_MS = 4_000;
|
|
6
7
|
const REQUEST_TIMEOUT_MS = 5_000;
|
|
8
|
+
const HISTORY_FETCH_FAILURE_COOLDOWN_MS = 15_000;
|
|
7
9
|
const MAX_FEED_ITEMS = 80;
|
|
8
10
|
const STREAM_SESSION_LIMIT = 8;
|
|
11
|
+
class HubRequestError extends Error {
|
|
12
|
+
_tag = 'HubRequestError';
|
|
13
|
+
path;
|
|
14
|
+
status;
|
|
15
|
+
statusText;
|
|
16
|
+
body;
|
|
17
|
+
plainArgs;
|
|
18
|
+
constructor(args) {
|
|
19
|
+
super(args.message, args.cause === undefined ? undefined : { cause: args.cause });
|
|
20
|
+
this.name = 'HubRequestError';
|
|
21
|
+
this.path = args.path;
|
|
22
|
+
this.status = args.status;
|
|
23
|
+
this.statusText = args.statusText;
|
|
24
|
+
this.body = args.body;
|
|
25
|
+
this.plainArgs = {
|
|
26
|
+
path: args.path,
|
|
27
|
+
status: args.status,
|
|
28
|
+
statusText: args.statusText,
|
|
29
|
+
body: args.body,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
9
33
|
function visibleWidth(text) {
|
|
10
34
|
return text.replace(ANSI_RE, '').replace(/\t/g, ' ').length;
|
|
11
35
|
}
|
|
@@ -48,8 +72,8 @@ function formatClock(ms) {
|
|
|
48
72
|
const d = new Date(ms);
|
|
49
73
|
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
50
74
|
}
|
|
51
|
-
function formatRelative(
|
|
52
|
-
const ts = Date.parse(
|
|
75
|
+
function formatRelative(value) {
|
|
76
|
+
const ts = typeof value === 'number' ? value : Date.parse(value);
|
|
53
77
|
if (Number.isNaN(ts))
|
|
54
78
|
return '-';
|
|
55
79
|
const seconds = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
@@ -64,6 +88,15 @@ function formatRelative(isoDate) {
|
|
|
64
88
|
const days = Math.floor(hours / 24);
|
|
65
89
|
return `${days}d ago`;
|
|
66
90
|
}
|
|
91
|
+
function formatElapsedCompact(ms) {
|
|
92
|
+
if (ms < 1_000)
|
|
93
|
+
return `${ms}ms`;
|
|
94
|
+
if (ms < 60_000)
|
|
95
|
+
return `${Math.round(ms / 1_000)}s`;
|
|
96
|
+
if (ms < 3_600_000)
|
|
97
|
+
return `${Math.round(ms / 60_000)}m`;
|
|
98
|
+
return `${Math.round(ms / 3_600_000)}h`;
|
|
99
|
+
}
|
|
67
100
|
function shortId(id) {
|
|
68
101
|
if (id.length <= 12)
|
|
69
102
|
return id;
|
|
@@ -249,14 +282,27 @@ export class HubOverlay {
|
|
|
249
282
|
cachedTodos = null;
|
|
250
283
|
feed = [];
|
|
251
284
|
sessionFeed = new Map();
|
|
285
|
+
sessionHistoryFeed = new Map();
|
|
252
286
|
sessionBuffers = new Map();
|
|
253
287
|
taskBuffers = new Map();
|
|
254
|
-
|
|
288
|
+
streamSources = new Map();
|
|
289
|
+
replayLoadedSessions = new Set();
|
|
290
|
+
replayInFlight = new Set();
|
|
291
|
+
replayRequestControllers = new Map();
|
|
292
|
+
replayRequestGenerations = new Map();
|
|
293
|
+
replayFailureUntil = new Map();
|
|
294
|
+
eventHistoryLoadedSessions = new Set();
|
|
295
|
+
eventHistoryInFlight = new Set();
|
|
296
|
+
eventHistoryRequestControllers = new Map();
|
|
297
|
+
eventHistoryRequestGenerations = new Map();
|
|
298
|
+
eventHistoryFailureUntil = new Map();
|
|
255
299
|
previousDigests = new Map();
|
|
256
300
|
loadingList = true;
|
|
257
301
|
loadingDetail = false;
|
|
258
302
|
listError = '';
|
|
259
303
|
detailError = '';
|
|
304
|
+
detailTodoRequestId = 0;
|
|
305
|
+
resumeBusySessionId = null;
|
|
260
306
|
lastUpdatedAt = 0;
|
|
261
307
|
listInFlight = false;
|
|
262
308
|
detailInFlight = false;
|
|
@@ -409,6 +455,14 @@ export class HubOverlay {
|
|
|
409
455
|
stream.controller.abort();
|
|
410
456
|
}
|
|
411
457
|
this.sseControllers.clear();
|
|
458
|
+
for (const controller of this.replayRequestControllers.values()) {
|
|
459
|
+
controller.abort();
|
|
460
|
+
}
|
|
461
|
+
this.replayRequestControllers.clear();
|
|
462
|
+
for (const controller of this.eventHistoryRequestControllers.values()) {
|
|
463
|
+
controller.abort();
|
|
464
|
+
}
|
|
465
|
+
this.eventHistoryRequestControllers.clear();
|
|
412
466
|
}
|
|
413
467
|
requestRender() {
|
|
414
468
|
try {
|
|
@@ -418,6 +472,52 @@ export class HubOverlay {
|
|
|
418
472
|
// Best effort render invalidation.
|
|
419
473
|
}
|
|
420
474
|
}
|
|
475
|
+
nextRequestGeneration(generations, sessionId) {
|
|
476
|
+
const next = (generations.get(sessionId) ?? 0) + 1;
|
|
477
|
+
generations.set(sessionId, next);
|
|
478
|
+
return next;
|
|
479
|
+
}
|
|
480
|
+
invalidateSessionRequest(generations, controllers, inFlight, sessionId) {
|
|
481
|
+
this.nextRequestGeneration(generations, sessionId);
|
|
482
|
+
inFlight.delete(sessionId);
|
|
483
|
+
const controller = controllers.get(sessionId);
|
|
484
|
+
if (controller)
|
|
485
|
+
controller.abort();
|
|
486
|
+
controllers.delete(sessionId);
|
|
487
|
+
}
|
|
488
|
+
isCurrentSessionRequest(generations, controllers, sessionId, generation, controller) {
|
|
489
|
+
return (!controller.signal.aborted &&
|
|
490
|
+
generations.get(sessionId) === generation &&
|
|
491
|
+
controllers.get(sessionId) === controller);
|
|
492
|
+
}
|
|
493
|
+
isFailureCoolingDown(failureUntil, sessionId) {
|
|
494
|
+
const until = failureUntil.get(sessionId);
|
|
495
|
+
if (!until)
|
|
496
|
+
return false;
|
|
497
|
+
if (until <= Date.now()) {
|
|
498
|
+
failureUntil.delete(sessionId);
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
markFailureCooldown(failureUntil, sessionId) {
|
|
504
|
+
failureUntil.set(sessionId, Date.now() + HISTORY_FETCH_FAILURE_COOLDOWN_MS);
|
|
505
|
+
}
|
|
506
|
+
clearReplayFailureCooldown(sessionId) {
|
|
507
|
+
this.replayFailureUntil.delete(sessionId);
|
|
508
|
+
}
|
|
509
|
+
clearEventHistoryFailureCooldown(sessionId) {
|
|
510
|
+
this.eventHistoryFailureUntil.delete(sessionId);
|
|
511
|
+
}
|
|
512
|
+
resetHistoricalSessionState(sessionId) {
|
|
513
|
+
this.invalidateSessionRequest(this.replayRequestGenerations, this.replayRequestControllers, this.replayInFlight, sessionId);
|
|
514
|
+
this.invalidateSessionRequest(this.eventHistoryRequestGenerations, this.eventHistoryRequestControllers, this.eventHistoryInFlight, sessionId);
|
|
515
|
+
this.replayLoadedSessions.delete(sessionId);
|
|
516
|
+
this.eventHistoryLoadedSessions.delete(sessionId);
|
|
517
|
+
this.sessionHistoryFeed.delete(sessionId);
|
|
518
|
+
this.clearReplayFailureCooldown(sessionId);
|
|
519
|
+
this.clearEventHistoryFailureCooldown(sessionId);
|
|
520
|
+
}
|
|
421
521
|
close() {
|
|
422
522
|
this.dispose();
|
|
423
523
|
this.done(undefined);
|
|
@@ -460,6 +560,12 @@ export class HubOverlay {
|
|
|
460
560
|
}
|
|
461
561
|
handleDetailInput(data) {
|
|
462
562
|
const tasks = this.getDetailTasks();
|
|
563
|
+
if ((matchesKey(data, 'w') || data.toLowerCase() === 'w') && this.detailSessionId) {
|
|
564
|
+
if (this.canResumeSession(this.detail)) {
|
|
565
|
+
void this.resumeSession(this.detailSessionId);
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
463
569
|
if (matchesKey(data, 'up')) {
|
|
464
570
|
if (tasks.length > 0) {
|
|
465
571
|
this.selectedTaskIndex = (this.selectedTaskIndex - 1 + tasks.length) % tasks.length;
|
|
@@ -708,9 +814,53 @@ export class HubOverlay {
|
|
|
708
814
|
}
|
|
709
815
|
return buffer;
|
|
710
816
|
}
|
|
817
|
+
getStreamSource(sessionId) {
|
|
818
|
+
return this.streamSources.get(sessionId) ?? 'none';
|
|
819
|
+
}
|
|
820
|
+
setStreamSource(sessionId, source) {
|
|
821
|
+
this.streamSources.set(sessionId, source);
|
|
822
|
+
}
|
|
823
|
+
clearTaskBuffers(sessionId) {
|
|
824
|
+
for (const key of this.taskBuffers.keys()) {
|
|
825
|
+
if (key.startsWith(`${sessionId}:`)) {
|
|
826
|
+
this.taskBuffers.delete(key);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
replaceStreamProjection(sessionId, projectionRaw, source) {
|
|
831
|
+
if (!projectionRaw)
|
|
832
|
+
return false;
|
|
833
|
+
const currentSource = this.getStreamSource(sessionId);
|
|
834
|
+
if (!shouldReplaceStreamProjection(currentSource, source))
|
|
835
|
+
return false;
|
|
836
|
+
const projection = normalizeStreamProjection(projectionRaw);
|
|
837
|
+
const sessionBuffer = this.getSessionBuffer(sessionId);
|
|
838
|
+
sessionBuffer.output = projection.output;
|
|
839
|
+
sessionBuffer.thinking = projection.thinking;
|
|
840
|
+
this.clearTaskBuffers(sessionId);
|
|
841
|
+
for (const [taskId, block] of Object.entries(projection.tasks)) {
|
|
842
|
+
const taskBuffer = this.getTaskBuffer(sessionId, taskId);
|
|
843
|
+
taskBuffer.output = block.output;
|
|
844
|
+
taskBuffer.thinking = block.thinking;
|
|
845
|
+
}
|
|
846
|
+
this.setStreamSource(sessionId, source);
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
applyReplayEntries(sessionId, entries) {
|
|
850
|
+
const projection = buildProjectionFromEntries(entries);
|
|
851
|
+
this.clearReplayFailureCooldown(sessionId);
|
|
852
|
+
if (this.replaceStreamProjection(sessionId, projection, 'replay')) {
|
|
853
|
+
this.replayLoadedSessions.add(sessionId);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
711
856
|
appendBufferText(sessionId, kind, chunk, taskId) {
|
|
712
857
|
if (!chunk)
|
|
713
858
|
return;
|
|
859
|
+
this.clearReplayFailureCooldown(sessionId);
|
|
860
|
+
if (this.replayInFlight.has(sessionId)) {
|
|
861
|
+
this.invalidateSessionRequest(this.replayRequestGenerations, this.replayRequestControllers, this.replayInFlight, sessionId);
|
|
862
|
+
}
|
|
863
|
+
this.setStreamSource(sessionId, 'live');
|
|
714
864
|
// Session stream is lead/top-level only; task-scoped output lives in task buffers.
|
|
715
865
|
if (!taskId) {
|
|
716
866
|
const sessionBuffer = this.getSessionBuffer(sessionId);
|
|
@@ -769,21 +919,62 @@ export class HubOverlay {
|
|
|
769
919
|
}
|
|
770
920
|
return ` ${tab('1', 'List', active === 'list')}${divider}${tab('2', 'Feed', active === 'feed')}`;
|
|
771
921
|
}
|
|
772
|
-
async fetchJson(path, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
922
|
+
async fetchJson(path, timeoutMs = REQUEST_TIMEOUT_MS, init) {
|
|
773
923
|
const controller = new AbortController();
|
|
774
924
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
775
925
|
try {
|
|
776
926
|
// TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
|
|
777
927
|
const apiKey = process.env.AGENTUITY_CODER_API_KEY;
|
|
778
|
-
const headers = {
|
|
928
|
+
const headers = {
|
|
929
|
+
accept: 'application/json',
|
|
930
|
+
...(init?.headers && typeof init.headers === 'object'
|
|
931
|
+
? init.headers
|
|
932
|
+
: {}),
|
|
933
|
+
};
|
|
779
934
|
if (apiKey)
|
|
780
935
|
headers['x-agentuity-auth-api-key'] = apiKey;
|
|
936
|
+
const signal = init?.signal
|
|
937
|
+
? AbortSignal.any([controller.signal, init.signal])
|
|
938
|
+
: controller.signal;
|
|
781
939
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
940
|
+
...init,
|
|
782
941
|
headers,
|
|
783
|
-
signal
|
|
942
|
+
signal,
|
|
784
943
|
});
|
|
785
944
|
if (!response.ok) {
|
|
786
|
-
|
|
945
|
+
let message = `Hub returned ${response.status}`;
|
|
946
|
+
let rawBody = '';
|
|
947
|
+
try {
|
|
948
|
+
const text = await response.text();
|
|
949
|
+
if (text) {
|
|
950
|
+
rawBody = text;
|
|
951
|
+
try {
|
|
952
|
+
const parsed = JSON.parse(text);
|
|
953
|
+
if (typeof parsed.error === 'string' && parsed.error.trim()) {
|
|
954
|
+
message = parsed.error;
|
|
955
|
+
}
|
|
956
|
+
else if (typeof parsed.message === 'string' && parsed.message.trim()) {
|
|
957
|
+
message = parsed.message;
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
message = text;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
catch {
|
|
964
|
+
message = text;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
catch {
|
|
969
|
+
// Fall back to the status-only message.
|
|
970
|
+
}
|
|
971
|
+
throw new HubRequestError({
|
|
972
|
+
message,
|
|
973
|
+
path,
|
|
974
|
+
status: response.status,
|
|
975
|
+
statusText: response.statusText || undefined,
|
|
976
|
+
body: rawBody || undefined,
|
|
977
|
+
});
|
|
787
978
|
}
|
|
788
979
|
return (await response.json());
|
|
789
980
|
}
|
|
@@ -791,6 +982,143 @@ export class HubOverlay {
|
|
|
791
982
|
clearTimeout(timeout);
|
|
792
983
|
}
|
|
793
984
|
}
|
|
985
|
+
getSessionSummary(sessionId) {
|
|
986
|
+
return this.sessions.find((item) => item.sessionId === sessionId);
|
|
987
|
+
}
|
|
988
|
+
isRuntimeAvailable(sessionId) {
|
|
989
|
+
const detailRuntime = this.detail?.sessionId === sessionId ? this.detail.runtimeAvailable : undefined;
|
|
990
|
+
if (typeof detailRuntime === 'boolean')
|
|
991
|
+
return detailRuntime;
|
|
992
|
+
const session = this.getSessionSummary(sessionId);
|
|
993
|
+
if (typeof session?.runtimeAvailable === 'boolean')
|
|
994
|
+
return session.runtimeAvailable;
|
|
995
|
+
if (session?.historyOnly)
|
|
996
|
+
return false;
|
|
997
|
+
if (session?.bucket === 'history')
|
|
998
|
+
return false;
|
|
999
|
+
return true;
|
|
1000
|
+
}
|
|
1001
|
+
isHistoryOnly(sessionId) {
|
|
1002
|
+
if (this.detail?.sessionId === sessionId && typeof this.detail.historyOnly === 'boolean') {
|
|
1003
|
+
return this.detail.historyOnly;
|
|
1004
|
+
}
|
|
1005
|
+
return this.getSessionSummary(sessionId)?.historyOnly === true;
|
|
1006
|
+
}
|
|
1007
|
+
canResumeSession(detail) {
|
|
1008
|
+
if (!detail)
|
|
1009
|
+
return false;
|
|
1010
|
+
return (detail.mode === 'sandbox' &&
|
|
1011
|
+
detail.bucket === 'paused' &&
|
|
1012
|
+
detail.historyOnly !== true &&
|
|
1013
|
+
detail.runtimeAvailable === false);
|
|
1014
|
+
}
|
|
1015
|
+
isArchivedStatus(status) {
|
|
1016
|
+
return status === 'archived' || status === 'shutdown' || status === 'stopped';
|
|
1017
|
+
}
|
|
1018
|
+
getStatusTone(status) {
|
|
1019
|
+
if (status === 'active' || status === 'running' || status === 'connected') {
|
|
1020
|
+
return 'success';
|
|
1021
|
+
}
|
|
1022
|
+
if (status === 'error' || status === 'failed' || status === 'stopped') {
|
|
1023
|
+
return 'error';
|
|
1024
|
+
}
|
|
1025
|
+
if (status === 'archived' || status === 'shutdown') {
|
|
1026
|
+
return 'muted';
|
|
1027
|
+
}
|
|
1028
|
+
return 'warning';
|
|
1029
|
+
}
|
|
1030
|
+
getConnectionState(input) {
|
|
1031
|
+
const isLocalSession = input.mode === 'tui';
|
|
1032
|
+
const isArchived = this.isArchivedStatus(input.status);
|
|
1033
|
+
const bucket = input.bucket ?? (input.historyOnly ? 'history' : 'running');
|
|
1034
|
+
const runtimeAvailable = input.runtimeAvailable !== false;
|
|
1035
|
+
const controlAvailable = input.controlAvailable ?? runtimeAvailable;
|
|
1036
|
+
const historyOnly = input.historyOnly === true || bucket === 'history';
|
|
1037
|
+
const canWake = !isLocalSession && !isArchived && bucket === 'paused' && historyOnly !== true;
|
|
1038
|
+
if (historyOnly)
|
|
1039
|
+
return { label: 'History only', tone: 'warning', controlAvailable };
|
|
1040
|
+
if (isLocalSession && bucket === 'provisioning')
|
|
1041
|
+
return { label: 'Starting', tone: 'warning', controlAvailable };
|
|
1042
|
+
if (isLocalSession && !isArchived)
|
|
1043
|
+
return { label: 'View only', tone: 'muted', controlAvailable };
|
|
1044
|
+
if (bucket === 'provisioning')
|
|
1045
|
+
return { label: 'Starting', tone: 'warning', controlAvailable };
|
|
1046
|
+
if (canWake) {
|
|
1047
|
+
return {
|
|
1048
|
+
label: this.resumeBusySessionId === input.sessionId ? 'Waking' : 'Paused',
|
|
1049
|
+
tone: this.resumeBusySessionId === input.sessionId ? 'accent' : 'warning',
|
|
1050
|
+
controlAvailable,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
if (!isArchived && runtimeAvailable && !controlAvailable) {
|
|
1054
|
+
return { label: 'Read only', tone: 'muted', controlAvailable };
|
|
1055
|
+
}
|
|
1056
|
+
if (!isArchived && runtimeAvailable)
|
|
1057
|
+
return { label: 'Live', tone: 'success', controlAvailable };
|
|
1058
|
+
if (isArchived)
|
|
1059
|
+
return { label: 'Archived', tone: 'muted', controlAvailable };
|
|
1060
|
+
return { label: 'Disconnected', tone: 'warning', controlAvailable };
|
|
1061
|
+
}
|
|
1062
|
+
formatTagSummary(tags, maxCount = 4) {
|
|
1063
|
+
if (!tags || tags.length === 0)
|
|
1064
|
+
return null;
|
|
1065
|
+
const visible = tags.slice(0, maxCount);
|
|
1066
|
+
const remainder = tags.length - visible.length;
|
|
1067
|
+
return remainder > 0 ? `${visible.join(', ')} +${remainder}` : visible.join(', ');
|
|
1068
|
+
}
|
|
1069
|
+
getSessionEventEntries(sessionId) {
|
|
1070
|
+
if (!this.isRuntimeAvailable(sessionId) || this.isHistoryOnly(sessionId)) {
|
|
1071
|
+
return this.sessionHistoryFeed.get(sessionId) ?? [];
|
|
1072
|
+
}
|
|
1073
|
+
return this.sessionFeed.get(sessionId) ?? [];
|
|
1074
|
+
}
|
|
1075
|
+
async refreshTodos(sessionId) {
|
|
1076
|
+
const requestId = ++this.detailTodoRequestId;
|
|
1077
|
+
try {
|
|
1078
|
+
const todosResponse = await this.fetchJson(`/api/hub/session/${encodeURIComponent(sessionId)}/todos?includeTerminal=true&includeSync=true&limit=30`, 15_000).catch((err) => {
|
|
1079
|
+
const isAbort = err?.name === 'AbortError' || err?.message?.includes('aborted');
|
|
1080
|
+
return {
|
|
1081
|
+
_fetchError: true,
|
|
1082
|
+
message: isAbort ? 'Todos loading...' : err?.message || 'Failed to load todos',
|
|
1083
|
+
};
|
|
1084
|
+
});
|
|
1085
|
+
if (this.disposed ||
|
|
1086
|
+
this.detail?.sessionId !== sessionId ||
|
|
1087
|
+
requestId !== this.detailTodoRequestId) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (todosResponse._fetchError) {
|
|
1091
|
+
if (this.cachedTodos && this.cachedTodos.sessionId === sessionId) {
|
|
1092
|
+
this.detail.todos = this.cachedTodos.todos;
|
|
1093
|
+
this.detail.todoSummary = this.cachedTodos.summary;
|
|
1094
|
+
}
|
|
1095
|
+
this.detail.todosUnavailable = todosResponse.message;
|
|
1096
|
+
this.requestRender();
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
this.detail.todos = Array.isArray(todosResponse.todos) ? todosResponse.todos : [];
|
|
1100
|
+
this.detail.todoSummary =
|
|
1101
|
+
todosResponse.summary && typeof todosResponse.summary === 'object'
|
|
1102
|
+
? todosResponse.summary
|
|
1103
|
+
: undefined;
|
|
1104
|
+
this.detail.todosUnavailable = todosResponse.unavailable
|
|
1105
|
+
? typeof todosResponse.message === 'string'
|
|
1106
|
+
? todosResponse.message
|
|
1107
|
+
: 'Task service unavailable'
|
|
1108
|
+
: undefined;
|
|
1109
|
+
if (this.detail.todos.length > 0) {
|
|
1110
|
+
this.cachedTodos = {
|
|
1111
|
+
todos: this.detail.todos,
|
|
1112
|
+
summary: this.detail.todoSummary,
|
|
1113
|
+
sessionId,
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
this.requestRender();
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
// Best-effort todos loading.
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
794
1122
|
async refreshList(initial = false) {
|
|
795
1123
|
if (this.disposed || this.listInFlight)
|
|
796
1124
|
return;
|
|
@@ -835,46 +1163,21 @@ export class HubOverlay {
|
|
|
835
1163
|
this.requestRender();
|
|
836
1164
|
}
|
|
837
1165
|
try {
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
_fetchError: true,
|
|
844
|
-
message: isAbort ? 'Todos loading\u2026' : err?.message || 'Failed to load todos',
|
|
845
|
-
};
|
|
846
|
-
}),
|
|
847
|
-
]);
|
|
848
|
-
if (todosResponse?._fetchError) {
|
|
849
|
-
// Use cached todos if available, show loading indicator
|
|
850
|
-
if (this.cachedTodos && this.cachedTodos.sessionId === sessionId) {
|
|
851
|
-
detail.todos = this.cachedTodos.todos;
|
|
852
|
-
detail.todoSummary = this.cachedTodos.summary;
|
|
853
|
-
detail.todosUnavailable = todosResponse.message;
|
|
854
|
-
}
|
|
855
|
-
else {
|
|
856
|
-
detail.todosUnavailable = todosResponse.message;
|
|
857
|
-
}
|
|
1166
|
+
const detail = await this.fetchJson(`/api/hub/session/${encodeURIComponent(sessionId)}`);
|
|
1167
|
+
if (this.cachedTodos && this.cachedTodos.sessionId === sessionId) {
|
|
1168
|
+
detail.todos = this.cachedTodos.todos;
|
|
1169
|
+
detail.todoSummary = this.cachedTodos.summary;
|
|
1170
|
+
detail.todosUnavailable = 'Loading todos...';
|
|
858
1171
|
}
|
|
859
|
-
else
|
|
860
|
-
detail.
|
|
861
|
-
detail.todoSummary =
|
|
862
|
-
todosResponse.summary && typeof todosResponse.summary === 'object'
|
|
863
|
-
? todosResponse.summary
|
|
864
|
-
: undefined;
|
|
865
|
-
detail.todosUnavailable = todosResponse.unavailable
|
|
866
|
-
? typeof todosResponse.message === 'string'
|
|
867
|
-
? todosResponse.message
|
|
868
|
-
: 'Task service unavailable'
|
|
869
|
-
: undefined;
|
|
870
|
-
// Cache successful todo data
|
|
871
|
-
if (detail.todos && detail.todos.length > 0) {
|
|
872
|
-
this.cachedTodos = { todos: detail.todos, summary: detail.todoSummary, sessionId };
|
|
873
|
-
}
|
|
1172
|
+
else {
|
|
1173
|
+
detail.todosUnavailable = 'Loading todos...';
|
|
874
1174
|
}
|
|
875
1175
|
this.detail = detail;
|
|
876
1176
|
this.detailSessionId = sessionId;
|
|
877
|
-
this.
|
|
1177
|
+
this.replaceStreamProjection(sessionId, detail.stream, 'snapshot');
|
|
1178
|
+
if (detail.runtimeAvailable !== false && detail.historyOnly !== true) {
|
|
1179
|
+
this.resetHistoricalSessionState(sessionId);
|
|
1180
|
+
}
|
|
878
1181
|
const taskCount = detail.tasks?.length ?? 0;
|
|
879
1182
|
if (taskCount === 0) {
|
|
880
1183
|
this.selectedTaskIndex = 0;
|
|
@@ -886,6 +1189,11 @@ export class HubOverlay {
|
|
|
886
1189
|
this.detailError = '';
|
|
887
1190
|
this.lastUpdatedAt = Date.now();
|
|
888
1191
|
this.requestRender();
|
|
1192
|
+
void this.refreshTodos(sessionId);
|
|
1193
|
+
if (detail.runtimeAvailable === false || detail.historyOnly === true) {
|
|
1194
|
+
void this.refreshReplay(sessionId);
|
|
1195
|
+
void this.refreshEventHistory(sessionId);
|
|
1196
|
+
}
|
|
889
1197
|
}
|
|
890
1198
|
catch (err) {
|
|
891
1199
|
this.loadingDetail = false;
|
|
@@ -896,6 +1204,127 @@ export class HubOverlay {
|
|
|
896
1204
|
this.detailInFlight = false;
|
|
897
1205
|
}
|
|
898
1206
|
}
|
|
1207
|
+
async refreshReplay(sessionId) {
|
|
1208
|
+
if (this.isFailureCoolingDown(this.replayFailureUntil, sessionId))
|
|
1209
|
+
return;
|
|
1210
|
+
if (this.replayLoadedSessions.has(sessionId) || this.replayInFlight.has(sessionId))
|
|
1211
|
+
return;
|
|
1212
|
+
const controller = new AbortController();
|
|
1213
|
+
const generation = this.nextRequestGeneration(this.replayRequestGenerations, sessionId);
|
|
1214
|
+
this.replayInFlight.add(sessionId);
|
|
1215
|
+
this.replayRequestControllers.set(sessionId, controller);
|
|
1216
|
+
try {
|
|
1217
|
+
const data = await this.fetchJson(`/api/hub/session/${encodeURIComponent(sessionId)}/replay`, 15_000, { signal: controller.signal });
|
|
1218
|
+
if (!this.isCurrentSessionRequest(this.replayRequestGenerations, this.replayRequestControllers, sessionId, generation, controller)) {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
this.applyReplayEntries(sessionId, Array.isArray(data.entries) ? data.entries : []);
|
|
1222
|
+
this.replayLoadedSessions.add(sessionId);
|
|
1223
|
+
this.requestRender();
|
|
1224
|
+
}
|
|
1225
|
+
catch (err) {
|
|
1226
|
+
if (controller.signal.aborted ||
|
|
1227
|
+
!this.isCurrentSessionRequest(this.replayRequestGenerations, this.replayRequestControllers, sessionId, generation, controller)) {
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
this.markFailureCooldown(this.replayFailureUntil, sessionId);
|
|
1231
|
+
this.pushFeed(`${this.getSessionLabel(sessionId)}: replay unavailable (${err instanceof Error ? err.message : String(err)})`, sessionId);
|
|
1232
|
+
this.requestRender();
|
|
1233
|
+
}
|
|
1234
|
+
finally {
|
|
1235
|
+
if (this.isCurrentSessionRequest(this.replayRequestGenerations, this.replayRequestControllers, sessionId, generation, controller)) {
|
|
1236
|
+
this.replayInFlight.delete(sessionId);
|
|
1237
|
+
this.replayRequestControllers.delete(sessionId);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
formatHistoryEventLine(sessionId, event) {
|
|
1242
|
+
const payload = event.payload && typeof event.payload === 'object'
|
|
1243
|
+
? event.payload
|
|
1244
|
+
: undefined;
|
|
1245
|
+
return (this.formatEventFeedLine(sessionId, event.event, payload) ??
|
|
1246
|
+
this.formatStreamLine(event.event, payload) ??
|
|
1247
|
+
`${event.event}${event.agent ? ` ${event.agent}` : ''}`);
|
|
1248
|
+
}
|
|
1249
|
+
async refreshEventHistory(sessionId) {
|
|
1250
|
+
if (this.isFailureCoolingDown(this.eventHistoryFailureUntil, sessionId)) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (this.eventHistoryLoadedSessions.has(sessionId) ||
|
|
1254
|
+
this.eventHistoryInFlight.has(sessionId)) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const controller = new AbortController();
|
|
1258
|
+
const generation = this.nextRequestGeneration(this.eventHistoryRequestGenerations, sessionId);
|
|
1259
|
+
this.eventHistoryInFlight.add(sessionId);
|
|
1260
|
+
this.eventHistoryRequestControllers.set(sessionId, controller);
|
|
1261
|
+
try {
|
|
1262
|
+
const data = await this.fetchJson(`/api/hub/session/${encodeURIComponent(sessionId)}/events/history?limit=100`, 15_000, { signal: controller.signal });
|
|
1263
|
+
if (!this.isCurrentSessionRequest(this.eventHistoryRequestGenerations, this.eventHistoryRequestControllers, sessionId, generation, controller)) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const entries = (data.events ?? [])
|
|
1267
|
+
.map((event) => {
|
|
1268
|
+
const text = this.formatHistoryEventLine(sessionId, event);
|
|
1269
|
+
if (!text)
|
|
1270
|
+
return null;
|
|
1271
|
+
return {
|
|
1272
|
+
at: Number.isNaN(Date.parse(event.occurredAt))
|
|
1273
|
+
? Date.now()
|
|
1274
|
+
: Date.parse(event.occurredAt),
|
|
1275
|
+
sessionId,
|
|
1276
|
+
text,
|
|
1277
|
+
};
|
|
1278
|
+
})
|
|
1279
|
+
.filter((entry) => entry !== null)
|
|
1280
|
+
.sort((a, b) => b.at - a.at);
|
|
1281
|
+
this.sessionHistoryFeed.set(sessionId, entries);
|
|
1282
|
+
this.clearEventHistoryFailureCooldown(sessionId);
|
|
1283
|
+
this.eventHistoryLoadedSessions.add(sessionId);
|
|
1284
|
+
this.requestRender();
|
|
1285
|
+
}
|
|
1286
|
+
catch (err) {
|
|
1287
|
+
if (controller.signal.aborted ||
|
|
1288
|
+
!this.isCurrentSessionRequest(this.eventHistoryRequestGenerations, this.eventHistoryRequestControllers, sessionId, generation, controller)) {
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
this.markFailureCooldown(this.eventHistoryFailureUntil, sessionId);
|
|
1292
|
+
this.pushFeed(`${this.getSessionLabel(sessionId)}: event history unavailable (${err instanceof Error ? err.message : String(err)})`, sessionId);
|
|
1293
|
+
this.requestRender();
|
|
1294
|
+
}
|
|
1295
|
+
finally {
|
|
1296
|
+
if (this.isCurrentSessionRequest(this.eventHistoryRequestGenerations, this.eventHistoryRequestControllers, sessionId, generation, controller)) {
|
|
1297
|
+
this.eventHistoryInFlight.delete(sessionId);
|
|
1298
|
+
this.eventHistoryRequestControllers.delete(sessionId);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
async resumeSession(sessionId) {
|
|
1303
|
+
if (this.resumeBusySessionId)
|
|
1304
|
+
return;
|
|
1305
|
+
this.resumeBusySessionId = sessionId;
|
|
1306
|
+
this.pushFeed(`${this.getSessionLabel(sessionId)}: resume requested`, sessionId);
|
|
1307
|
+
this.requestRender();
|
|
1308
|
+
try {
|
|
1309
|
+
const response = await this.fetchJson(`/api/hub/session/${encodeURIComponent(sessionId)}/resume`, 15_000, { method: 'POST' });
|
|
1310
|
+
if (response.resumed !== true) {
|
|
1311
|
+
throw new Error(response.error || 'Hub declined to resume the session');
|
|
1312
|
+
}
|
|
1313
|
+
this.resetHistoricalSessionState(sessionId);
|
|
1314
|
+
await this.refreshList();
|
|
1315
|
+
await this.refreshDetail(sessionId);
|
|
1316
|
+
}
|
|
1317
|
+
catch (err) {
|
|
1318
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1319
|
+
this.detailError = message;
|
|
1320
|
+
this.pushFeed(`${this.getSessionLabel(sessionId)}: resume failed (${message})`, sessionId);
|
|
1321
|
+
this.requestRender();
|
|
1322
|
+
}
|
|
1323
|
+
finally {
|
|
1324
|
+
this.resumeBusySessionId = null;
|
|
1325
|
+
this.requestRender();
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
899
1328
|
updateFeedFromList(sessions) {
|
|
900
1329
|
if (this.previousDigests.size === 0) {
|
|
901
1330
|
if (sessions.length > 0) {
|
|
@@ -952,9 +1381,14 @@ export class HubOverlay {
|
|
|
952
1381
|
return;
|
|
953
1382
|
const desiredModes = new Map();
|
|
954
1383
|
for (const session of sessions.slice(0, STREAM_SESSION_LIMIT)) {
|
|
1384
|
+
if (session.runtimeAvailable === false || session.historyOnly === true)
|
|
1385
|
+
continue;
|
|
955
1386
|
desiredModes.set(session.sessionId, 'summary');
|
|
956
1387
|
}
|
|
957
|
-
if (this.detailSessionId &&
|
|
1388
|
+
if (this.detailSessionId &&
|
|
1389
|
+
this.isSessionContext() &&
|
|
1390
|
+
this.isRuntimeAvailable(this.detailSessionId) &&
|
|
1391
|
+
!this.isHistoryOnly(this.detailSessionId)) {
|
|
958
1392
|
desiredModes.set(this.detailSessionId, 'full');
|
|
959
1393
|
}
|
|
960
1394
|
for (const [sessionId, stream] of this.sseControllers) {
|
|
@@ -1113,52 +1547,16 @@ export class HubOverlay {
|
|
|
1113
1547
|
}
|
|
1114
1548
|
}
|
|
1115
1549
|
applyHydration(sessionId, eventData) {
|
|
1116
|
-
if (this.hydratedSessions.has(sessionId))
|
|
1117
|
-
return;
|
|
1118
1550
|
if (!eventData || typeof eventData !== 'object')
|
|
1119
1551
|
return;
|
|
1120
1552
|
const payload = eventData;
|
|
1121
|
-
const loadedFromProjection = this.
|
|
1553
|
+
const loadedFromProjection = this.replaceStreamProjection(sessionId, payload.stream, 'hydration');
|
|
1122
1554
|
if (!loadedFromProjection) {
|
|
1123
|
-
const entries = Array.isArray(payload.entries)
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
const entry = rawEntry;
|
|
1128
|
-
const type = typeof entry.type === 'string' ? entry.type : '';
|
|
1129
|
-
const content = typeof entry.content === 'string' ? entry.content : '';
|
|
1130
|
-
const taskId = typeof entry.taskId === 'string' ? entry.taskId : undefined;
|
|
1131
|
-
if (!content)
|
|
1132
|
-
continue;
|
|
1133
|
-
if (type === 'message' || type === 'task_result') {
|
|
1134
|
-
this.appendBufferText(sessionId, 'output', content + '\n\n', taskId);
|
|
1135
|
-
}
|
|
1136
|
-
else if (type === 'thinking') {
|
|
1137
|
-
this.appendBufferText(sessionId, 'thinking', content + '\n\n', taskId);
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
this.hydratedSessions.add(sessionId);
|
|
1142
|
-
}
|
|
1143
|
-
applyStreamProjection(sessionId, streamRaw) {
|
|
1144
|
-
if (!streamRaw || typeof streamRaw !== 'object')
|
|
1145
|
-
return false;
|
|
1146
|
-
const stream = streamRaw;
|
|
1147
|
-
const sessionBuffer = this.getSessionBuffer(sessionId);
|
|
1148
|
-
sessionBuffer.output = typeof stream.output === 'string' ? stream.output : '';
|
|
1149
|
-
sessionBuffer.thinking = typeof stream.thinking === 'string' ? stream.thinking : '';
|
|
1150
|
-
const taskStreams = stream.tasks && typeof stream.tasks === 'object'
|
|
1151
|
-
? stream.tasks
|
|
1152
|
-
: {};
|
|
1153
|
-
for (const [taskId, rawBlock] of Object.entries(taskStreams)) {
|
|
1154
|
-
if (!rawBlock || typeof rawBlock !== 'object')
|
|
1155
|
-
continue;
|
|
1156
|
-
const block = rawBlock;
|
|
1157
|
-
const taskBuffer = this.getTaskBuffer(sessionId, taskId);
|
|
1158
|
-
taskBuffer.output = typeof block.output === 'string' ? block.output : '';
|
|
1159
|
-
taskBuffer.thinking = typeof block.thinking === 'string' ? block.thinking : '';
|
|
1555
|
+
const entries = Array.isArray(payload.entries)
|
|
1556
|
+
? payload.entries.filter((entry) => !!entry && typeof entry === 'object')
|
|
1557
|
+
: [];
|
|
1558
|
+
this.replaceStreamProjection(sessionId, buildProjectionFromEntries(entries), 'hydration');
|
|
1160
1559
|
}
|
|
1161
|
-
return true;
|
|
1162
1560
|
}
|
|
1163
1561
|
captureStreamContent(sessionId, eventName, eventData, taskId) {
|
|
1164
1562
|
const data = eventData && typeof eventData === 'object'
|
|
@@ -1269,7 +1667,7 @@ export class HubOverlay {
|
|
|
1269
1667
|
}
|
|
1270
1668
|
getFeedEntries(scope) {
|
|
1271
1669
|
if (scope === 'session' && this.detailSessionId) {
|
|
1272
|
-
return this.
|
|
1670
|
+
return this.getSessionEventEntries(this.detailSessionId);
|
|
1273
1671
|
}
|
|
1274
1672
|
return this.feed;
|
|
1275
1673
|
}
|
|
@@ -1457,7 +1855,21 @@ export class HubOverlay {
|
|
|
1457
1855
|
const updated = this.lastUpdatedAt
|
|
1458
1856
|
? `${formatClock(this.lastUpdatedAt)} updated`
|
|
1459
1857
|
: 'not updated';
|
|
1460
|
-
|
|
1858
|
+
const liveCount = this.sessions.filter((session) => {
|
|
1859
|
+
const connection = this.getConnectionState({
|
|
1860
|
+
sessionId: session.sessionId,
|
|
1861
|
+
mode: session.mode,
|
|
1862
|
+
status: session.status,
|
|
1863
|
+
bucket: session.bucket,
|
|
1864
|
+
runtimeAvailable: session.runtimeAvailable,
|
|
1865
|
+
controlAvailable: session.controlAvailable,
|
|
1866
|
+
historyOnly: session.historyOnly,
|
|
1867
|
+
});
|
|
1868
|
+
return connection.label === 'Live';
|
|
1869
|
+
}).length;
|
|
1870
|
+
const pausedCount = this.sessions.filter((session) => session.bucket === 'paused' && session.historyOnly !== true).length;
|
|
1871
|
+
const historyCount = this.sessions.filter((session) => session.historyOnly === true || session.bucket === 'history').length;
|
|
1872
|
+
body.push(this.contentLine(this.theme.fg('muted', ` Active: ${this.sessions.length} sessions Live:${liveCount} Paused:${pausedCount} History:${historyCount} ${updated}`), inner));
|
|
1461
1873
|
}
|
|
1462
1874
|
body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
|
|
1463
1875
|
body.push(this.contentLine(this.theme.bold(' Teams / Observers'), inner));
|
|
@@ -1476,17 +1888,31 @@ export class HubOverlay {
|
|
|
1476
1888
|
const marker = selected ? this.theme.fg('accent', '›') : ' ';
|
|
1477
1889
|
const label = session.label || shortId(session.sessionId);
|
|
1478
1890
|
const name = selected ? this.theme.bold(label) : label;
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
: session.
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1891
|
+
const status = this.theme.fg(this.getStatusTone(session.status), session.status);
|
|
1892
|
+
const connection = this.getConnectionState({
|
|
1893
|
+
sessionId: session.sessionId,
|
|
1894
|
+
mode: session.mode,
|
|
1895
|
+
status: session.status,
|
|
1896
|
+
bucket: session.bucket,
|
|
1897
|
+
runtimeAvailable: session.runtimeAvailable,
|
|
1898
|
+
controlAvailable: session.controlAvailable,
|
|
1899
|
+
historyOnly: session.historyOnly,
|
|
1900
|
+
});
|
|
1901
|
+
const connectionLabel = this.theme.fg(connection.tone, connection.label);
|
|
1485
1902
|
const self = this.currentSessionId === session.sessionId
|
|
1486
1903
|
? this.theme.fg('accent', ' (this)')
|
|
1487
1904
|
: '';
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1905
|
+
const tagSummary = this.formatTagSummary(session.tags, 2);
|
|
1906
|
+
const metricsParts = [
|
|
1907
|
+
`obs:${session.observerCount}`,
|
|
1908
|
+
`agents:${session.subAgentCount}`,
|
|
1909
|
+
`tasks:${session.taskCount}`,
|
|
1910
|
+
formatRelative(session.createdAt),
|
|
1911
|
+
session.defaultAgent ? `agent:${session.defaultAgent}` : undefined,
|
|
1912
|
+
tagSummary ? `tags:${tagSummary}` : undefined,
|
|
1913
|
+
].filter((part) => !!part);
|
|
1914
|
+
const metrics = this.theme.fg('muted', metricsParts.join(' '));
|
|
1915
|
+
body.push(this.contentLine(` ${marker} ${name}${self} ${status} ${session.mode} ${connectionLabel} ${metrics}`, inner));
|
|
1490
1916
|
}
|
|
1491
1917
|
if (end < this.sessions.length) {
|
|
1492
1918
|
body.push(this.contentLine(this.theme.fg('dim', ` ↓ ${this.sessions.length - end} more below`), inner));
|
|
@@ -1504,11 +1930,12 @@ export class HubOverlay {
|
|
|
1504
1930
|
renderDetailScreen(width, maxLines) {
|
|
1505
1931
|
const inner = Math.max(0, width - 2);
|
|
1506
1932
|
const lines = [];
|
|
1507
|
-
const title = this.detail?.label ||
|
|
1933
|
+
const title = this.detail?.label ||
|
|
1934
|
+
(this.detailSessionId ? shortId(this.detailSessionId) : 'Hub Session');
|
|
1508
1935
|
const headerRows = 3;
|
|
1509
1936
|
const footerRows = 2;
|
|
1510
1937
|
const contentBudget = Math.max(5, maxLines - headerRows - footerRows);
|
|
1511
|
-
lines.push(buildTopBorder(width, `Session ${
|
|
1938
|
+
lines.push(buildTopBorder(width, `Session ${title}`));
|
|
1512
1939
|
lines.push(this.contentLine('', inner));
|
|
1513
1940
|
lines.push(this.contentLine(this.buildTopTabs('detail', true), inner));
|
|
1514
1941
|
const body = [];
|
|
@@ -1526,17 +1953,67 @@ export class HubOverlay {
|
|
|
1526
1953
|
const participants = session.participants ?? [];
|
|
1527
1954
|
const tasks = session.tasks ?? [];
|
|
1528
1955
|
const activityEntries = Object.entries(session.agentActivity ?? {});
|
|
1956
|
+
const connection = this.getConnectionState({
|
|
1957
|
+
sessionId: session.sessionId,
|
|
1958
|
+
mode: session.mode,
|
|
1959
|
+
status: session.status,
|
|
1960
|
+
bucket: session.bucket,
|
|
1961
|
+
runtimeAvailable: session.runtimeAvailable,
|
|
1962
|
+
controlAvailable: session.controlAvailable,
|
|
1963
|
+
historyOnly: session.historyOnly,
|
|
1964
|
+
});
|
|
1965
|
+
const inactiveTasks = session.diagnostics?.inactiveRunningTasks ?? [];
|
|
1966
|
+
const inactiveTaskById = new Map(inactiveTasks.map((item) => [item.taskId, item]));
|
|
1967
|
+
const pushWrappedLine = (text, tone = 'muted') => {
|
|
1968
|
+
for (const wrapped of wrapText(text, Math.max(12, inner - 4))) {
|
|
1969
|
+
body.push(this.contentLine(this.theme.fg(tone, ` ${wrapped}`), inner));
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1529
1972
|
body.push(this.contentLine(this.theme.bold(' Overview'), inner));
|
|
1530
1973
|
body.push(this.contentLine(this.theme.fg('muted', ` ID: ${session.sessionId}`), inner));
|
|
1531
|
-
body.push(this.contentLine(this.theme.fg(
|
|
1974
|
+
body.push(this.contentLine(` Status: ${this.theme.fg(this.getStatusTone(session.status), session.status)} Mode: ${session.mode} Bucket: ${session.bucket ?? '-'}`, inner));
|
|
1975
|
+
body.push(this.contentLine(` Connection: ${this.theme.fg(connection.tone, connection.label)} Runtime: ${session.runtimeAvailable === false ? 'offline' : 'ready'} Control: ${connection.controlAvailable ? 'enabled' : 'read-only'}`, inner));
|
|
1532
1976
|
body.push(this.contentLine(this.theme.fg('muted', ` Created: ${formatRelative(session.createdAt)}`), inner));
|
|
1533
1977
|
body.push(this.contentLine(this.theme.fg('muted', ` Participants: ${participants.length} Tasks: ${tasks.length} Active agents: ${activityEntries.length}`), inner));
|
|
1978
|
+
if (session.defaultAgent) {
|
|
1979
|
+
body.push(this.contentLine(this.theme.fg('muted', ` Default agent: ${session.defaultAgent}`), inner));
|
|
1980
|
+
}
|
|
1981
|
+
const tagSummary = this.formatTagSummary(session.tags);
|
|
1982
|
+
if (tagSummary) {
|
|
1983
|
+
pushWrappedLine(`Tags: ${tagSummary}`);
|
|
1984
|
+
}
|
|
1985
|
+
if (session.streamId || session.streamUrl) {
|
|
1986
|
+
const streamState = session.streamId
|
|
1987
|
+
? shortId(session.streamId)
|
|
1988
|
+
: session.streamUrl
|
|
1989
|
+
? 'attached'
|
|
1990
|
+
: 'none';
|
|
1991
|
+
body.push(this.contentLine(this.theme.fg('muted', ` Stream: ${streamState}`), inner));
|
|
1992
|
+
}
|
|
1534
1993
|
if (session.context?.branch) {
|
|
1535
1994
|
body.push(this.contentLine(this.theme.fg('muted', ` Branch: ${session.context.branch}`), inner));
|
|
1536
1995
|
}
|
|
1537
1996
|
if (session.context?.workingDirectory) {
|
|
1538
1997
|
body.push(this.contentLine(this.theme.fg('muted', ` CWD: ${session.context.workingDirectory}`), inner));
|
|
1539
1998
|
}
|
|
1999
|
+
if (session.task) {
|
|
2000
|
+
body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
|
|
2001
|
+
body.push(this.contentLine(this.theme.bold(' Root Task'), inner));
|
|
2002
|
+
for (const wrapped of wrapText(session.task, Math.max(12, inner - 4))) {
|
|
2003
|
+
body.push(this.contentLine(` ${wrapped}`, inner));
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
if (session.error) {
|
|
2007
|
+
body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
|
|
2008
|
+
body.push(this.contentLine(this.theme.bold(' Error'), inner));
|
|
2009
|
+
pushWrappedLine(session.error, 'error');
|
|
2010
|
+
}
|
|
2011
|
+
if (this.canResumeSession(session)) {
|
|
2012
|
+
const resumeMessage = this.resumeBusySessionId === session.sessionId
|
|
2013
|
+
? 'Waking sandbox session...'
|
|
2014
|
+
: 'Press [w] to wake this paused sandbox session.';
|
|
2015
|
+
pushWrappedLine(resumeMessage, this.resumeBusySessionId === session.sessionId ? 'muted' : 'warning');
|
|
2016
|
+
}
|
|
1540
2017
|
body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
|
|
1541
2018
|
body.push(this.contentLine(this.theme.bold(' Tasks'), inner));
|
|
1542
2019
|
body.push(this.contentLine(this.theme.fg('dim', ' Use ↑ and ↓ to move, Enter to open task view'), inner));
|
|
@@ -1554,14 +2031,22 @@ export class HubOverlay {
|
|
|
1554
2031
|
? 'success'
|
|
1555
2032
|
: task.status === 'failed'
|
|
1556
2033
|
? 'error'
|
|
1557
|
-
: '
|
|
2034
|
+
: task.status === 'running'
|
|
2035
|
+
? 'accent'
|
|
2036
|
+
: 'warning';
|
|
1558
2037
|
const status = this.theme.fg(statusColor, task.status);
|
|
2038
|
+
const inactive = inactiveTaskById.get(task.taskId);
|
|
1559
2039
|
const prompt = task.prompt
|
|
1560
2040
|
? truncateToWidth(toSingleLine(task.prompt), Math.max(16, inner - 34))
|
|
1561
2041
|
: '';
|
|
1562
|
-
const duration = typeof task.duration === 'number'
|
|
2042
|
+
const duration = typeof task.duration === 'number'
|
|
2043
|
+
? ` ${formatElapsedCompact(task.duration)}`
|
|
2044
|
+
: '';
|
|
2045
|
+
const idle = inactive
|
|
2046
|
+
? ` ${this.theme.fg('warning', `idle ${formatElapsedCompact(inactive.inactivityMs)}`)}`
|
|
2047
|
+
: '';
|
|
1563
2048
|
const marker = selected ? this.theme.fg('accent', '›') : ' ';
|
|
1564
|
-
body.push(this.contentLine(`${marker} ${shortId(task.taskId).padEnd(12)} ${task.agent.padEnd(9)} ${status}${duration} ${prompt}`, inner));
|
|
2049
|
+
body.push(this.contentLine(`${marker} ${shortId(task.taskId).padEnd(12)} ${task.agent.padEnd(9)} ${status}${duration}${idle} ${prompt}`, inner));
|
|
1565
2050
|
}
|
|
1566
2051
|
}
|
|
1567
2052
|
body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
|
|
@@ -1617,6 +2102,17 @@ export class HubOverlay {
|
|
|
1617
2102
|
body.push(this.contentLine(` ${participant.id.padEnd(12)} ${participant.role.padEnd(9)} ${(participant.transport || 'ws').padEnd(3)} ${when}${idle}`, inner));
|
|
1618
2103
|
}
|
|
1619
2104
|
}
|
|
2105
|
+
if (inactiveTasks.length > 0) {
|
|
2106
|
+
body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
|
|
2107
|
+
body.push(this.contentLine(this.theme.bold(' Diagnostics'), inner));
|
|
2108
|
+
body.push(this.contentLine(this.theme.fg('warning', ` ${inactiveTasks.length} running task${inactiveTasks.length === 1 ? '' : 's'} without recent activity`), inner));
|
|
2109
|
+
for (const item of inactiveTasks.slice(0, 8)) {
|
|
2110
|
+
const lastSeen = item.lastActivityAt
|
|
2111
|
+
? ` last ${formatRelative(item.lastActivityAt)}`
|
|
2112
|
+
: '';
|
|
2113
|
+
body.push(this.contentLine(this.theme.fg('warning', ` ${shortId(item.taskId).padEnd(12)} ${item.agent.padEnd(10)} idle ${formatElapsedCompact(item.inactivityMs)} started ${formatRelative(item.startedAt)}${lastSeen}`), inner));
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
1620
2116
|
body.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
|
|
1621
2117
|
body.push(this.contentLine(this.theme.bold(' Agent Activity'), inner));
|
|
1622
2118
|
if (activityEntries.length === 0) {
|
|
@@ -1624,12 +2120,28 @@ export class HubOverlay {
|
|
|
1624
2120
|
}
|
|
1625
2121
|
else {
|
|
1626
2122
|
for (const [agent, info] of activityEntries.slice(0, 15)) {
|
|
1627
|
-
const tool = info.currentTool ? ` ${info.currentTool}` : '';
|
|
1628
|
-
const calls = typeof info.toolCallCount === 'number'
|
|
1629
|
-
? this.theme.fg('dim', ` (${info.toolCallCount} calls)`)
|
|
1630
|
-
: '';
|
|
1631
2123
|
const status = info.status || 'idle';
|
|
1632
|
-
|
|
2124
|
+
const statusTone = status === 'completed'
|
|
2125
|
+
? 'success'
|
|
2126
|
+
: status === 'failed'
|
|
2127
|
+
? 'error'
|
|
2128
|
+
: status === 'tool_start' || status === 'running'
|
|
2129
|
+
? 'accent'
|
|
2130
|
+
: 'warning';
|
|
2131
|
+
const tool = info.currentTool ? ` tool:${info.currentTool}` : '';
|
|
2132
|
+
const calls = typeof info.toolCallCount === 'number' ? ` calls:${info.toolCallCount}` : '';
|
|
2133
|
+
const lastSeen = info.lastActivity !== undefined && info.lastActivity !== null
|
|
2134
|
+
? ` last:${formatRelative(info.lastActivity)}`
|
|
2135
|
+
: '';
|
|
2136
|
+
const totalElapsed = typeof info.totalElapsed === 'number'
|
|
2137
|
+
? ` total:${formatElapsedCompact(info.totalElapsed)}`
|
|
2138
|
+
: '';
|
|
2139
|
+
body.push(this.contentLine(` ${agent.padEnd(12)} ${this.theme.fg(statusTone, status)}${this.theme.fg('dim', `${tool}${calls}${lastSeen}${totalElapsed}`)}`, inner));
|
|
2140
|
+
if (info.currentToolArgs) {
|
|
2141
|
+
for (const wrapped of wrapText(toSingleLine(info.currentToolArgs), Math.max(12, inner - 6))) {
|
|
2142
|
+
body.push(this.contentLine(this.theme.fg('dim', ` ${wrapped}`), inner));
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
1633
2145
|
}
|
|
1634
2146
|
}
|
|
1635
2147
|
}
|
|
@@ -1645,7 +2157,8 @@ export class HubOverlay {
|
|
|
1645
2157
|
const scrollInfo = this.detailMaxScroll > 0
|
|
1646
2158
|
? this.theme.fg('dim', ` scroll ${this.detailScrollOffset}/${this.detailMaxScroll}`)
|
|
1647
2159
|
: this.theme.fg('dim', ' scroll 0/0');
|
|
1648
|
-
|
|
2160
|
+
const wakeHint = this.canResumeSession(this.detail) ? ' [w] Wake' : '';
|
|
2161
|
+
lines.push(this.contentLine(`${scrollInfo} ${this.theme.fg('dim', `[↑↓] Task [j/k] Scroll [Enter] Open${wakeHint} [r] Refresh [Esc] Back`)}`, inner));
|
|
1649
2162
|
lines.push(buildBottomBorder(width));
|
|
1650
2163
|
return lines.slice(0, maxLines);
|
|
1651
2164
|
}
|
|
@@ -1656,25 +2169,41 @@ export class HubOverlay {
|
|
|
1656
2169
|
const footerRows = 2;
|
|
1657
2170
|
const contentBudget = Math.max(5, maxLines - headerRows - footerRows);
|
|
1658
2171
|
const scoped = this.feedScope === 'session' && !!this.detailSessionId;
|
|
2172
|
+
const sessionId = scoped ? this.detailSessionId : null;
|
|
2173
|
+
const historySession = sessionId
|
|
2174
|
+
? !this.isRuntimeAvailable(sessionId) || this.isHistoryOnly(sessionId)
|
|
2175
|
+
: false;
|
|
1659
2176
|
const title = scoped
|
|
1660
|
-
? `${this.feedViewMode === 'events' ? 'Session Events' : 'Session Feed'} ${
|
|
2177
|
+
? `${this.feedViewMode === 'events' ? 'Session Events' : 'Session Feed'} ${this.getSessionLabel(sessionId)}`
|
|
1661
2178
|
: 'Global Feed';
|
|
1662
2179
|
const entryBudget = Math.max(1, contentBudget - 2);
|
|
1663
|
-
const sessionBuffer =
|
|
2180
|
+
const sessionBuffer = sessionId ? this.sessionBuffers.get(sessionId) : undefined;
|
|
1664
2181
|
lines.push(buildTopBorder(width, title));
|
|
1665
2182
|
lines.push(this.contentLine('', inner));
|
|
1666
2183
|
lines.push(this.contentLine(this.buildTopTabs(scoped ? (this.feedViewMode === 'events' ? 'events' : 'feed') : 'feed', scoped), inner));
|
|
1667
2184
|
lines.push(this.contentLine(this.theme.fg('muted', scoped
|
|
1668
2185
|
? this.feedViewMode === 'stream'
|
|
1669
|
-
?
|
|
1670
|
-
|
|
2186
|
+
? historySession
|
|
2187
|
+
? ' Rehydrated session output from stored replay — [v] task stream'
|
|
2188
|
+
: ' Live rendered session output (sub-agent style) — [v] task stream'
|
|
2189
|
+
: historySession
|
|
2190
|
+
? ' Stored session events from history'
|
|
2191
|
+
: ' Live full session events'
|
|
1671
2192
|
: ' Streaming event summaries across all sessions'), inner));
|
|
1672
2193
|
lines.push(this.contentLine(this.theme.fg('dim', ` ${hLine(Math.max(0, inner - 2))}`), inner));
|
|
1673
2194
|
let contentLines = [];
|
|
1674
2195
|
if (scoped && this.feedViewMode === 'stream') {
|
|
1675
2196
|
contentLines = this.renderStreamLines(sessionBuffer?.output ?? '', sessionBuffer?.thinking ?? '', this.showFeedThinking, Math.max(12, inner - 4));
|
|
1676
2197
|
if (contentLines.length === 0) {
|
|
1677
|
-
|
|
2198
|
+
if (sessionId && historySession && this.replayInFlight.has(sessionId)) {
|
|
2199
|
+
contentLines = [this.theme.fg('dim', '(loading replay...)')];
|
|
2200
|
+
}
|
|
2201
|
+
else if (sessionId && historySession && this.replayLoadedSessions.has(sessionId)) {
|
|
2202
|
+
contentLines = [this.theme.fg('dim', '(no replay output available)')];
|
|
2203
|
+
}
|
|
2204
|
+
else {
|
|
2205
|
+
contentLines = [this.theme.fg('dim', '(no streamed output yet)')];
|
|
2206
|
+
}
|
|
1678
2207
|
}
|
|
1679
2208
|
}
|
|
1680
2209
|
else {
|
|
@@ -1683,7 +2212,19 @@ export class HubOverlay {
|
|
|
1683
2212
|
.reverse()
|
|
1684
2213
|
.map((entry) => `${this.theme.fg('dim', formatClock(entry.at))} ${entry.text}`);
|
|
1685
2214
|
if (contentLines.length === 0) {
|
|
1686
|
-
|
|
2215
|
+
if (sessionId && historySession && this.eventHistoryInFlight.has(sessionId)) {
|
|
2216
|
+
contentLines = [this.theme.fg('dim', '(loading event history...)')];
|
|
2217
|
+
}
|
|
2218
|
+
else if (sessionId &&
|
|
2219
|
+
historySession &&
|
|
2220
|
+
this.eventHistoryLoadedSessions.has(sessionId)) {
|
|
2221
|
+
contentLines = [this.theme.fg('dim', '(no stored events available)')];
|
|
2222
|
+
}
|
|
2223
|
+
else {
|
|
2224
|
+
contentLines = [
|
|
2225
|
+
this.theme.fg('dim', scoped ? '(no session feed items yet)' : '(no feed items yet)'),
|
|
2226
|
+
];
|
|
2227
|
+
}
|
|
1687
2228
|
}
|
|
1688
2229
|
}
|
|
1689
2230
|
this.feedMaxScroll = Math.max(0, contentLines.length - entryBudget);
|
|
@@ -1728,12 +2269,16 @@ export class HubOverlay {
|
|
|
1728
2269
|
body.push(this.contentLine(this.theme.fg('dim', ' No task selected'), inner));
|
|
1729
2270
|
}
|
|
1730
2271
|
else {
|
|
2272
|
+
const historySession = this.detailSessionId
|
|
2273
|
+
? !this.isRuntimeAvailable(this.detailSessionId) ||
|
|
2274
|
+
this.isHistoryOnly(this.detailSessionId)
|
|
2275
|
+
: false;
|
|
1731
2276
|
body.push(this.contentLine(this.theme.bold(' Task Overview'), inner));
|
|
1732
2277
|
body.push(this.contentLine(this.theme.fg('muted', ` Task ID: ${selected.taskId}`), inner));
|
|
1733
2278
|
body.push(this.contentLine(this.theme.fg('muted', ` Agent: ${selected.agent}`), inner));
|
|
1734
2279
|
body.push(this.contentLine(this.theme.fg('muted', ` Status: ${selected.status}`), inner));
|
|
1735
2280
|
if (typeof selected.duration === 'number') {
|
|
1736
|
-
body.push(this.contentLine(this.theme.fg('muted', ` Duration: ${selected.duration}
|
|
2281
|
+
body.push(this.contentLine(this.theme.fg('muted', ` Duration: ${formatElapsedCompact(selected.duration)}`), inner));
|
|
1737
2282
|
}
|
|
1738
2283
|
if (selected.startedAt) {
|
|
1739
2284
|
body.push(this.contentLine(this.theme.fg('muted', ` Started: ${selected.startedAt}`), inner));
|
|
@@ -1754,7 +2299,14 @@ export class HubOverlay {
|
|
|
1754
2299
|
: undefined;
|
|
1755
2300
|
const rendered = this.renderStreamLines(taskBuffer?.output ?? '', taskBuffer?.thinking ?? '', this.showTaskThinking, Math.max(12, inner - 4));
|
|
1756
2301
|
if (rendered.length === 0) {
|
|
1757
|
-
|
|
2302
|
+
if (historySession &&
|
|
2303
|
+
this.detailSessionId &&
|
|
2304
|
+
this.replayInFlight.has(this.detailSessionId)) {
|
|
2305
|
+
body.push(this.contentLine(this.theme.fg('dim', ' (loading replay...)'), inner));
|
|
2306
|
+
}
|
|
2307
|
+
else {
|
|
2308
|
+
body.push(this.contentLine(this.theme.fg('dim', ' (no task output yet)'), inner));
|
|
2309
|
+
}
|
|
1758
2310
|
}
|
|
1759
2311
|
else {
|
|
1760
2312
|
for (const line of rendered) {
|