@pellux/goodvibes-tui 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +4 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +53 -8
- package/src/core/session-recovery.ts +26 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +13 -16
- package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
- package/src/daemon/handlers/inbox/index.ts +2 -1
- package/src/daemon/handlers/inbox/poller.ts +2 -1
- package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
- package/src/daemon/handlers/inbox/providers/email.ts +2 -1
- package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
- package/src/daemon/handlers/triage/integration.ts +2 -1
- package/src/daemon/handlers/triage/pipeline.ts +2 -1
- package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
- package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
- package/src/export/gist-uploader.ts +3 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +2 -1
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +2 -4
- package/src/input/commands/health-runtime.ts +13 -7
- package/src/input/commands/knowledge.ts +2 -1
- package/src/input/commands/runtime-services.ts +40 -10
- package/src/input/handler-command-route.ts +1 -1
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-interactions.ts +2 -1
- package/src/input/handler-modal-routes.ts +2 -1
- package/src/input/handler-onboarding-cloudflare.ts +2 -1
- package/src/input/handler-onboarding.ts +8 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +5 -1
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -60
- package/src/panels/agent-inspector-panel.ts +90 -15
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +149 -72
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +28 -3
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +2 -1
- package/src/panels/builtin/session.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +49 -15
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +87 -65
- package/src/panels/debug-panel.ts +61 -37
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +30 -24
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +42 -42
- package/src/panels/file-preview-panel.ts +11 -3
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +65 -84
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +33 -13
- package/src/panels/memory-panel.ts +7 -5
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +27 -4
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +118 -87
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +44 -24
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +42 -26
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/skills-panel.ts +2 -1
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +36 -14
- package/src/panels/thinking-panel.ts +32 -36
- package/src/panels/token-budget-panel.ts +80 -53
- package/src/panels/tool-inspector-panel.ts +37 -39
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +127 -35
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +53 -147
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/modal-utils.ts +0 -10
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-modal.ts +2 -2
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +31 -31
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/onboarding/apply.ts +4 -3
- package/src/runtime/onboarding/snapshot.ts +4 -3
- package/src/runtime/process-lifecycle.ts +195 -0
- package/src/runtime/services.ts +4 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/verification/live-verifier.ts +4 -3
- package/src/version.ts +1 -1
- package/src/core/context-auto-compact.ts +0 -110
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
|
@@ -77,6 +77,8 @@ const COLOR = {
|
|
|
77
77
|
selected: '#ffee88',
|
|
78
78
|
dimmed: '#555566',
|
|
79
79
|
expandHint: '#445566',
|
|
80
|
+
stalled: '#f59e0b', // amber — stalled-agent warning
|
|
81
|
+
cursorBg: '#1a2233', // timeline cursor row background
|
|
80
82
|
} as const;
|
|
81
83
|
|
|
82
84
|
// ---------------------------------------------------------------------------
|
|
@@ -91,6 +93,15 @@ export interface AgentInspectorPanelDeps {
|
|
|
91
93
|
readonly workingDirectory: string;
|
|
92
94
|
/** Cancel the agent by id. Uses the same orphan-free path as WRFC. Returns true if cancelled. */
|
|
93
95
|
readonly cancelAgent: (agentId: string) => boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Request a compositor repaint. The 500ms refresh timer and other async
|
|
98
|
+
* update paths call this (via markDirty) so live agent output is painted
|
|
99
|
+
* while the main thread is idle — render() is otherwise only invoked on
|
|
100
|
+
* input/turn/resize, which makes a running agent's timeline look frozen.
|
|
101
|
+
* Optional: when omitted the panel still marks dirty and repaints on the
|
|
102
|
+
* next event.
|
|
103
|
+
*/
|
|
104
|
+
readonly requestRender?: () => void;
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
export class AgentInspectorPanel extends BasePanel {
|
|
@@ -115,6 +126,9 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
115
126
|
/** Pending cancel confirmation — subject is the agent id to cancel. */
|
|
116
127
|
private confirmCancel: ConfirmState<string> | null = null;
|
|
117
128
|
|
|
129
|
+
/** True while this panel is the active view — gates async repaint requests. */
|
|
130
|
+
private _active = false;
|
|
131
|
+
|
|
118
132
|
constructor(private readonly deps: AgentInspectorPanelDeps) {
|
|
119
133
|
super('inspector', 'Inspector', 'I', 'agent');
|
|
120
134
|
}
|
|
@@ -131,6 +145,10 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
131
145
|
override markDirty(): void {
|
|
132
146
|
this._cachedRows = null;
|
|
133
147
|
super.markDirty();
|
|
148
|
+
// T17: a bare markDirty() does not repaint — render() only runs on
|
|
149
|
+
// input/turn/resize. While active, ask the compositor for a frame so
|
|
150
|
+
// timer/async refreshes (live streaming output) are not stuck off-screen.
|
|
151
|
+
if (this._active) this.deps.requestRender?.();
|
|
134
152
|
}
|
|
135
153
|
|
|
136
154
|
// -------------------------------------------------------------------------
|
|
@@ -152,11 +170,13 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
152
170
|
// -------------------------------------------------------------------------
|
|
153
171
|
|
|
154
172
|
override onActivate(): void {
|
|
173
|
+
this._active = true;
|
|
155
174
|
this.needsRender = true;
|
|
156
175
|
this._startRefresh();
|
|
157
176
|
}
|
|
158
177
|
|
|
159
178
|
override onDeactivate(): void {
|
|
179
|
+
this._active = false;
|
|
160
180
|
this._stopRefresh();
|
|
161
181
|
}
|
|
162
182
|
|
|
@@ -238,9 +258,11 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
238
258
|
width,
|
|
239
259
|
agents.length === 0 ? ' No agents running' : ' No agent selected',
|
|
240
260
|
agents.length === 0
|
|
241
|
-
? 'Spawn an agent to inspect its live and historical timeline.'
|
|
261
|
+
? 'Spawn an agent to inspect its live and historical timeline, tool calls, and output.'
|
|
242
262
|
: 'Press Tab to cycle through available agents and open one in the inspector.',
|
|
243
|
-
|
|
263
|
+
agents.length === 0
|
|
264
|
+
? [{ command: '/spawn <task>', summary: 'launch an agent, then Tab here to inspect its timeline' }]
|
|
265
|
+
: [{ command: 'Tab', summary: 'select the first available agent' }],
|
|
244
266
|
DEFAULT_PANEL_PALETTE,
|
|
245
267
|
),
|
|
246
268
|
},
|
|
@@ -256,7 +278,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
256
278
|
const now = Date.now();
|
|
257
279
|
const isStalled = this._isAgentStalled(rec, now);
|
|
258
280
|
if (isStalled) {
|
|
259
|
-
summaryLines.push(buildPanelLine(width, [[' STALLED',
|
|
281
|
+
summaryLines.push(buildPanelLine(width, [[' STALLED', COLOR.stalled], [' — no activity for 5+ minutes', DEFAULT_PANEL_PALETTE.dim]]));
|
|
260
282
|
}
|
|
261
283
|
const allRows = this._getCachedRows();
|
|
262
284
|
if (allRows.length === 0) {
|
|
@@ -270,8 +292,12 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
270
292
|
lines: buildEmptyState(
|
|
271
293
|
width,
|
|
272
294
|
' No messages yet',
|
|
273
|
-
|
|
274
|
-
|
|
295
|
+
rec.status === 'running'
|
|
296
|
+
? 'The selected agent is running but has not emitted visible timeline entries yet — live output streams in as it works.'
|
|
297
|
+
: 'The selected agent has not emitted any visible timeline entries.',
|
|
298
|
+
rec.status === 'running'
|
|
299
|
+
? [{ command: 'c', summary: 'cancel this running agent if it appears stalled' }]
|
|
300
|
+
: [{ command: 'Tab', summary: 'cycle to another agent with timeline activity' }],
|
|
275
301
|
DEFAULT_PANEL_PALETTE,
|
|
276
302
|
),
|
|
277
303
|
},
|
|
@@ -379,9 +405,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
379
405
|
const elapsed = (rec.completedAt ?? now) - rec.startedAt;
|
|
380
406
|
const taskPreview = rec.task.split('\n')[0] ?? '';
|
|
381
407
|
const maxTask = Math.max(0, width - 40);
|
|
382
|
-
const taskDisplay = taskPreview
|
|
383
|
-
? taskPreview.slice(0, maxTask - 1) + '\u2026'
|
|
384
|
-
: taskPreview;
|
|
408
|
+
const taskDisplay = truncateDisplay(taskPreview, maxTask);
|
|
385
409
|
return buildPanelLine(width, [
|
|
386
410
|
[' Status ', DEFAULT_PANEL_PALETTE.label],
|
|
387
411
|
[rec.status.toUpperCase(), rec.status === 'running' ? DEFAULT_PANEL_PALETTE.good : rec.status === 'failed' ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
|
|
@@ -407,7 +431,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
407
431
|
row: DisplayRow,
|
|
408
432
|
isCursor: boolean,
|
|
409
433
|
): Line {
|
|
410
|
-
const bg = isCursor ?
|
|
434
|
+
const bg = isCursor ? COLOR.cursorBg : '';
|
|
411
435
|
const ts = shortTime(row.timestamp);
|
|
412
436
|
const { fg, prefix } = agentKindStyle(row.kind, COLOR);
|
|
413
437
|
const hint = row.hasDetail ? (row.expanded ? ' ▾' : ' ▸') : '';
|
|
@@ -441,8 +465,21 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
441
465
|
|
|
442
466
|
// Merge bus messages (live) + JSONL (historical), sorted by timestamp
|
|
443
467
|
const busEntries = this._busToTimeline();
|
|
444
|
-
const merged = [...this.timeline, ...busEntries]
|
|
445
|
-
|
|
468
|
+
const merged = [...this.timeline, ...busEntries];
|
|
469
|
+
|
|
470
|
+
// T13: the JSONL timeline only records coarse per-turn summaries
|
|
471
|
+
// ([assistant] N chars, M tool calls) and never the model's actual text,
|
|
472
|
+
// so a mid-turn agent looks idle here. Surface the selected agent's live
|
|
473
|
+
// streaming output while it runs, and its final output once terminal.
|
|
474
|
+
const liveRec = this.selectedAgentId
|
|
475
|
+
? this.deps.agentManager.getStatus(this.selectedAgentId)
|
|
476
|
+
: null;
|
|
477
|
+
if (liveRec) {
|
|
478
|
+
const liveEntry = this._buildLiveOutputEntry(liveRec);
|
|
479
|
+
if (liveEntry) merged.push(liveEntry);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
merged.sort((a, b) => a.timestamp - b.timestamp);
|
|
446
483
|
|
|
447
484
|
// Deduplicate: bus messages that already appear in JSONL will have
|
|
448
485
|
// approximate timestamps. We just show all — bus msgs tend to have
|
|
@@ -496,15 +533,52 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
496
533
|
kind,
|
|
497
534
|
timestamp: msg.timestamp,
|
|
498
535
|
label: msg.from,
|
|
499
|
-
content: msg.content
|
|
500
|
-
? msg.content.slice(0, 197) + '\u2026'
|
|
501
|
-
: msg.content,
|
|
536
|
+
content: truncateDisplay(msg.content, 200),
|
|
502
537
|
expanded: false,
|
|
503
538
|
} satisfies TimelineEntry);
|
|
504
539
|
}
|
|
505
540
|
return result;
|
|
506
541
|
}
|
|
507
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Build a synthetic timeline entry exposing the selected agent's live model
|
|
545
|
+
* output. While the agent runs this is the streaming token buffer
|
|
546
|
+
* (tail-truncated like AgentDetailModal); once it terminates it is the final
|
|
547
|
+
* assistant text (expandable). Exactly one is produced — the SDK clears
|
|
548
|
+
* streamingContent on completion — so live and final rows never coexist.
|
|
549
|
+
* Returns null when there is nothing to show.
|
|
550
|
+
*/
|
|
551
|
+
private _buildLiveOutputEntry(rec: AgentRecord): TimelineEntry | null {
|
|
552
|
+
const STREAM_MAX_CHARS = 500;
|
|
553
|
+
if (rec.status === 'running' && rec.streamingContent) {
|
|
554
|
+
const sc = rec.streamingContent;
|
|
555
|
+
const truncated = sc.length > STREAM_MAX_CHARS;
|
|
556
|
+
const tail = (truncated ? sc.slice(-STREAM_MAX_CHARS) : sc)
|
|
557
|
+
.replace(/\s+/g, ' ')
|
|
558
|
+
.trim();
|
|
559
|
+
return {
|
|
560
|
+
kind: 'assistant',
|
|
561
|
+
timestamp: Date.now(),
|
|
562
|
+
label: 'streaming',
|
|
563
|
+
content: `(live) ${truncated ? '…' : ''}${tail}`,
|
|
564
|
+
expanded: false,
|
|
565
|
+
} satisfies TimelineEntry;
|
|
566
|
+
}
|
|
567
|
+
if (AGENT_TERMINAL_STATUSES.has(rec.status) && rec.fullOutput) {
|
|
568
|
+
const fo = rec.fullOutput;
|
|
569
|
+
const firstLine = (fo.split('\n').find((l) => l.trim().length > 0) ?? '').trim();
|
|
570
|
+
return {
|
|
571
|
+
kind: 'assistant',
|
|
572
|
+
timestamp: rec.completedAt ?? Date.now(),
|
|
573
|
+
label: 'output',
|
|
574
|
+
content: `(final) ${firstLine}`,
|
|
575
|
+
detail: fo,
|
|
576
|
+
expanded: false,
|
|
577
|
+
} satisfies TimelineEntry;
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
|
|
508
582
|
// -------------------------------------------------------------------------
|
|
509
583
|
// Private — refresh
|
|
510
584
|
// -------------------------------------------------------------------------
|
|
@@ -599,7 +673,8 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
599
673
|
if (!this.selectedAgentId) return;
|
|
600
674
|
const rec = this.deps.agentManager.getStatus(this.selectedAgentId);
|
|
601
675
|
if (!rec || AGENT_TERMINAL_STATUSES.has(rec.status)) return;
|
|
602
|
-
const
|
|
676
|
+
const firstLine = rec.task.split('\n')[0];
|
|
677
|
+
const label = firstLine ? truncateDisplay(firstLine, 40) : rec.id.slice(-8);
|
|
603
678
|
this.confirmCancel = { subject: rec.id, label };
|
|
604
679
|
this.markDirty();
|
|
605
680
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { formatDuration } from '../utils/format-duration.ts';
|
|
2
|
+
|
|
1
3
|
export type AgentInspectorEntryKind = 'user' | 'assistant' | 'tool_call' | 'tool_result' | 'session' | 'error';
|
|
2
4
|
|
|
3
5
|
// ---------------------------------------------------------------------------
|
|
@@ -61,9 +63,7 @@ export function agentStatusColor(status: string, colors: Record<string, string>)
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
export function formatAgentDuration(ms: number): string {
|
|
64
|
-
|
|
65
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
66
|
-
return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
|
|
66
|
+
return formatDuration(ms);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export function formatAgentTime(ts: number): string {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { promises as fsPromises, watch, type FSWatcher } from 'fs';
|
|
2
2
|
import type { Line } from '../types/grid.ts';
|
|
3
|
-
import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
4
3
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
5
4
|
import type { AgentManager, AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
6
5
|
import type { AgentEvent } from '@/runtime/index.ts';
|
|
@@ -29,8 +28,17 @@ import {
|
|
|
29
28
|
const POLL_INTERVAL_MS = 500;
|
|
30
29
|
|
|
31
30
|
export interface AgentLogsPanelDeps {
|
|
32
|
-
readonly agentManager: Pick<AgentManager, 'list'
|
|
31
|
+
readonly agentManager: Pick<AgentManager, 'list'> & Partial<Pick<AgentManager, 'getStatus'>>;
|
|
33
32
|
readonly workingDirectory: string;
|
|
33
|
+
/**
|
|
34
|
+
* Request a compositor repaint. The 500ms poll timer, fs.watch callback and
|
|
35
|
+
* other async update paths call this (via markDirty) so new log lines and
|
|
36
|
+
* live streaming output are painted while the main thread is idle — render()
|
|
37
|
+
* is otherwise only invoked on input/turn/resize, which makes a running
|
|
38
|
+
* agent's log tail look frozen. Optional: when omitted the panel still marks
|
|
39
|
+
* dirty and repaints on the next event.
|
|
40
|
+
*/
|
|
41
|
+
readonly requestRender?: () => void;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
|
|
@@ -47,12 +55,22 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
47
55
|
private allEntries: LogEntry[] = []; // raw parsed JSONL for selected agent
|
|
48
56
|
private filteredEntries: LogEntry[] = []; // after filter applied
|
|
49
57
|
private lastFileSize = 0;
|
|
58
|
+
/**
|
|
59
|
+
* Length of the selected agent's live streamingContent buffer at the last
|
|
60
|
+
* poll. During an LLM turn the record's streamingContent grows token-by-token
|
|
61
|
+
* with NO JSONL append, so the file-size gate alone never repaints the live
|
|
62
|
+
* tail; we also watch this length and markDirty when it advances.
|
|
63
|
+
*/
|
|
64
|
+
private lastStreamLen = 0;
|
|
50
65
|
|
|
51
66
|
// ── Modes ────────────────────────────────────────────────────────────────
|
|
52
67
|
private autoFollow = true;
|
|
53
68
|
private paused = false;
|
|
54
69
|
private filter: FilterType = 'all';
|
|
55
70
|
|
|
71
|
+
/** True while this panel is the active view — gates async repaint requests. */
|
|
72
|
+
private _active = false;
|
|
73
|
+
|
|
56
74
|
// ── Infrastructure ───────────────────────────────────────────────────────
|
|
57
75
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
58
76
|
private fsWatcher: FSWatcher | null = null;
|
|
@@ -81,12 +99,14 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
81
99
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
82
100
|
|
|
83
101
|
override onActivate(): void {
|
|
102
|
+
this._active = true;
|
|
84
103
|
super.onActivate();
|
|
85
104
|
this._refreshAgents();
|
|
86
105
|
this._pollCurrentAgent();
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
override onDeactivate(): void {
|
|
109
|
+
this._active = false;
|
|
90
110
|
super.onDeactivate();
|
|
91
111
|
}
|
|
92
112
|
|
|
@@ -95,6 +115,18 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
95
115
|
this._unsubscribeEvents();
|
|
96
116
|
}
|
|
97
117
|
|
|
118
|
+
/**
|
|
119
|
+
* T17: a bare markDirty() does not repaint — render() only runs on
|
|
120
|
+
* input/turn/resize. While active, ask the compositor for a frame so the
|
|
121
|
+
* background poll / fs.watch updates (new log lines, live streaming output)
|
|
122
|
+
* are not stuck off-screen. Gated on active state so the always-on poller
|
|
123
|
+
* does not force full-frame rebuilds while this panel is hidden.
|
|
124
|
+
*/
|
|
125
|
+
protected override markDirty(): void {
|
|
126
|
+
super.markDirty();
|
|
127
|
+
if (this._active) this.deps.requestRender?.();
|
|
128
|
+
}
|
|
129
|
+
|
|
98
130
|
// ── Input ─────────────────────────────────────────────────────────────────
|
|
99
131
|
|
|
100
132
|
handleInput(key: string): boolean {
|
|
@@ -128,19 +160,28 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
128
160
|
|
|
129
161
|
render(width: number, height: number): Line[] {
|
|
130
162
|
this.needsRender = false;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
163
|
+
// Context-aware footer: when there are no agents only the Tab cycle is
|
|
164
|
+
// meaningless, so hide the per-stream keys; otherwise reflect live state
|
|
165
|
+
// (Space toggles pause→resume, g/G only matter once there is a tail).
|
|
166
|
+
const runningCount = this.agents.filter((a) => a.status === 'running' || a.status === 'pending').length;
|
|
167
|
+
const footerLines = this.agents.length === 0
|
|
168
|
+
? [buildPanelLine(width, [[' Waiting for an agent session to start...', DEFAULT_PANEL_PALETTE.dim]])]
|
|
169
|
+
: [
|
|
170
|
+
buildPanelLine(width, [
|
|
171
|
+
[` ${this.selectedAgentIndex + 1}/${this.agents.length}`, DEFAULT_PANEL_PALETTE.dim],
|
|
172
|
+
[' Tab', DEFAULT_PANEL_PALETTE.info], [' next agent', DEFAULT_PANEL_PALETTE.dim],
|
|
173
|
+
[' Space', DEFAULT_PANEL_PALETTE.info], [this.paused ? ' resume' : ' pause', DEFAULT_PANEL_PALETTE.dim],
|
|
174
|
+
[' f', DEFAULT_PANEL_PALETTE.info], [' filter', DEFAULT_PANEL_PALETTE.dim],
|
|
175
|
+
[' g/G', DEFAULT_PANEL_PALETTE.info], [this.autoFollow ? ' top/follow' : ' top/bottom', DEFAULT_PANEL_PALETTE.dim],
|
|
176
|
+
]),
|
|
177
|
+
];
|
|
139
178
|
|
|
140
179
|
const summaryLines = [
|
|
141
180
|
buildPanelLine(width, [
|
|
142
181
|
[' Agents ', DEFAULT_PANEL_PALETTE.label],
|
|
143
182
|
[String(this.agents.length), DEFAULT_PANEL_PALETTE.value],
|
|
183
|
+
[' Running ', DEFAULT_PANEL_PALETTE.label],
|
|
184
|
+
[String(runningCount), runningCount > 0 ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
|
|
144
185
|
[' Filter ', DEFAULT_PANEL_PALETTE.label],
|
|
145
186
|
[FILTER_LABELS[this.filter], DEFAULT_PANEL_PALETTE.info],
|
|
146
187
|
[' Mode ', DEFAULT_PANEL_PALETTE.label],
|
|
@@ -158,8 +199,11 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
158
199
|
lines: buildEmptyState(
|
|
159
200
|
width,
|
|
160
201
|
' No agents running',
|
|
161
|
-
'Spawn or attach to an agent session and its structured logs
|
|
162
|
-
[
|
|
202
|
+
'Spawn or attach to an agent session and its structured logs stream here in real time.',
|
|
203
|
+
[
|
|
204
|
+
{ command: '/spawn <task>', summary: 'launch an agent and watch its live session log here' },
|
|
205
|
+
{ command: '/inspector', summary: 'open the inspector for a per-agent timeline and tool activity' },
|
|
206
|
+
],
|
|
163
207
|
DEFAULT_PANEL_PALETTE,
|
|
164
208
|
),
|
|
165
209
|
},
|
|
@@ -180,7 +224,17 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
180
224
|
]));
|
|
181
225
|
}
|
|
182
226
|
|
|
183
|
-
|
|
227
|
+
// T13: surface the selected agent's live streaming output (running) or
|
|
228
|
+
// final output (terminal) — the per-turn JSONL tail never carries it. Pull
|
|
229
|
+
// the freshest record via getStatus so the streaming buffer reflects the
|
|
230
|
+
// current turn; the list() snapshot in this.agents only refreshes on
|
|
231
|
+
// spawn/complete/fail events, never mid-stream.
|
|
232
|
+
const liveAgent = selectedAgent
|
|
233
|
+
? (this.deps.agentManager.getStatus?.(selectedAgent.id) ?? selectedAgent)
|
|
234
|
+
: null;
|
|
235
|
+
const liveLines = liveAgent ? this._buildLiveOutputLines(width, liveAgent) : [];
|
|
236
|
+
|
|
237
|
+
if (this.filteredEntries.length === 0 && liveLines.length === 0) {
|
|
184
238
|
return buildPanelWorkspace(width, height, {
|
|
185
239
|
title: ' Agents',
|
|
186
240
|
intro: 'View-only live session stream for running agents, with per-agent switching and filtered event tails.',
|
|
@@ -202,9 +256,13 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
202
256
|
});
|
|
203
257
|
}
|
|
204
258
|
|
|
259
|
+
const scrollableLines = [
|
|
260
|
+
...this.filteredEntries.map((entry) => this._renderEntry(entry, width)),
|
|
261
|
+
...liveLines,
|
|
262
|
+
];
|
|
205
263
|
const focusIndex = this.autoFollow
|
|
206
|
-
? Math.max(0,
|
|
207
|
-
: Math.min(this.selectedIndex, Math.max(0,
|
|
264
|
+
? Math.max(0, scrollableLines.length - 1)
|
|
265
|
+
: Math.min(this.selectedIndex, Math.max(0, scrollableLines.length - 1));
|
|
208
266
|
const summarySection = { title: 'Summary', lines: summaryLines } as const;
|
|
209
267
|
const agentsSection = { title: 'Agents', lines: [selectorLine] } as const;
|
|
210
268
|
const logStreamSection = resolveScrollablePanelSection(width, height, {
|
|
@@ -214,7 +272,7 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
214
272
|
beforeSections: [summarySection, agentsSection],
|
|
215
273
|
section: {
|
|
216
274
|
title: 'Log Stream',
|
|
217
|
-
scrollableLines
|
|
275
|
+
scrollableLines,
|
|
218
276
|
selectedIndex: focusIndex,
|
|
219
277
|
scrollOffset: this.scrollStart,
|
|
220
278
|
minRows: 8,
|
|
@@ -264,28 +322,50 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
264
322
|
const agent = this._selectedAgent();
|
|
265
323
|
if (!agent) return;
|
|
266
324
|
|
|
325
|
+
// Detect live streaming growth even when no JSONL is written this tick.
|
|
326
|
+
// During an LLM turn the record's streamingContent grows token-by-token
|
|
327
|
+
// with no session-log append, so the file-size gate below would never fire
|
|
328
|
+
// and the live tail (_buildLiveOutputLines) would never repaint.
|
|
329
|
+
let streamChanged = false;
|
|
330
|
+
const live = this.deps.agentManager.getStatus?.(agent.id);
|
|
331
|
+
if (live) {
|
|
332
|
+
const streamLen = (live.streamingContent ?? '').length;
|
|
333
|
+
if (streamLen !== this.lastStreamLen) {
|
|
334
|
+
this.lastStreamLen = streamLen;
|
|
335
|
+
streamChanged = true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
267
339
|
const sessionFile = this._sessionFilePath(agent.id);
|
|
340
|
+
let fileExists = true;
|
|
268
341
|
try {
|
|
269
342
|
await fsPromises.access(sessionFile);
|
|
270
343
|
} catch {
|
|
271
|
-
|
|
344
|
+
fileExists = false;
|
|
272
345
|
}
|
|
273
346
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
347
|
+
if (fileExists) {
|
|
348
|
+
try {
|
|
349
|
+
const content = await fsPromises.readFile(sessionFile, 'utf-8');
|
|
350
|
+
if (content.length !== this.lastFileSize) {
|
|
351
|
+
this.lastFileSize = content.length;
|
|
352
|
+
|
|
353
|
+
// Re-parse all lines (simple: no partial-line tracking needed at 500ms)
|
|
354
|
+
this.allEntries = parseAgentJsonl(content);
|
|
355
|
+
this._applyFilter();
|
|
356
|
+
if (this.autoFollow) {
|
|
357
|
+
this.selectedIndex = Math.max(0, this.filteredEntries.length - 1);
|
|
358
|
+
}
|
|
359
|
+
this.markDirty();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
// Non-fatal: file may be mid-write
|
|
284
364
|
}
|
|
285
|
-
this.markDirty();
|
|
286
|
-
} catch {
|
|
287
|
-
// Non-fatal: file may be mid-write
|
|
288
365
|
}
|
|
366
|
+
|
|
367
|
+
// File unchanged or absent, but the live stream advanced — repaint the tail.
|
|
368
|
+
if (streamChanged) this.markDirty();
|
|
289
369
|
}
|
|
290
370
|
|
|
291
371
|
// ── Private: fs.watch (supplemental) ─────────────────────────────────────
|
|
@@ -382,6 +462,7 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
382
462
|
this.allEntries = [];
|
|
383
463
|
this.filteredEntries = [];
|
|
384
464
|
this.lastFileSize = 0;
|
|
465
|
+
this.lastStreamLen = 0;
|
|
385
466
|
this.selectedIndex = 0;
|
|
386
467
|
this.scrollStart = 0;
|
|
387
468
|
this.autoFollow = true;
|
|
@@ -414,6 +495,7 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
414
495
|
this.allEntries = [];
|
|
415
496
|
this.filteredEntries = [];
|
|
416
497
|
this.lastFileSize = 0;
|
|
498
|
+
this.lastStreamLen = 0;
|
|
417
499
|
this.markDirty();
|
|
418
500
|
return;
|
|
419
501
|
}
|
|
@@ -473,22 +555,6 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
473
555
|
|
|
474
556
|
// ── Private: rendering helpers ─────────────────────────────────────────────
|
|
475
557
|
|
|
476
|
-
/** Top header bar: title + filter label + mode indicators */
|
|
477
|
-
private _renderHeader(width: number): Line {
|
|
478
|
-
const title = ' Agent Logs ';
|
|
479
|
-
const filterLabel = `[${FILTER_LABELS[this.filter]}] `;
|
|
480
|
-
const pause = this.paused ? ' PAUSED ' : '';
|
|
481
|
-
const follow = this.autoFollow ? ' AUTO-FOLLOW ' : '';
|
|
482
|
-
const keyhints = ' Tab:next Space:pause f:filter g/G:scroll ';
|
|
483
|
-
return buildStyledPanelLine(width, [
|
|
484
|
-
{ text: title, fg: COLOR.header_accent, bold: true },
|
|
485
|
-
{ text: filterLabel, fg: COLOR.filter_active },
|
|
486
|
-
{ text: pause, fg: COLOR.paused },
|
|
487
|
-
{ text: follow, fg: COLOR.auto_follow },
|
|
488
|
-
{ text: keyhints, fg: COLOR.header_label },
|
|
489
|
-
]);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
558
|
/** Agent selector bar: shows running agents with cycle indicator */
|
|
493
559
|
private _renderAgentSelector(width: number): Line {
|
|
494
560
|
const prefix = ' Agents: ';
|
|
@@ -512,32 +578,6 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
512
578
|
return buildStyledPanelLine(width, segments);
|
|
513
579
|
}
|
|
514
580
|
|
|
515
|
-
private _renderNoAgents(width: number): Line {
|
|
516
|
-
const msg = ' No agents running. ';
|
|
517
|
-
return buildStyledPanelLine(width, [{ text: msg, fg: COLOR.dim }]);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
private _renderSeparator(width: number): Line {
|
|
521
|
-
return buildStyledPanelLine(width, [{ text: '─'.repeat(width), fg: COLOR.separator }]);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
private _renderEmpty(width: number, bodyHeight: number): Line[] {
|
|
525
|
-
const lines: Line[] = [];
|
|
526
|
-
const msg = this.agents.length === 0
|
|
527
|
-
? ' No agents running '
|
|
528
|
-
: ` No ${this.filter === 'all' ? '' : this.filter + ' '}log entries yet `;
|
|
529
|
-
const offset = Math.max(0, Math.floor((width - msg.length) / 2));
|
|
530
|
-
const textLine = buildStyledPanelLine(width, [
|
|
531
|
-
{ text: ' '.repeat(offset), fg: COLOR.dim },
|
|
532
|
-
{ text: msg, fg: COLOR.dim },
|
|
533
|
-
]);
|
|
534
|
-
lines.push(textLine);
|
|
535
|
-
while (lines.length < bodyHeight) {
|
|
536
|
-
lines.push(createEmptyLine(width));
|
|
537
|
-
}
|
|
538
|
-
return lines;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
581
|
private _renderEntry(entry: LogEntry, width: number): Line {
|
|
542
582
|
// Indent non-session entries
|
|
543
583
|
const prefix = entry.type === 'session_start' ? '' : ' ';
|
|
@@ -545,6 +585,43 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
545
585
|
return buildStyledPanelLine(width, [{ text: fullText, fg: entry.color, bold: entry.bold }]);
|
|
546
586
|
}
|
|
547
587
|
|
|
588
|
+
/**
|
|
589
|
+
* Build trailing lines exposing the selected agent's live model output. The
|
|
590
|
+
* JSONL tail is written per-turn, so while an agent is mid-turn its actual
|
|
591
|
+
* work is invisible here; this surfaces the streaming token buffer
|
|
592
|
+
* (tail-truncated like AgentDetailModal) for a running agent, and the final
|
|
593
|
+
* assistant text once it terminates. Empty when there is nothing live to show.
|
|
594
|
+
*/
|
|
595
|
+
private _buildLiveOutputLines(width: number, agent: AgentRecord): Line[] {
|
|
596
|
+
const STREAM_MAX_CHARS = 500;
|
|
597
|
+
if (agent.status === 'running' && agent.streamingContent) {
|
|
598
|
+
const sc = agent.streamingContent;
|
|
599
|
+
const truncated = sc.length > STREAM_MAX_CHARS;
|
|
600
|
+
const display = truncated ? sc.slice(-STREAM_MAX_CHARS) : sc;
|
|
601
|
+
const header = truncated
|
|
602
|
+
? ` streaming (last ${STREAM_MAX_CHARS} of ${sc.length} chars):`
|
|
603
|
+
: ' streaming:';
|
|
604
|
+
const lines: Line[] = [
|
|
605
|
+
buildStyledPanelLine(width, [{ text: header, fg: COLOR.auto_follow, bold: true }]),
|
|
606
|
+
];
|
|
607
|
+
for (const raw of display.split('\n')) {
|
|
608
|
+
lines.push(buildStyledPanelLine(width, [{ text: ` ${raw}`, fg: COLOR.assistant }]));
|
|
609
|
+
}
|
|
610
|
+
return lines;
|
|
611
|
+
}
|
|
612
|
+
if (agent.status !== 'running' && agent.status !== 'pending' && agent.fullOutput) {
|
|
613
|
+
const fo = agent.fullOutput;
|
|
614
|
+
const lines: Line[] = [
|
|
615
|
+
buildStyledPanelLine(width, [{ text: ' output:', fg: COLOR.header_accent, bold: true }]),
|
|
616
|
+
];
|
|
617
|
+
for (const raw of fo.split('\n')) {
|
|
618
|
+
lines.push(buildStyledPanelLine(width, [{ text: ` ${raw}`, fg: COLOR.assistant }]));
|
|
619
|
+
}
|
|
620
|
+
return lines;
|
|
621
|
+
}
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
|
|
548
625
|
private _agentStatusColor(status: AgentRecord['status']): string {
|
|
549
626
|
switch (status) {
|
|
550
627
|
case 'running': return COLOR.agent_running;
|