@pellux/goodvibes-tui 0.25.0 → 0.27.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 +22 -3
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +2422 -1040
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +49 -2
- package/src/core/session-recovery.ts +24 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +12 -16
- package/src/daemon/{calendar → handlers/calendar}/caldav-client.ts +29 -24
- package/src/daemon/handlers/calendar/index.ts +316 -0
- package/src/daemon/handlers/context.ts +29 -0
- package/src/daemon/handlers/contracts.ts +77 -0
- package/src/daemon/{channels → handlers}/drafts/draft-store.ts +114 -50
- package/src/daemon/handlers/drafts/index.ts +17 -0
- package/src/daemon/handlers/drafts/register.ts +331 -0
- package/src/daemon/handlers/email/config.ts +164 -0
- package/src/daemon/handlers/email/index.ts +43 -0
- package/src/daemon/handlers/email/read-handlers.ts +80 -0
- package/src/daemon/handlers/email/runtime.ts +140 -0
- package/src/daemon/handlers/email/validation.ts +147 -0
- package/src/daemon/handlers/email/write-handlers.ts +133 -0
- package/src/daemon/handlers/errors.ts +18 -0
- package/src/daemon/{channels → handlers}/inbox/cursor-store.ts +58 -66
- package/src/daemon/handlers/inbox/index.ts +211 -0
- package/src/daemon/{channels → handlers}/inbox/mapping.ts +8 -6
- package/src/daemon/{channels → handlers}/inbox/poller.ts +6 -5
- package/src/daemon/{channels → handlers}/inbox/provider-adapter.ts +14 -10
- package/src/daemon/{channels → handlers}/inbox/providers/discord.ts +10 -11
- package/src/daemon/{channels → handlers}/inbox/providers/email.ts +2 -1
- package/src/daemon/{channels → handlers}/inbox/providers/imap-client.ts +1 -1
- package/src/daemon/{channels → handlers}/inbox/providers/route-util.ts +4 -3
- package/src/daemon/{channels → handlers}/inbox/providers/slack.ts +11 -12
- package/src/daemon/handlers/index.ts +107 -0
- package/src/daemon/handlers/register.ts +161 -0
- package/src/daemon/{remote → handlers/remote}/backends/cloud-terminal.ts +8 -3
- package/src/daemon/{remote → handlers/remote}/backends/docker.ts +2 -3
- package/src/daemon/handlers/remote/backends/index.ts +40 -0
- package/src/daemon/{remote → handlers/remote}/backends/process-runner.ts +16 -40
- package/src/daemon/{remote → handlers/remote}/backends/ssh.ts +8 -3
- package/src/daemon/{remote → handlers/remote}/backends/types.ts +29 -3
- package/src/daemon/{remote → handlers/remote}/dispatcher.ts +27 -6
- package/src/daemon/handlers/remote/index.ts +119 -0
- package/src/daemon/{remote → handlers/remote}/peer-registry.ts +47 -11
- package/src/daemon/handlers/remote/service.ts +191 -0
- package/src/daemon/{channels → handlers}/routing/inbox-bridge.ts +33 -20
- package/src/daemon/handlers/routing/index.ts +261 -0
- package/src/daemon/{channels → handlers}/routing/route-store.ts +57 -16
- package/src/daemon/{channels → handlers}/routing/routing-resolver.ts +1 -1
- package/src/daemon/{operator → handlers}/sqlite-store.ts +5 -5
- package/src/daemon/handlers/triage/index.ts +57 -0
- package/src/daemon/handlers/triage/integration.ts +213 -0
- package/src/daemon/{triage → handlers/triage}/pipeline.ts +60 -71
- package/src/daemon/{triage → handlers/triage}/scorer.ts +21 -21
- package/src/daemon/handlers/triage/tagger/discord.ts +187 -0
- package/src/daemon/handlers/triage/tagger/imap.ts +384 -0
- package/src/daemon/handlers/triage/tagger/index.ts +184 -0
- package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
- package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
- package/src/daemon/handlers/triage/types.ts +50 -0
- 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/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-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-ui-state.ts +1 -1
- package/src/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -59
- package/src/panels/agent-inspector-panel.ts +72 -2
- package/src/panels/agent-logs-panel.ts +127 -19
- package/src/panels/base-panel.ts +2 -1
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +1 -0
- package/src/panels/builtin/session.ts +1 -1
- package/src/panels/context-visualizer-panel.ts +24 -14
- package/src/panels/cost-tracker-panel.ts +7 -13
- package/src/panels/debug-panel.ts +11 -22
- package/src/panels/docs-panel.ts +14 -24
- package/src/panels/file-explorer-panel.ts +2 -1
- package/src/panels/file-preview-panel.ts +2 -1
- package/src/panels/git-panel.ts +10 -17
- package/src/panels/marketplace-panel.ts +2 -1
- package/src/panels/memory-panel.ts +2 -1
- package/src/panels/project-planning-panel.ts +5 -4
- package/src/panels/provider-health-panel.ts +56 -67
- package/src/panels/schedule-panel.ts +4 -7
- package/src/panels/session-browser-panel.ts +13 -21
- package/src/panels/skills-panel.ts +2 -1
- package/src/panels/tasks-panel.ts +2 -7
- package/src/panels/thinking-panel.ts +6 -11
- package/src/panels/token-budget-panel.ts +31 -15
- package/src/panels/tool-inspector-panel.ts +10 -18
- package/src/panels/work-plan-panel.ts +2 -1
- package/src/panels/wrfc-panel.ts +37 -35
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/modal-utils.ts +0 -10
- package/src/renderer/process-modal.ts +2 -2
- 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 +52 -36
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- 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/daemon/calendar/index.ts +0 -52
- package/src/daemon/calendar/register.ts +0 -527
- package/src/daemon/channels/drafts/index.ts +0 -22
- package/src/daemon/channels/drafts/register.ts +0 -449
- package/src/daemon/channels/inbox/index.ts +0 -58
- package/src/daemon/channels/inbox/register.ts +0 -247
- package/src/daemon/channels/routing/index.ts +0 -39
- package/src/daemon/channels/routing/register.ts +0 -296
- package/src/daemon/email/index.ts +0 -68
- package/src/daemon/email/register.ts +0 -715
- package/src/daemon/operator/index.ts +0 -43
- package/src/daemon/operator/register-helper.ts +0 -150
- package/src/daemon/operator/surfaces.ts +0 -137
- package/src/daemon/operator/types.ts +0 -207
- package/src/daemon/remote/backends/index.ts +0 -34
- package/src/daemon/remote/index.ts +0 -74
- package/src/daemon/remote/register.ts +0 -411
- package/src/daemon/triage/index.ts +0 -59
- package/src/daemon/triage/integration.ts +0 -179
- package/src/daemon/triage/register.ts +0 -231
- package/src/daemon/triage/tagger.ts +0 -777
- /package/src/daemon/{calendar → handlers/calendar}/ics.ts +0 -0
- /package/src/daemon/{operator/credential-store.ts → handlers/credentials.ts} +0 -0
- /package/src/daemon/{email → handlers/email}/imap-connector.ts +0 -0
- /package/src/daemon/{email → handlers/email}/imap-parsing.ts +0 -0
- /package/src/daemon/{email → handlers/email}/smtp-connector.ts +0 -0
- /package/src/daemon/{remote → handlers/remote}/backends/local-process.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
2
|
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config';
|
|
3
|
-
import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils';
|
|
3
|
+
import { summarizeError, openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
4
|
import { getProviderIdFromModel } from '../config/provider-model.ts';
|
|
5
5
|
import { buildProviderAccountSnapshot } from '@/runtime/index.ts';
|
|
6
6
|
import { OnboardingWizardController, type OnboardingWizardAction, type OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
|
|
@@ -258,7 +258,7 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
258
258
|
deleteWizardProgress(handler.uiServices.environment.shellPaths);
|
|
259
259
|
} catch (markerError) {
|
|
260
260
|
handler.commandContext?.print?.(
|
|
261
|
-
`Onboarding check marker could not be written: ${
|
|
261
|
+
`Onboarding check marker could not be written: ${summarizeError(markerError)}`,
|
|
262
262
|
);
|
|
263
263
|
}
|
|
264
264
|
const activationVerification = await handler.restartOnboardingExternalServicesIfNeeded(request);
|
|
@@ -275,7 +275,7 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
275
275
|
severity: 'error',
|
|
276
276
|
title: 'Apply failed',
|
|
277
277
|
summary: 'The wizard could not persist these settings. No service restart was attempted.',
|
|
278
|
-
messages: [
|
|
278
|
+
messages: [summarizeError(error)],
|
|
279
279
|
});
|
|
280
280
|
return;
|
|
281
281
|
} finally {
|
|
@@ -346,7 +346,7 @@ export async function refreshOnboardingHydrationForHandler(handler: InputHandler
|
|
|
346
346
|
handler.requestRender();
|
|
347
347
|
} catch (error) {
|
|
348
348
|
if (!handler.onboardingWizard.active || hydrationSerial !== handler.onboardingHydrationSerial) return;
|
|
349
|
-
const message =
|
|
349
|
+
const message = summarizeError(error);
|
|
350
350
|
handler.onboardingWizard.failRuntimeHydration(message);
|
|
351
351
|
handler.requestRender();
|
|
352
352
|
}
|
|
@@ -405,7 +405,7 @@ export async function handleOpenAiSubscriptionStartForHandler(handler: InputHand
|
|
|
405
405
|
listener?.close();
|
|
406
406
|
handler.commandContext?.print?.([
|
|
407
407
|
'OpenAI subscription sign-in could not start.',
|
|
408
|
-
` ${
|
|
408
|
+
` ${summarizeError(error)}`,
|
|
409
409
|
].join('\n'));
|
|
410
410
|
handler.requestRender();
|
|
411
411
|
} finally {
|
|
@@ -449,7 +449,7 @@ export async function completeOpenAiSubscriptionFromListenerForHandler(
|
|
|
449
449
|
} catch (error) {
|
|
450
450
|
handler.commandContext?.print?.([
|
|
451
451
|
'OpenAI subscription listener could not complete automatically.',
|
|
452
|
-
` listener: ${
|
|
452
|
+
` listener: ${summarizeError(error)}`,
|
|
453
453
|
'Paste the callback code or URL into the OpenAI callback field to finish in onboarding.',
|
|
454
454
|
].join('\n'));
|
|
455
455
|
handler.requestRender();
|
|
@@ -501,7 +501,7 @@ export async function handleOpenAiSubscriptionFinishForHandler(handler: InputHan
|
|
|
501
501
|
} catch (error) {
|
|
502
502
|
handler.commandContext?.print?.([
|
|
503
503
|
'OpenAI subscription sign-in could not finish.',
|
|
504
|
-
` ${
|
|
504
|
+
` ${summarizeError(error)}`,
|
|
505
505
|
].join('\n'));
|
|
506
506
|
handler.requestRender();
|
|
507
507
|
} finally {
|
|
@@ -663,7 +663,7 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
|
|
|
663
663
|
return [{
|
|
664
664
|
id: 'runtime:activation-restart',
|
|
665
665
|
status: 'fail',
|
|
666
|
-
message: `Background services could not restart: ${
|
|
666
|
+
message: `Background services could not restart: ${summarizeError(error)}`,
|
|
667
667
|
target: 'service',
|
|
668
668
|
}];
|
|
669
669
|
}
|
|
@@ -345,7 +345,7 @@ export function handleHistorySearchToken(state: HistorySearchRouteState, token:
|
|
|
345
345
|
if (token.logicalName === 'escape' || (token.ctrl && token.logicalName === 'g')) {
|
|
346
346
|
state.prompt = state.historySearch.cancel();
|
|
347
347
|
state.cursorPos = state.prompt.length;
|
|
348
|
-
} else if (token.logicalName === 'return') {
|
|
348
|
+
} else if (token.logicalName === 'enter' || token.logicalName === 'return') {
|
|
349
349
|
state.prompt = state.historySearch.accept();
|
|
350
350
|
state.cursorPos = state.prompt.length;
|
|
351
351
|
} else if (token.logicalName === 'backspace') {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
2
|
import type { CommandContext } from './command-registry.ts';
|
|
3
3
|
import type { SelectionItem } from './selection-modal.ts';
|
|
4
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
5
|
|
|
5
6
|
function getStreamingTtsProviders(ctx: CommandContext): Array<{ id: string; label: string; capabilities: readonly string[] }> {
|
|
6
7
|
const registry = ctx.platform.voiceProviderRegistry;
|
|
@@ -94,7 +95,7 @@ export async function openTtsVoicePicker(ctx: CommandContext, providerArg?: stri
|
|
|
94
95
|
});
|
|
95
96
|
return true;
|
|
96
97
|
} catch (error) {
|
|
97
|
-
ctx.print(`Unable to list TTS voices: ${
|
|
98
|
+
ctx.print(`Unable to list TTS voices: ${summarizeError(error)}`);
|
|
98
99
|
return true;
|
|
99
100
|
}
|
|
100
101
|
}
|
package/src/main.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { renderToolCallBlock } from './renderer/tool-call.ts';
|
|
|
52
52
|
import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
|
|
53
53
|
import { attachSpokenTurnModelRouting, createSpokenTurnInputOptions } from './audio/spoken-turn-model-routing.ts';
|
|
54
54
|
import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/terminal-output-guard.ts';
|
|
55
|
+
import { installProcessLifecycle } from './runtime/process-lifecycle.ts';
|
|
55
56
|
import { buildCommandArgsHint } from './input/command-args-hint.ts';
|
|
56
57
|
import { summarizeRunningAgents } from './renderer/process-summary.ts';
|
|
57
58
|
import { formatUserFacingErrorLine } from './core/format-user-error.ts';
|
|
@@ -165,6 +166,9 @@ async function main() {
|
|
|
165
166
|
|
|
166
167
|
let scrollTop = 0;
|
|
167
168
|
let scrollLocked = true;
|
|
169
|
+
// Cached from the overlay-aware clamp the renderer computes (conversation-layout) each
|
|
170
|
+
// frame so scroll() clamps against exactly what is displayed, not a footer estimate.
|
|
171
|
+
let lastMaxScroll: number | null = null;
|
|
168
172
|
// Stream and tool-timer state; mutated by wireStreamEventMetrics handlers, read during render.
|
|
169
173
|
const streamMetrics: StreamMetrics = {
|
|
170
174
|
startTime: 0,
|
|
@@ -187,18 +191,23 @@ async function main() {
|
|
|
187
191
|
if (input.onboardingWizard.active) return stdout.rows || 24;
|
|
188
192
|
const promptLines: number = input.getVisiblePromptLineCount(getPromptContentWidth());
|
|
189
193
|
const currentModel = providerRegistry.getCurrentModel();
|
|
190
|
-
|
|
194
|
+
const contextWindow = providerRegistry.getContextWindowForModel(currentModel);
|
|
195
|
+
return (stdout.rows || 24) - 2 - estimateShellFooterHeight(promptLines, contextWindow);
|
|
191
196
|
};
|
|
192
197
|
|
|
193
198
|
const scroll = (delta: number) => {
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
// Prefer the last clamp the renderer computed (overlay- and real-footer-aware).
|
|
200
|
+
// The footer estimate is only a fallback for the pre-first-render frame.
|
|
201
|
+
const maxScroll = lastMaxScroll ?? Math.max(0, conversation.history.getLineCount() - getViewportHeight());
|
|
196
202
|
scrollTop = Math.max(0, Math.min(scrollTop + delta, maxScroll));
|
|
197
203
|
// Re-lock if user scrolled to bottom, otherwise unlock
|
|
198
204
|
scrollLocked = scrollTop >= maxScroll;
|
|
199
205
|
};
|
|
200
206
|
|
|
201
207
|
const scrollToEnd = (vHeight: number) => {
|
|
208
|
+
// Respect a manual scroll-up by the user: only auto-follow the tail when parked at the
|
|
209
|
+
// bottom (scrollLocked). submitInput re-locks on new input so turns resume following.
|
|
210
|
+
if (!scrollLocked) return;
|
|
202
211
|
scrollTop = Math.max(0, conversation.history.getLineCount() - vHeight);
|
|
203
212
|
};
|
|
204
213
|
|
|
@@ -207,59 +216,31 @@ async function main() {
|
|
|
207
216
|
let stopSpokenOutputForExit: (() => void) | null = null;
|
|
208
217
|
let recoveryPending = false;
|
|
209
218
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
render();
|
|
236
|
-
};
|
|
237
|
-
const resizeHandler = (): void => {
|
|
238
|
-
input.setContentWidth(getPromptContentWidth());
|
|
239
|
-
compositor.resetDiff();
|
|
240
|
-
render();
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const exitApp = (): void => {
|
|
244
|
-
stopSpokenOutputForExit?.();
|
|
245
|
-
unsubs.forEach(fn => fn());
|
|
246
|
-
const snapshot = conversation.toJSON() as { messages: Array<import('./core/conversation.ts').ConversationMessageSnapshot>; timestamp?: number };
|
|
247
|
-
ctx.shutdown({ ...snapshot, ...buildPersistedSessionContext(snapshot.messages, conversation.getTitleSource(), buildSessionContinuityHints()) }).catch((err) => {
|
|
248
|
-
logger.debug('ctx.shutdown error during exitApp (non-fatal)', { error: summarizeError(err) });
|
|
249
|
-
});
|
|
250
|
-
if (recoveryInterval !== null) { clearInterval(recoveryInterval); recoveryInterval = null; }
|
|
251
|
-
deleteRecoveryFile({ homeDirectory });
|
|
252
|
-
stdin.removeAllListeners('data');
|
|
253
|
-
stdout.removeListener('resize', resizeHandler);
|
|
254
|
-
process.removeListener('SIGINT', sigintHandler);
|
|
255
|
-
process.removeListener('unhandledRejection', unhandledRejectionHandler);
|
|
256
|
-
const exitScreen = cli.flags.noAltScreen ? CLEAR_SCREEN : CLEAR_SCREEN + ALT_SCREEN_EXIT;
|
|
257
|
-
allowTerminalWrite(() => stdout.write(PASTE_DISABLE + KEYBOARD_EXT_DISABLE + MOUSE_DISABLE + CURSOR_SHOW + exitScreen));
|
|
258
|
-
terminalOutputGuard.dispose();
|
|
259
|
-
stdin.setRawMode(false);
|
|
260
|
-
process.exit(0);
|
|
261
|
-
};
|
|
262
|
-
|
|
219
|
+
const lifecycle = installProcessLifecycle({
|
|
220
|
+
stdin,
|
|
221
|
+
stdout,
|
|
222
|
+
ctx,
|
|
223
|
+
noAltScreen: cli.flags.noAltScreen,
|
|
224
|
+
ansi: { CLEAR_SCREEN, ALT_SCREEN_EXIT, PASTE_DISABLE, KEYBOARD_EXT_DISABLE, MOUSE_DISABLE, CURSOR_SHOW },
|
|
225
|
+
getInput: () => input,
|
|
226
|
+
render: () => render(),
|
|
227
|
+
getPromptContentWidth,
|
|
228
|
+
getTerminalOutputGuard: () => terminalOutputGuard,
|
|
229
|
+
buildSessionContinuityHints,
|
|
230
|
+
unsubs,
|
|
231
|
+
getRecoveryInterval: () => recoveryInterval,
|
|
232
|
+
setRecoveryInterval: (value) => { recoveryInterval = value; },
|
|
233
|
+
getStopSpokenOutputForExit: () => stopSpokenOutputForExit,
|
|
234
|
+
});
|
|
235
|
+
const {
|
|
236
|
+
exitApp,
|
|
237
|
+
resizeHandler,
|
|
238
|
+
sigintHandler,
|
|
239
|
+
unhandledRejectionHandler,
|
|
240
|
+
uncaughtExceptionHandler,
|
|
241
|
+
terminationSignalHandler,
|
|
242
|
+
exitListener,
|
|
243
|
+
} = lifecycle;
|
|
263
244
|
commandContext.exit = exitApp;
|
|
264
245
|
|
|
265
246
|
const spokenTurns = wireSpokenTurnRuntime({
|
|
@@ -456,6 +437,9 @@ async function main() {
|
|
|
456
437
|
|
|
457
438
|
// Cache the current model for consistent values across the entire render frame
|
|
458
439
|
const currentModel = providerRegistry.getCurrentModel();
|
|
440
|
+
// Resolve the effective context window (provider_api / configured_cap overrides) once,
|
|
441
|
+
// so the footer meter, footer-height, and context inspector agree with the Tokens panel.
|
|
442
|
+
const contextWindow = providerRegistry.getContextWindowForModel(currentModel);
|
|
459
443
|
const sessionSnapshot = uiServices.readModels.session.getSnapshot();
|
|
460
444
|
const agentSnapshot = uiServices.readModels.agents.getSnapshot();
|
|
461
445
|
|
|
@@ -481,7 +465,7 @@ async function main() {
|
|
|
481
465
|
const maintenanceStatus = evaluateSessionMaintenance({
|
|
482
466
|
configManager,
|
|
483
467
|
currentTokens: orchestrator.lastInputTokens,
|
|
484
|
-
contextWindow
|
|
468
|
+
contextWindow,
|
|
485
469
|
sessionMemoryCount: ctx.services.sessionMemoryStore.list().length,
|
|
486
470
|
});
|
|
487
471
|
const contextStatusHint = buildContextStatusHint({
|
|
@@ -505,7 +489,7 @@ async function main() {
|
|
|
505
489
|
toolCount: toolRegistry.list().length,
|
|
506
490
|
workingDir,
|
|
507
491
|
provider: runtime.provider,
|
|
508
|
-
contextWindow
|
|
492
|
+
contextWindow,
|
|
509
493
|
contextStatusHint,
|
|
510
494
|
// behavior.autoCompactThreshold is stored as a percent integer (e.g. 80);
|
|
511
495
|
// the meter expects a fraction [0..1]. Clamp to [0,1] to guard nonsense values.
|
|
@@ -586,6 +570,7 @@ async function main() {
|
|
|
586
570
|
overlayRows,
|
|
587
571
|
});
|
|
588
572
|
scrollTop = conversationViewport.nextScrollTop;
|
|
573
|
+
lastMaxScroll = conversationViewport.maxScroll;
|
|
589
574
|
let viewport = conversationViewport.viewport;
|
|
590
575
|
|
|
591
576
|
if (orchestrator.isThinking) {
|
|
@@ -628,7 +613,7 @@ async function main() {
|
|
|
628
613
|
keybindingsManager: ctx.services.keybindingsManager,
|
|
629
614
|
conversationWidth,
|
|
630
615
|
viewportHeight: vHeight,
|
|
631
|
-
contextWindow
|
|
616
|
+
contextWindow,
|
|
632
617
|
});
|
|
633
618
|
|
|
634
619
|
// Panel composite data
|
|
@@ -732,6 +717,14 @@ async function main() {
|
|
|
732
717
|
}
|
|
733
718
|
});
|
|
734
719
|
|
|
720
|
+
// Register terminal-restoring crash/termination handlers BEFORE entering raw mode so a
|
|
721
|
+
// throw during terminal setup or the initial render still restores the terminal; the
|
|
722
|
+
// 'exit' listener is the final safety net for any process.exit path.
|
|
723
|
+
process.on('uncaughtException', uncaughtExceptionHandler);
|
|
724
|
+
process.on('SIGTERM', terminationSignalHandler);
|
|
725
|
+
process.on('SIGHUP', terminationSignalHandler);
|
|
726
|
+
process.on('exit', exitListener);
|
|
727
|
+
|
|
735
728
|
// --- Terminal setup ---
|
|
736
729
|
stdin.setRawMode(true);
|
|
737
730
|
stdin.resume();
|
|
@@ -91,6 +91,15 @@ export interface AgentInspectorPanelDeps {
|
|
|
91
91
|
readonly workingDirectory: string;
|
|
92
92
|
/** Cancel the agent by id. Uses the same orphan-free path as WRFC. Returns true if cancelled. */
|
|
93
93
|
readonly cancelAgent: (agentId: string) => boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Request a compositor repaint. The 500ms refresh timer and other async
|
|
96
|
+
* update paths call this (via markDirty) so live agent output is painted
|
|
97
|
+
* while the main thread is idle — render() is otherwise only invoked on
|
|
98
|
+
* input/turn/resize, which makes a running agent's timeline look frozen.
|
|
99
|
+
* Optional: when omitted the panel still marks dirty and repaints on the
|
|
100
|
+
* next event.
|
|
101
|
+
*/
|
|
102
|
+
readonly requestRender?: () => void;
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
export class AgentInspectorPanel extends BasePanel {
|
|
@@ -115,6 +124,9 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
115
124
|
/** Pending cancel confirmation — subject is the agent id to cancel. */
|
|
116
125
|
private confirmCancel: ConfirmState<string> | null = null;
|
|
117
126
|
|
|
127
|
+
/** True while this panel is the active view — gates async repaint requests. */
|
|
128
|
+
private _active = false;
|
|
129
|
+
|
|
118
130
|
constructor(private readonly deps: AgentInspectorPanelDeps) {
|
|
119
131
|
super('inspector', 'Inspector', 'I', 'agent');
|
|
120
132
|
}
|
|
@@ -131,6 +143,10 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
131
143
|
override markDirty(): void {
|
|
132
144
|
this._cachedRows = null;
|
|
133
145
|
super.markDirty();
|
|
146
|
+
// T17: a bare markDirty() does not repaint — render() only runs on
|
|
147
|
+
// input/turn/resize. While active, ask the compositor for a frame so
|
|
148
|
+
// timer/async refreshes (live streaming output) are not stuck off-screen.
|
|
149
|
+
if (this._active) this.deps.requestRender?.();
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
// -------------------------------------------------------------------------
|
|
@@ -152,11 +168,13 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
152
168
|
// -------------------------------------------------------------------------
|
|
153
169
|
|
|
154
170
|
override onActivate(): void {
|
|
171
|
+
this._active = true;
|
|
155
172
|
this.needsRender = true;
|
|
156
173
|
this._startRefresh();
|
|
157
174
|
}
|
|
158
175
|
|
|
159
176
|
override onDeactivate(): void {
|
|
177
|
+
this._active = false;
|
|
160
178
|
this._stopRefresh();
|
|
161
179
|
}
|
|
162
180
|
|
|
@@ -441,8 +459,21 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
441
459
|
|
|
442
460
|
// Merge bus messages (live) + JSONL (historical), sorted by timestamp
|
|
443
461
|
const busEntries = this._busToTimeline();
|
|
444
|
-
const merged = [...this.timeline, ...busEntries]
|
|
445
|
-
|
|
462
|
+
const merged = [...this.timeline, ...busEntries];
|
|
463
|
+
|
|
464
|
+
// T13: the JSONL timeline only records coarse per-turn summaries
|
|
465
|
+
// ([assistant] N chars, M tool calls) and never the model's actual text,
|
|
466
|
+
// so a mid-turn agent looks idle here. Surface the selected agent's live
|
|
467
|
+
// streaming output while it runs, and its final output once terminal.
|
|
468
|
+
const liveRec = this.selectedAgentId
|
|
469
|
+
? this.deps.agentManager.getStatus(this.selectedAgentId)
|
|
470
|
+
: null;
|
|
471
|
+
if (liveRec) {
|
|
472
|
+
const liveEntry = this._buildLiveOutputEntry(liveRec);
|
|
473
|
+
if (liveEntry) merged.push(liveEntry);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
merged.sort((a, b) => a.timestamp - b.timestamp);
|
|
446
477
|
|
|
447
478
|
// Deduplicate: bus messages that already appear in JSONL will have
|
|
448
479
|
// approximate timestamps. We just show all — bus msgs tend to have
|
|
@@ -505,6 +536,45 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
505
536
|
return result;
|
|
506
537
|
}
|
|
507
538
|
|
|
539
|
+
/**
|
|
540
|
+
* Build a synthetic timeline entry exposing the selected agent's live model
|
|
541
|
+
* output. While the agent runs this is the streaming token buffer
|
|
542
|
+
* (tail-truncated like AgentDetailModal); once it terminates it is the final
|
|
543
|
+
* assistant text (expandable). Exactly one is produced — the SDK clears
|
|
544
|
+
* streamingContent on completion — so live and final rows never coexist.
|
|
545
|
+
* Returns null when there is nothing to show.
|
|
546
|
+
*/
|
|
547
|
+
private _buildLiveOutputEntry(rec: AgentRecord): TimelineEntry | null {
|
|
548
|
+
const STREAM_MAX_CHARS = 500;
|
|
549
|
+
if (rec.status === 'running' && rec.streamingContent) {
|
|
550
|
+
const sc = rec.streamingContent;
|
|
551
|
+
const truncated = sc.length > STREAM_MAX_CHARS;
|
|
552
|
+
const tail = (truncated ? sc.slice(-STREAM_MAX_CHARS) : sc)
|
|
553
|
+
.replace(/\s+/g, ' ')
|
|
554
|
+
.trim();
|
|
555
|
+
return {
|
|
556
|
+
kind: 'assistant',
|
|
557
|
+
timestamp: Date.now(),
|
|
558
|
+
label: 'streaming',
|
|
559
|
+
content: `(live) ${truncated ? '…' : ''}${tail}`,
|
|
560
|
+
expanded: false,
|
|
561
|
+
} satisfies TimelineEntry;
|
|
562
|
+
}
|
|
563
|
+
if (AGENT_TERMINAL_STATUSES.has(rec.status) && rec.fullOutput) {
|
|
564
|
+
const fo = rec.fullOutput;
|
|
565
|
+
const firstLine = (fo.split('\n').find((l) => l.trim().length > 0) ?? '').trim();
|
|
566
|
+
return {
|
|
567
|
+
kind: 'assistant',
|
|
568
|
+
timestamp: rec.completedAt ?? Date.now(),
|
|
569
|
+
label: 'output',
|
|
570
|
+
content: `(final) ${firstLine}`,
|
|
571
|
+
detail: fo,
|
|
572
|
+
expanded: false,
|
|
573
|
+
} satisfies TimelineEntry;
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
508
578
|
// -------------------------------------------------------------------------
|
|
509
579
|
// Private — refresh
|
|
510
580
|
// -------------------------------------------------------------------------
|
|
@@ -29,8 +29,17 @@ import {
|
|
|
29
29
|
const POLL_INTERVAL_MS = 500;
|
|
30
30
|
|
|
31
31
|
export interface AgentLogsPanelDeps {
|
|
32
|
-
readonly agentManager: Pick<AgentManager, 'list'
|
|
32
|
+
readonly agentManager: Pick<AgentManager, 'list'> & Partial<Pick<AgentManager, 'getStatus'>>;
|
|
33
33
|
readonly workingDirectory: string;
|
|
34
|
+
/**
|
|
35
|
+
* Request a compositor repaint. The 500ms poll timer, fs.watch callback and
|
|
36
|
+
* other async update paths call this (via markDirty) so new log lines and
|
|
37
|
+
* live streaming output are painted while the main thread is idle — render()
|
|
38
|
+
* is otherwise only invoked on input/turn/resize, which makes a running
|
|
39
|
+
* agent's log tail look frozen. Optional: when omitted the panel still marks
|
|
40
|
+
* dirty and repaints on the next event.
|
|
41
|
+
*/
|
|
42
|
+
readonly requestRender?: () => void;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
|
|
@@ -47,12 +56,22 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
47
56
|
private allEntries: LogEntry[] = []; // raw parsed JSONL for selected agent
|
|
48
57
|
private filteredEntries: LogEntry[] = []; // after filter applied
|
|
49
58
|
private lastFileSize = 0;
|
|
59
|
+
/**
|
|
60
|
+
* Length of the selected agent's live streamingContent buffer at the last
|
|
61
|
+
* poll. During an LLM turn the record's streamingContent grows token-by-token
|
|
62
|
+
* with NO JSONL append, so the file-size gate alone never repaints the live
|
|
63
|
+
* tail; we also watch this length and markDirty when it advances.
|
|
64
|
+
*/
|
|
65
|
+
private lastStreamLen = 0;
|
|
50
66
|
|
|
51
67
|
// ── Modes ────────────────────────────────────────────────────────────────
|
|
52
68
|
private autoFollow = true;
|
|
53
69
|
private paused = false;
|
|
54
70
|
private filter: FilterType = 'all';
|
|
55
71
|
|
|
72
|
+
/** True while this panel is the active view — gates async repaint requests. */
|
|
73
|
+
private _active = false;
|
|
74
|
+
|
|
56
75
|
// ── Infrastructure ───────────────────────────────────────────────────────
|
|
57
76
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
58
77
|
private fsWatcher: FSWatcher | null = null;
|
|
@@ -81,12 +100,14 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
81
100
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
82
101
|
|
|
83
102
|
override onActivate(): void {
|
|
103
|
+
this._active = true;
|
|
84
104
|
super.onActivate();
|
|
85
105
|
this._refreshAgents();
|
|
86
106
|
this._pollCurrentAgent();
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
override onDeactivate(): void {
|
|
110
|
+
this._active = false;
|
|
90
111
|
super.onDeactivate();
|
|
91
112
|
}
|
|
92
113
|
|
|
@@ -95,6 +116,18 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
95
116
|
this._unsubscribeEvents();
|
|
96
117
|
}
|
|
97
118
|
|
|
119
|
+
/**
|
|
120
|
+
* T17: a bare markDirty() does not repaint — render() only runs on
|
|
121
|
+
* input/turn/resize. While active, ask the compositor for a frame so the
|
|
122
|
+
* background poll / fs.watch updates (new log lines, live streaming output)
|
|
123
|
+
* are not stuck off-screen. Gated on active state so the always-on poller
|
|
124
|
+
* does not force full-frame rebuilds while this panel is hidden.
|
|
125
|
+
*/
|
|
126
|
+
protected override markDirty(): void {
|
|
127
|
+
super.markDirty();
|
|
128
|
+
if (this._active) this.deps.requestRender?.();
|
|
129
|
+
}
|
|
130
|
+
|
|
98
131
|
// ── Input ─────────────────────────────────────────────────────────────────
|
|
99
132
|
|
|
100
133
|
handleInput(key: string): boolean {
|
|
@@ -180,7 +213,17 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
180
213
|
]));
|
|
181
214
|
}
|
|
182
215
|
|
|
183
|
-
|
|
216
|
+
// T13: surface the selected agent's live streaming output (running) or
|
|
217
|
+
// final output (terminal) — the per-turn JSONL tail never carries it. Pull
|
|
218
|
+
// the freshest record via getStatus so the streaming buffer reflects the
|
|
219
|
+
// current turn; the list() snapshot in this.agents only refreshes on
|
|
220
|
+
// spawn/complete/fail events, never mid-stream.
|
|
221
|
+
const liveAgent = selectedAgent
|
|
222
|
+
? (this.deps.agentManager.getStatus?.(selectedAgent.id) ?? selectedAgent)
|
|
223
|
+
: null;
|
|
224
|
+
const liveLines = liveAgent ? this._buildLiveOutputLines(width, liveAgent) : [];
|
|
225
|
+
|
|
226
|
+
if (this.filteredEntries.length === 0 && liveLines.length === 0) {
|
|
184
227
|
return buildPanelWorkspace(width, height, {
|
|
185
228
|
title: ' Agents',
|
|
186
229
|
intro: 'View-only live session stream for running agents, with per-agent switching and filtered event tails.',
|
|
@@ -202,9 +245,13 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
202
245
|
});
|
|
203
246
|
}
|
|
204
247
|
|
|
248
|
+
const scrollableLines = [
|
|
249
|
+
...this.filteredEntries.map((entry) => this._renderEntry(entry, width)),
|
|
250
|
+
...liveLines,
|
|
251
|
+
];
|
|
205
252
|
const focusIndex = this.autoFollow
|
|
206
|
-
? Math.max(0,
|
|
207
|
-
: Math.min(this.selectedIndex, Math.max(0,
|
|
253
|
+
? Math.max(0, scrollableLines.length - 1)
|
|
254
|
+
: Math.min(this.selectedIndex, Math.max(0, scrollableLines.length - 1));
|
|
208
255
|
const summarySection = { title: 'Summary', lines: summaryLines } as const;
|
|
209
256
|
const agentsSection = { title: 'Agents', lines: [selectorLine] } as const;
|
|
210
257
|
const logStreamSection = resolveScrollablePanelSection(width, height, {
|
|
@@ -214,7 +261,7 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
214
261
|
beforeSections: [summarySection, agentsSection],
|
|
215
262
|
section: {
|
|
216
263
|
title: 'Log Stream',
|
|
217
|
-
scrollableLines
|
|
264
|
+
scrollableLines,
|
|
218
265
|
selectedIndex: focusIndex,
|
|
219
266
|
scrollOffset: this.scrollStart,
|
|
220
267
|
minRows: 8,
|
|
@@ -264,28 +311,50 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
264
311
|
const agent = this._selectedAgent();
|
|
265
312
|
if (!agent) return;
|
|
266
313
|
|
|
314
|
+
// Detect live streaming growth even when no JSONL is written this tick.
|
|
315
|
+
// During an LLM turn the record's streamingContent grows token-by-token
|
|
316
|
+
// with no session-log append, so the file-size gate below would never fire
|
|
317
|
+
// and the live tail (_buildLiveOutputLines) would never repaint.
|
|
318
|
+
let streamChanged = false;
|
|
319
|
+
const live = this.deps.agentManager.getStatus?.(agent.id);
|
|
320
|
+
if (live) {
|
|
321
|
+
const streamLen = (live.streamingContent ?? '').length;
|
|
322
|
+
if (streamLen !== this.lastStreamLen) {
|
|
323
|
+
this.lastStreamLen = streamLen;
|
|
324
|
+
streamChanged = true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
267
328
|
const sessionFile = this._sessionFilePath(agent.id);
|
|
329
|
+
let fileExists = true;
|
|
268
330
|
try {
|
|
269
331
|
await fsPromises.access(sessionFile);
|
|
270
332
|
} catch {
|
|
271
|
-
|
|
333
|
+
fileExists = false;
|
|
272
334
|
}
|
|
273
335
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
336
|
+
if (fileExists) {
|
|
337
|
+
try {
|
|
338
|
+
const content = await fsPromises.readFile(sessionFile, 'utf-8');
|
|
339
|
+
if (content.length !== this.lastFileSize) {
|
|
340
|
+
this.lastFileSize = content.length;
|
|
341
|
+
|
|
342
|
+
// Re-parse all lines (simple: no partial-line tracking needed at 500ms)
|
|
343
|
+
this.allEntries = parseAgentJsonl(content);
|
|
344
|
+
this._applyFilter();
|
|
345
|
+
if (this.autoFollow) {
|
|
346
|
+
this.selectedIndex = Math.max(0, this.filteredEntries.length - 1);
|
|
347
|
+
}
|
|
348
|
+
this.markDirty();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
// Non-fatal: file may be mid-write
|
|
284
353
|
}
|
|
285
|
-
this.markDirty();
|
|
286
|
-
} catch {
|
|
287
|
-
// Non-fatal: file may be mid-write
|
|
288
354
|
}
|
|
355
|
+
|
|
356
|
+
// File unchanged or absent, but the live stream advanced — repaint the tail.
|
|
357
|
+
if (streamChanged) this.markDirty();
|
|
289
358
|
}
|
|
290
359
|
|
|
291
360
|
// ── Private: fs.watch (supplemental) ─────────────────────────────────────
|
|
@@ -382,6 +451,7 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
382
451
|
this.allEntries = [];
|
|
383
452
|
this.filteredEntries = [];
|
|
384
453
|
this.lastFileSize = 0;
|
|
454
|
+
this.lastStreamLen = 0;
|
|
385
455
|
this.selectedIndex = 0;
|
|
386
456
|
this.scrollStart = 0;
|
|
387
457
|
this.autoFollow = true;
|
|
@@ -414,6 +484,7 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
414
484
|
this.allEntries = [];
|
|
415
485
|
this.filteredEntries = [];
|
|
416
486
|
this.lastFileSize = 0;
|
|
487
|
+
this.lastStreamLen = 0;
|
|
417
488
|
this.markDirty();
|
|
418
489
|
return;
|
|
419
490
|
}
|
|
@@ -545,6 +616,43 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
545
616
|
return buildStyledPanelLine(width, [{ text: fullText, fg: entry.color, bold: entry.bold }]);
|
|
546
617
|
}
|
|
547
618
|
|
|
619
|
+
/**
|
|
620
|
+
* Build trailing lines exposing the selected agent's live model output. The
|
|
621
|
+
* JSONL tail is written per-turn, so while an agent is mid-turn its actual
|
|
622
|
+
* work is invisible here; this surfaces the streaming token buffer
|
|
623
|
+
* (tail-truncated like AgentDetailModal) for a running agent, and the final
|
|
624
|
+
* assistant text once it terminates. Empty when there is nothing live to show.
|
|
625
|
+
*/
|
|
626
|
+
private _buildLiveOutputLines(width: number, agent: AgentRecord): Line[] {
|
|
627
|
+
const STREAM_MAX_CHARS = 500;
|
|
628
|
+
if (agent.status === 'running' && agent.streamingContent) {
|
|
629
|
+
const sc = agent.streamingContent;
|
|
630
|
+
const truncated = sc.length > STREAM_MAX_CHARS;
|
|
631
|
+
const display = truncated ? sc.slice(-STREAM_MAX_CHARS) : sc;
|
|
632
|
+
const header = truncated
|
|
633
|
+
? ` streaming (last ${STREAM_MAX_CHARS} of ${sc.length} chars):`
|
|
634
|
+
: ' streaming:';
|
|
635
|
+
const lines: Line[] = [
|
|
636
|
+
buildStyledPanelLine(width, [{ text: header, fg: COLOR.auto_follow, bold: true }]),
|
|
637
|
+
];
|
|
638
|
+
for (const raw of display.split('\n')) {
|
|
639
|
+
lines.push(buildStyledPanelLine(width, [{ text: ` ${raw}`, fg: COLOR.assistant }]));
|
|
640
|
+
}
|
|
641
|
+
return lines;
|
|
642
|
+
}
|
|
643
|
+
if (agent.status !== 'running' && agent.status !== 'pending' && agent.fullOutput) {
|
|
644
|
+
const fo = agent.fullOutput;
|
|
645
|
+
const lines: Line[] = [
|
|
646
|
+
buildStyledPanelLine(width, [{ text: ' output:', fg: COLOR.header_accent, bold: true }]),
|
|
647
|
+
];
|
|
648
|
+
for (const raw of fo.split('\n')) {
|
|
649
|
+
lines.push(buildStyledPanelLine(width, [{ text: ` ${raw}`, fg: COLOR.assistant }]));
|
|
650
|
+
}
|
|
651
|
+
return lines;
|
|
652
|
+
}
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
|
|
548
656
|
private _agentStatusColor(status: AgentRecord['status']): string {
|
|
549
657
|
switch (status) {
|
|
550
658
|
case 'running': return COLOR.agent_running;
|
package/src/panels/base-panel.ts
CHANGED
|
@@ -142,7 +142,8 @@ export abstract class BasePanel implements Panel {
|
|
|
142
142
|
*/
|
|
143
143
|
protected renderLoadingLine(width: number, frame = 0): Line | null {
|
|
144
144
|
if (this.loadingState !== 'loading') return null;
|
|
145
|
-
const
|
|
145
|
+
const idx = (frame || Math.floor(Date.now() / 100)) % SPINNER_FRAMES.length;
|
|
146
|
+
const spinner = SPINNER_FRAMES[idx] ?? SPINNER_FRAMES[0]!;
|
|
146
147
|
const text = ` ${spinner} ${this._loadingLabel}`;
|
|
147
148
|
return UIFactory.stringToLine(fitDisplay(text, width), width, { fg: '135', bold: true });
|
|
148
149
|
}
|