@pellux/goodvibes-tui 0.21.0 → 0.22.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 +23 -0
- package/README.md +1 -1
- package/package.json +2 -1
- package/src/cli/completions/generate.ts +4 -8
- package/src/cli/entrypoint.ts +6 -0
- package/src/cli/parser.ts +17 -0
- package/src/cli/types.ts +2 -0
- package/src/config/goodvibes-home-audit.ts +2 -0
- package/src/core/context-auto-compact.ts +77 -0
- package/src/core/turn-event-wiring.ts +124 -0
- package/src/daemon/cli.ts +5 -0
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/provider.ts +57 -3
- package/src/input/commands/session-workflow.ts +8 -16
- package/src/input/commands/session.ts +70 -20
- package/src/input/commands.ts +0 -2
- package/src/input/handler-modal-routes.ts +37 -0
- package/src/input/handler-modal-token-routes.ts +19 -5
- package/src/input/handler-onboarding.ts +18 -0
- package/src/input/handler.ts +1 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +10 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +14 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +6 -0
- package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
- package/src/input/settings-modal-behavior.ts +5 -0
- package/src/input/settings-modal-data.ts +77 -3
- package/src/input/settings-modal-mutations.ts +3 -0
- package/src/input/settings-modal-reset.ts +154 -0
- package/src/input/settings-modal.ts +55 -13
- package/src/main.ts +36 -28
- package/src/panels/agent-inspector-panel.ts +120 -18
- package/src/panels/agent-inspector-shared.ts +29 -0
- package/src/panels/builtin/development.ts +1 -0
- package/src/panels/builtin/knowledge.ts +14 -13
- package/src/panels/builtin/operations.ts +22 -1
- package/src/panels/builtin/shared.ts +7 -0
- package/src/panels/cockpit-panel.ts +123 -3
- package/src/panels/cockpit-read-model.ts +232 -0
- package/src/panels/index.ts +1 -1
- package/src/panels/knowledge-graph-panel.ts +84 -0
- package/src/panels/memory-panel.ts +370 -40
- package/src/panels/session-maintenance.ts +66 -15
- package/src/renderer/agent-detail-modal.ts +107 -3
- package/src/renderer/context-status-hint.ts +54 -0
- package/src/renderer/settings-modal.ts +14 -3
- package/src/renderer/shell-surface.ts +10 -0
- package/src/runtime/bootstrap-command-parts.ts +4 -0
- package/src/runtime/bootstrap-core.ts +24 -0
- package/src/runtime/bootstrap-shell.ts +11 -0
- package/src/runtime/bootstrap.ts +7 -0
- package/src/runtime/services.ts +6 -1
- package/src/version.ts +1 -1
- package/src/panels/knowledge-panel.ts +0 -343
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings-modal-reset — pure reset helpers for SettingsModal.
|
|
3
|
+
*
|
|
4
|
+
* These functions encapsulate the three reset operations:
|
|
5
|
+
* - resetSelected: reset the currently selected setting to its schema default
|
|
6
|
+
* - initiateResetCategory: arm the category-reset confirmation gate
|
|
7
|
+
* - initiateResetAll: arm the reset-all confirmation gate
|
|
8
|
+
* - handleResetConfirmKey: route a keypress through the active gate
|
|
9
|
+
*
|
|
10
|
+
* Each function takes its dependencies as explicit arguments rather than
|
|
11
|
+
* accessing class-level state. resetCategoryConfirm and resetAllConfirm
|
|
12
|
+
* remain public class fields on SettingsModal — the renderer reads them
|
|
13
|
+
* directly to decide the footer state.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
17
|
+
import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
18
|
+
import { buildGoodVibesSecretKey, isSecretConfigKey } from '../config/secret-config.ts';
|
|
19
|
+
import type { SettingEntry, SettingsCategory } from './settings-modal-types.ts';
|
|
20
|
+
import type { SettingsSecretsManager } from './settings-modal-secrets.ts';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// resetSelected
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export function resetSelected({
|
|
27
|
+
editingMode,
|
|
28
|
+
hasConfigManager,
|
|
29
|
+
selected,
|
|
30
|
+
secretsManager,
|
|
31
|
+
setValue,
|
|
32
|
+
}: {
|
|
33
|
+
editingMode: boolean;
|
|
34
|
+
hasConfigManager: boolean;
|
|
35
|
+
selected: SettingEntry | null;
|
|
36
|
+
secretsManager: SettingsSecretsManager | null;
|
|
37
|
+
setValue: (key: ConfigKey, value: unknown) => void;
|
|
38
|
+
}): { key: ConfigKey; value: unknown } | null {
|
|
39
|
+
if (editingMode || !hasConfigManager) return null;
|
|
40
|
+
if (!selected) return null;
|
|
41
|
+
const key = selected.setting.key as ConfigKey;
|
|
42
|
+
setValue(key, selected.setting.default);
|
|
43
|
+
if (isSecretConfigKey(key) && secretsManager) {
|
|
44
|
+
void secretsManager.delete(buildGoodVibesSecretKey(key), { scope: 'user' }).catch((error) => {
|
|
45
|
+
logger.error('SettingsModal: failed to clear secret while resetting setting', { key, error: summarizeError(error) });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return { key, value: selected.setting.default };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// initiateResetCategory
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
export function initiateResetCategory({
|
|
56
|
+
hasConfigManager,
|
|
57
|
+
currentCategory,
|
|
58
|
+
setResetCategoryConfirm,
|
|
59
|
+
setResetAllConfirm,
|
|
60
|
+
}: {
|
|
61
|
+
hasConfigManager: boolean;
|
|
62
|
+
currentCategory: string;
|
|
63
|
+
setResetCategoryConfirm: (value: { readonly subject: string } | null) => void;
|
|
64
|
+
setResetAllConfirm: (value: { readonly subject: 'all' } | null) => void;
|
|
65
|
+
}): void {
|
|
66
|
+
if (!hasConfigManager) return;
|
|
67
|
+
setResetCategoryConfirm({ subject: currentCategory });
|
|
68
|
+
setResetAllConfirm(null);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// initiateResetAll
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
export function initiateResetAll({
|
|
76
|
+
hasConfigManager,
|
|
77
|
+
setResetCategoryConfirm,
|
|
78
|
+
setResetAllConfirm,
|
|
79
|
+
}: {
|
|
80
|
+
hasConfigManager: boolean;
|
|
81
|
+
setResetCategoryConfirm: (value: { readonly subject: string } | null) => void;
|
|
82
|
+
setResetAllConfirm: (value: { readonly subject: 'all' } | null) => void;
|
|
83
|
+
}): void {
|
|
84
|
+
if (!hasConfigManager) return;
|
|
85
|
+
setResetAllConfirm({ subject: 'all' });
|
|
86
|
+
setResetCategoryConfirm(null);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// handleResetConfirmKey
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
export type ResetConfirmKeyResult =
|
|
94
|
+
| { result: 'confirmed'; entries: ReadonlyArray<{ key: string; value: unknown }> }
|
|
95
|
+
| 'cancelled'
|
|
96
|
+
| 'absorbed'
|
|
97
|
+
| 'inactive';
|
|
98
|
+
|
|
99
|
+
export function handleResetConfirmKey({
|
|
100
|
+
key,
|
|
101
|
+
resetCategoryConfirm,
|
|
102
|
+
resetAllConfirm,
|
|
103
|
+
hasConfigManager,
|
|
104
|
+
currentItems,
|
|
105
|
+
groups,
|
|
106
|
+
setValue,
|
|
107
|
+
setResetCategoryConfirm,
|
|
108
|
+
setResetAllConfirm,
|
|
109
|
+
}: {
|
|
110
|
+
key: string;
|
|
111
|
+
resetCategoryConfirm: { readonly subject: string } | null;
|
|
112
|
+
resetAllConfirm: { readonly subject: 'all' } | null;
|
|
113
|
+
hasConfigManager: boolean;
|
|
114
|
+
currentItems: () => SettingEntry[];
|
|
115
|
+
groups: Map<SettingsCategory, SettingEntry[]>;
|
|
116
|
+
setValue: (key: ConfigKey, value: unknown) => void;
|
|
117
|
+
setResetCategoryConfirm: (value: { readonly subject: string } | null) => void;
|
|
118
|
+
setResetAllConfirm: (value: { readonly subject: 'all' } | null) => void;
|
|
119
|
+
}): ResetConfirmKeyResult {
|
|
120
|
+
const gate = resetCategoryConfirm ?? resetAllConfirm;
|
|
121
|
+
if (!gate || !hasConfigManager) return 'inactive';
|
|
122
|
+
|
|
123
|
+
if (key === 'enter' || key === 'y') {
|
|
124
|
+
const entries: Array<{ key: string; value: unknown }> = [];
|
|
125
|
+
if (resetCategoryConfirm) {
|
|
126
|
+
// Reset all settings in the current category to defaults.
|
|
127
|
+
const items = currentItems();
|
|
128
|
+
for (const item of items) {
|
|
129
|
+
setValue(item.setting.key as ConfigKey, item.setting.default);
|
|
130
|
+
entries.push({ key: item.setting.key, value: item.setting.default });
|
|
131
|
+
}
|
|
132
|
+
setResetCategoryConfirm(null);
|
|
133
|
+
} else {
|
|
134
|
+
// Reset ALL settings across all categories to defaults.
|
|
135
|
+
for (const [, items] of groups) {
|
|
136
|
+
for (const item of items) {
|
|
137
|
+
setValue(item.setting.key as ConfigKey, item.setting.default);
|
|
138
|
+
entries.push({ key: item.setting.key, value: item.setting.default });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
setResetAllConfirm(null);
|
|
142
|
+
}
|
|
143
|
+
return { result: 'confirmed', entries };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (key === 'escape' || key === 'n') {
|
|
147
|
+
setResetCategoryConfirm(null);
|
|
148
|
+
setResetAllConfirm(null);
|
|
149
|
+
return 'cancelled';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// All other keys are absorbed while the gate is active.
|
|
153
|
+
return 'absorbed';
|
|
154
|
+
}
|
|
@@ -19,7 +19,7 @@ import type { ModelPickerTarget } from './model-picker.ts';
|
|
|
19
19
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
20
20
|
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
21
21
|
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
22
|
-
import {
|
|
22
|
+
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
23
23
|
import {
|
|
24
24
|
getNumericAdjustmentMeta,
|
|
25
25
|
modelPickerLaunchForKey,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
33
33
|
import type { FlagState } from '@/runtime/index.ts';
|
|
34
34
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
import {
|
|
37
37
|
SETTINGS_CATEGORIES,
|
|
38
38
|
SETTINGS_CATEGORY_GROUPS,
|
|
@@ -60,6 +60,13 @@ import {
|
|
|
60
60
|
persistFlagState,
|
|
61
61
|
type SettingAppliedCallback,
|
|
62
62
|
} from './settings-modal-mutations.ts';
|
|
63
|
+
import {
|
|
64
|
+
resetSelected as _resetSelected,
|
|
65
|
+
initiateResetCategory as _initiateResetCategory,
|
|
66
|
+
initiateResetAll as _initiateResetAll,
|
|
67
|
+
handleResetConfirmKey as _handleResetConfirmKey,
|
|
68
|
+
type ResetConfirmKeyResult,
|
|
69
|
+
} from './settings-modal-reset.ts';
|
|
63
70
|
|
|
64
71
|
export interface SettingsModalChange {
|
|
65
72
|
readonly key: ConfigKey;
|
|
@@ -124,6 +131,11 @@ export class SettingsModal {
|
|
|
124
131
|
/** Provider awaiting explicit logout confirmation, if any. */
|
|
125
132
|
public subscriptionLogoutConfirmationTarget: string | null = null;
|
|
126
133
|
|
|
134
|
+
/** Pending category-reset confirmation gate, or null when inactive. */
|
|
135
|
+
public resetCategoryConfirm: { readonly subject: string } | null = null;
|
|
136
|
+
/** Pending reset-all confirmation gate, or null when inactive. */
|
|
137
|
+
public resetAllConfirm: { readonly subject: 'all' } | null = null;
|
|
138
|
+
|
|
127
139
|
/** Settings grouped by category. */
|
|
128
140
|
public groups: Map<SettingsCategory, SettingEntry[]> = new Map();
|
|
129
141
|
|
|
@@ -674,17 +686,47 @@ export class SettingsModal {
|
|
|
674
686
|
}
|
|
675
687
|
|
|
676
688
|
resetSelected(): { key: ConfigKey; value: unknown } | null {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
689
|
+
return _resetSelected({
|
|
690
|
+
editingMode: this.editingMode,
|
|
691
|
+
hasConfigManager: this.configManager !== null,
|
|
692
|
+
selected: this.getSelected(),
|
|
693
|
+
secretsManager: this.secretsManager,
|
|
694
|
+
setValue: (key, value) => this._setValue(key, value),
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Arm a category-reset confirmation gate for the current category. */
|
|
699
|
+
initiateResetCategory(): void {
|
|
700
|
+
_initiateResetCategory({
|
|
701
|
+
hasConfigManager: this.configManager !== null,
|
|
702
|
+
currentCategory: this.currentCategory,
|
|
703
|
+
setResetCategoryConfirm: (v) => { this.resetCategoryConfirm = v; },
|
|
704
|
+
setResetAllConfirm: (v) => { this.resetAllConfirm = v; },
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** Arm a reset-all confirmation gate. */
|
|
709
|
+
initiateResetAll(): void {
|
|
710
|
+
_initiateResetAll({
|
|
711
|
+
hasConfigManager: this.configManager !== null,
|
|
712
|
+
setResetCategoryConfirm: (v) => { this.resetCategoryConfirm = v; },
|
|
713
|
+
setResetAllConfirm: (v) => { this.resetAllConfirm = v; },
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Route a key through the active reset confirm gate. See ResetConfirmKeyResult for the return contract. */
|
|
718
|
+
handleResetConfirmKey(key: string): ResetConfirmKeyResult {
|
|
719
|
+
return _handleResetConfirmKey({
|
|
720
|
+
key,
|
|
721
|
+
resetCategoryConfirm: this.resetCategoryConfirm,
|
|
722
|
+
resetAllConfirm: this.resetAllConfirm,
|
|
723
|
+
hasConfigManager: this.configManager !== null,
|
|
724
|
+
currentItems: () => this._currentItems(),
|
|
725
|
+
groups: this.groups,
|
|
726
|
+
setValue: (k, value) => this._setValue(k, value),
|
|
727
|
+
setResetCategoryConfirm: (v) => { this.resetCategoryConfirm = v; },
|
|
728
|
+
setResetAllConfirm: (v) => { this.resetAllConfirm = v; },
|
|
729
|
+
});
|
|
688
730
|
}
|
|
689
731
|
|
|
690
732
|
/** Handle a keystroke in edit mode: regular chars appended, Backspace removes last char. */
|
package/src/main.ts
CHANGED
|
@@ -33,12 +33,10 @@ import { renderPanelTabBar } from './renderer/panel-tab-bar.ts';
|
|
|
33
33
|
import { bootstrapRuntime } from './runtime/bootstrap.ts';
|
|
34
34
|
import type { BootstrapContext } from './runtime/bootstrap.ts';
|
|
35
35
|
import type { HITLMode } from '@pellux/goodvibes-sdk/platform/state';
|
|
36
|
-
import type { HookPhase, HookCategory, HookEventPath } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
37
36
|
import {
|
|
38
37
|
checkRecoveryFile,
|
|
39
38
|
deleteRecoveryFile,
|
|
40
39
|
loadRecoveryConversation,
|
|
41
|
-
persistConversation,
|
|
42
40
|
writeRecoveryFile,
|
|
43
41
|
} from '@/runtime/index.ts';
|
|
44
42
|
import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
|
|
@@ -57,6 +55,9 @@ import { buildCommandArgsHint } from './input/command-args-hint.ts';
|
|
|
57
55
|
import { summarizeRunningAgents } from './renderer/process-summary.ts';
|
|
58
56
|
import { formatUserFacingErrorLine } from './core/format-user-error.ts';
|
|
59
57
|
import { wireStreamEventMetrics, type StreamMetrics } from './core/stream-event-wiring.ts';
|
|
58
|
+
import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
|
|
59
|
+
import { buildContextStatusHint } from './renderer/context-status-hint.ts';
|
|
60
|
+
import { evaluateSessionMaintenance } from './panels/session-maintenance.ts';
|
|
60
61
|
|
|
61
62
|
const ALT_SCREEN_ENTER = '\x1b[?1049h';
|
|
62
63
|
const ALT_SCREEN_EXIT = '\x1b[?1049l';
|
|
@@ -104,6 +105,7 @@ async function main() {
|
|
|
104
105
|
permissionPromptRef,
|
|
105
106
|
_writeLastSessionPointer: writeLastSessionPointer,
|
|
106
107
|
systemMessageRouter,
|
|
108
|
+
setOpenAgentDetail,
|
|
107
109
|
} = ctx;
|
|
108
110
|
const workingDir = ctx.services.workingDirectory;
|
|
109
111
|
const homeDirectory = ctx.services.homeDirectory;
|
|
@@ -439,6 +441,7 @@ async function main() {
|
|
|
439
441
|
input.filePicker.setOnUpdate(() => render());
|
|
440
442
|
input.agentDetailModal.setOnRefresh(() => render());
|
|
441
443
|
input.processModal.setOnRefresh(() => render());
|
|
444
|
+
setOpenAgentDetail((id) => input.agentDetailModal.open(id));
|
|
442
445
|
|
|
443
446
|
// Model picker callback is handled in bootstrap.ts — do not duplicate here.
|
|
444
447
|
input.setHistory(inputHistory);
|
|
@@ -479,6 +482,17 @@ async function main() {
|
|
|
479
482
|
hasAttachments: input.getImageAttachments().size > 0,
|
|
480
483
|
turnState: sessionSnapshot.turnState,
|
|
481
484
|
});
|
|
485
|
+
const maintenanceStatus = evaluateSessionMaintenance({
|
|
486
|
+
configManager,
|
|
487
|
+
currentTokens: orchestrator.lastInputTokens,
|
|
488
|
+
contextWindow: currentModel.contextWindow,
|
|
489
|
+
sessionMemoryCount: ctx.services.sessionMemoryStore.list().length,
|
|
490
|
+
});
|
|
491
|
+
const contextStatusHint = buildContextStatusHint({
|
|
492
|
+
level: maintenanceStatus.level,
|
|
493
|
+
autoCompactEnabled: maintenanceStatus.autoCompactEnabled,
|
|
494
|
+
usagePct: maintenanceStatus.usagePct,
|
|
495
|
+
});
|
|
482
496
|
const footerLines = buildShellFooter({
|
|
483
497
|
width,
|
|
484
498
|
promptText: promptInfo.visibleLines.join('\n'),
|
|
@@ -496,6 +510,7 @@ async function main() {
|
|
|
496
510
|
workingDir,
|
|
497
511
|
provider: runtime.provider,
|
|
498
512
|
contextWindow: currentModel.contextWindow,
|
|
513
|
+
contextStatusHint,
|
|
499
514
|
// behavior.autoCompactThreshold is stored as a percent integer (e.g. 80);
|
|
500
515
|
// the meter expects a fraction [0..1]. Clamp to [0,1] to guard nonsense values.
|
|
501
516
|
compactThreshold: Math.min(1, Math.max(0, (configManager.get('behavior.autoCompactThreshold') as number) / 100)),
|
|
@@ -674,32 +689,25 @@ async function main() {
|
|
|
674
689
|
render,
|
|
675
690
|
});
|
|
676
691
|
|
|
677
|
-
// ---
|
|
678
|
-
const refreshGit
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
}));
|
|
697
|
-
unsubs.push(uiServices.events.tools.on('TOOL_SUCCEEDED', () => {
|
|
698
|
-
refreshGit();
|
|
699
|
-
}));
|
|
700
|
-
unsubs.push(uiServices.events.tools.on('TOOL_FAILED', () => {
|
|
701
|
-
refreshGit();
|
|
702
|
-
}));
|
|
692
|
+
// --- Turn-completed / git-refresh event wiring ---
|
|
693
|
+
const { refreshGit, unsubs: turnUnsubs } = wireTurnEventHandlers({
|
|
694
|
+
events: uiServices.events,
|
|
695
|
+
conversation,
|
|
696
|
+
runtime,
|
|
697
|
+
orchestrator,
|
|
698
|
+
configManager,
|
|
699
|
+
providerRegistry,
|
|
700
|
+
systemMessageRouter,
|
|
701
|
+
hookDispatcher,
|
|
702
|
+
workingDir,
|
|
703
|
+
homeDirectory,
|
|
704
|
+
sessionManager: ctx.services.sessionManager,
|
|
705
|
+
gitStatusProvider,
|
|
706
|
+
lastGitInfoRef,
|
|
707
|
+
buildSessionContinuityHints,
|
|
708
|
+
render,
|
|
709
|
+
});
|
|
710
|
+
unsubs.push(...turnUnsubs);
|
|
703
711
|
|
|
704
712
|
// --- Stream metrics + tool-timer event wiring ---
|
|
705
713
|
const streamUnsubs = wireStreamEventMetrics({
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
resolveScrollablePanelSection,
|
|
22
22
|
DEFAULT_PANEL_PALETTE,
|
|
23
23
|
} from './polish.ts';
|
|
24
|
+
import {
|
|
25
|
+
type ConfirmState,
|
|
26
|
+
handleConfirmInput,
|
|
27
|
+
} from './confirm-state.ts';
|
|
24
28
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
25
29
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
26
30
|
import {
|
|
@@ -32,6 +36,8 @@ import {
|
|
|
32
36
|
formatAgentDuration as formatMs,
|
|
33
37
|
formatAgentTime as shortTime,
|
|
34
38
|
jsonlToTimeline,
|
|
39
|
+
AGENT_TERMINAL_STATUSES,
|
|
40
|
+
AGENT_STALL_THRESHOLD_MS,
|
|
35
41
|
} from './agent-inspector-shared.ts';
|
|
36
42
|
|
|
37
43
|
// ---------------------------------------------------------------------------
|
|
@@ -77,10 +83,14 @@ const COLOR = {
|
|
|
77
83
|
// AgentInspectorPanel
|
|
78
84
|
// ---------------------------------------------------------------------------
|
|
79
85
|
|
|
86
|
+
// AGENT_TERMINAL_STATUSES and AGENT_STALL_THRESHOLD_MS imported from agent-inspector-shared.ts
|
|
87
|
+
|
|
80
88
|
export interface AgentInspectorPanelDeps {
|
|
81
|
-
readonly agentManager: Pick<AgentManager, 'list' | 'getStatus'>;
|
|
89
|
+
readonly agentManager: Pick<AgentManager, 'list' | 'getStatus' | 'cancel'>;
|
|
82
90
|
readonly agentMessageBus: Pick<AgentMessageBus, 'getMessages'>;
|
|
83
91
|
readonly workingDirectory: string;
|
|
92
|
+
/** Cancel the agent by id. Uses the same orphan-free path as WRFC. Returns true if cancelled. */
|
|
93
|
+
readonly cancelAgent: (agentId: string) => boolean;
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
export class AgentInspectorPanel extends BasePanel {
|
|
@@ -102,6 +112,9 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
102
112
|
// Row cache — cleared on markDirty(), computed once per render cycle
|
|
103
113
|
private _cachedRows: DisplayRow[] | null = null;
|
|
104
114
|
|
|
115
|
+
/** Pending cancel confirmation — subject is the agent id to cancel. */
|
|
116
|
+
private confirmCancel: ConfirmState<string> | null = null;
|
|
117
|
+
|
|
105
118
|
constructor(private readonly deps: AgentInspectorPanelDeps) {
|
|
106
119
|
super('inspector', 'Inspector', 'I', 'agent');
|
|
107
120
|
}
|
|
@@ -157,13 +170,36 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
157
170
|
// -------------------------------------------------------------------------
|
|
158
171
|
|
|
159
172
|
handleInput(key: string): boolean {
|
|
173
|
+
// Confirm-cancel flow takes priority — same contract as WRFC panel.
|
|
174
|
+
if (this.confirmCancel) {
|
|
175
|
+
const result = handleConfirmInput(this.confirmCancel, key);
|
|
176
|
+
if (result === 'confirmed') {
|
|
177
|
+
const rec = this.selectedAgentId
|
|
178
|
+
? this.deps.agentManager.getStatus(this.selectedAgentId)
|
|
179
|
+
: null;
|
|
180
|
+
if (rec && !AGENT_TERMINAL_STATUSES.has(rec.status)) {
|
|
181
|
+
this.deps.cancelAgent(rec.id);
|
|
182
|
+
}
|
|
183
|
+
this.confirmCancel = null;
|
|
184
|
+
this.markDirty();
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (result === 'cancelled') {
|
|
188
|
+
this.confirmCancel = null;
|
|
189
|
+
this.markDirty();
|
|
190
|
+
}
|
|
191
|
+
// absorbed: confirm stays pending
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
160
195
|
switch (key) {
|
|
161
|
-
case 'up': this._moveCursor(-1);
|
|
162
|
-
case 'down': this._moveCursor(1);
|
|
163
|
-
case 'pageup': this._scroll(-10);
|
|
164
|
-
case 'pagedown': this._scroll(10);
|
|
165
|
-
case 'return': this._toggleExpand();
|
|
166
|
-
case 'tab': this._nextAgent();
|
|
196
|
+
case 'up': this._moveCursor(-1); return true;
|
|
197
|
+
case 'down': this._moveCursor(1); return true;
|
|
198
|
+
case 'pageup': this._scroll(-10); return true;
|
|
199
|
+
case 'pagedown': this._scroll(10); return true;
|
|
200
|
+
case 'return': this._toggleExpand(); return true;
|
|
201
|
+
case 'tab': this._nextAgent(); return true;
|
|
202
|
+
case 'c': this._beginCancelConfirm(); return true;
|
|
167
203
|
default: return false;
|
|
168
204
|
}
|
|
169
205
|
}
|
|
@@ -217,6 +253,11 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
217
253
|
}
|
|
218
254
|
|
|
219
255
|
summaryLines.push(this._renderAgentInfoSummary(width, rec));
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
const isStalled = this._isAgentStalled(rec, now);
|
|
258
|
+
if (isStalled) {
|
|
259
|
+
summaryLines.push(buildPanelLine(width, [[' STALLED', '#f59e0b'], [' — no activity for 5+ minutes', DEFAULT_PANEL_PALETTE.dim]]));
|
|
260
|
+
}
|
|
220
261
|
const allRows = this._getCachedRows();
|
|
221
262
|
if (allRows.length === 0) {
|
|
222
263
|
return buildPanelWorkspace(width, height, {
|
|
@@ -243,13 +284,42 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
243
284
|
}
|
|
244
285
|
|
|
245
286
|
this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, allRows.length - 1));
|
|
287
|
+
const selectedRec = this.selectedAgentId
|
|
288
|
+
? this.deps.agentManager.getStatus(this.selectedAgentId)
|
|
289
|
+
: null;
|
|
290
|
+
const cancellable = selectedRec && !AGENT_TERMINAL_STATUSES.has(selectedRec.status);
|
|
246
291
|
const summarySection = { title: 'Summary', lines: summaryLines } as const;
|
|
247
292
|
const agentsSection = { title: 'Agents', lines: [selectorLine] } as const;
|
|
293
|
+
|
|
294
|
+
// Confirm-cancel overlay section.
|
|
295
|
+
const confirmSection = this.confirmCancel ? {
|
|
296
|
+
title: 'Confirm Cancel',
|
|
297
|
+
lines: [
|
|
298
|
+
buildPanelLine(width, [
|
|
299
|
+
[' Cancel agent "', DEFAULT_PANEL_PALETTE.warn],
|
|
300
|
+
[this.confirmCancel.label, DEFAULT_PANEL_PALETTE.value],
|
|
301
|
+
['"?', DEFAULT_PANEL_PALETTE.warn],
|
|
302
|
+
]),
|
|
303
|
+
buildPanelLine(width, [
|
|
304
|
+
[' y', DEFAULT_PANEL_PALETTE.info], [' confirm', DEFAULT_PANEL_PALETTE.dim],
|
|
305
|
+
[' Enter', DEFAULT_PANEL_PALETTE.info], [' confirm', DEFAULT_PANEL_PALETTE.dim],
|
|
306
|
+
[' n / Esc', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim],
|
|
307
|
+
]),
|
|
308
|
+
],
|
|
309
|
+
} : null;
|
|
310
|
+
|
|
311
|
+
const cancelHintFg = cancellable ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim;
|
|
312
|
+
const footerLine = buildPanelLine(width, [
|
|
313
|
+
[` L${this.cursorIndex + 1}/${allRows.length}`, DEFAULT_PANEL_PALETTE.dim],
|
|
314
|
+
[' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim],
|
|
315
|
+
[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim],
|
|
316
|
+
[' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim],
|
|
317
|
+
[' c', cancelHintFg], [cancellable ? ' cancel' : ' cancel (n/a)', DEFAULT_PANEL_PALETTE.dim],
|
|
318
|
+
]);
|
|
319
|
+
|
|
248
320
|
const timelineSection = resolveScrollablePanelSection(width, height, {
|
|
249
321
|
intro: 'Inspect a selected agent timeline, tool activity, expanded details, and live/historical message flow.',
|
|
250
|
-
footerLines: [
|
|
251
|
-
buildPanelLine(width, [[` L${this.cursorIndex + 1}/${allRows.length}`, DEFAULT_PANEL_PALETTE.dim], [' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim], [' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
|
|
252
|
-
],
|
|
322
|
+
footerLines: [footerLine],
|
|
253
323
|
palette: DEFAULT_PANEL_PALETTE,
|
|
254
324
|
beforeSections: [summarySection, agentsSection],
|
|
255
325
|
section: {
|
|
@@ -259,20 +329,22 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
259
329
|
scrollOffset: this.scrollOffset,
|
|
260
330
|
minRows: 8,
|
|
261
331
|
},
|
|
332
|
+
afterSections: confirmSection ? [confirmSection] : undefined,
|
|
262
333
|
});
|
|
263
334
|
this.scrollOffset = timelineSection.scrollOffset;
|
|
264
335
|
|
|
336
|
+
const sections = [
|
|
337
|
+
summarySection,
|
|
338
|
+
agentsSection,
|
|
339
|
+
timelineSection.section,
|
|
340
|
+
...(confirmSection ? [confirmSection] : []),
|
|
341
|
+
];
|
|
342
|
+
|
|
265
343
|
return buildPanelWorkspace(width, height, {
|
|
266
344
|
title: ` Inspector [${agents.length} agent${agents.length !== 1 ? 's' : ''}]`,
|
|
267
345
|
intro: 'Inspect a selected agent timeline, tool activity, expanded details, and live/historical message flow.',
|
|
268
|
-
sections
|
|
269
|
-
|
|
270
|
-
agentsSection,
|
|
271
|
-
timelineSection.section,
|
|
272
|
-
],
|
|
273
|
-
footerLines: [
|
|
274
|
-
buildPanelLine(width, [[` L${this.cursorIndex + 1}/${allRows.length}`, DEFAULT_PANEL_PALETTE.dim], [' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim], [' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
|
|
275
|
-
],
|
|
346
|
+
sections,
|
|
347
|
+
footerLines: [footerLine],
|
|
276
348
|
palette: DEFAULT_PANEL_PALETTE,
|
|
277
349
|
});
|
|
278
350
|
}
|
|
@@ -517,4 +589,34 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
517
589
|
this.inspectAgent(next.id);
|
|
518
590
|
}
|
|
519
591
|
}
|
|
592
|
+
|
|
593
|
+
// -------------------------------------------------------------------------
|
|
594
|
+
// Private — cancel + stall
|
|
595
|
+
// -------------------------------------------------------------------------
|
|
596
|
+
|
|
597
|
+
/** Initiate cancel-confirm flow for the selected agent (noop if terminal or none selected). */
|
|
598
|
+
private _beginCancelConfirm(): void {
|
|
599
|
+
if (!this.selectedAgentId) return;
|
|
600
|
+
const rec = this.deps.agentManager.getStatus(this.selectedAgentId);
|
|
601
|
+
if (!rec || AGENT_TERMINAL_STATUSES.has(rec.status)) return;
|
|
602
|
+
const label = rec.task.split('\n')[0]?.slice(0, 40) ?? rec.id.slice(-8);
|
|
603
|
+
this.confirmCancel = { subject: rec.id, label };
|
|
604
|
+
this.markDirty();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Returns whether an agent is considered stalled (non-terminal, running past threshold). */
|
|
608
|
+
private _isAgentStalled(rec: AgentRecord, now: number): boolean {
|
|
609
|
+
if (AGENT_TERMINAL_STATUSES.has(rec.status)) return false;
|
|
610
|
+
return (now - rec.startedAt) >= AGENT_STALL_THRESHOLD_MS;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Count of all tracked agents that are stalled (non-terminal, no activity
|
|
615
|
+
* for AGENT_STALL_THRESHOLD_MS). Exposed so callers can aggregate a
|
|
616
|
+
* stalledAgentCount for cockpit / roster read-models.
|
|
617
|
+
*/
|
|
618
|
+
getStalledAgentCount(): number {
|
|
619
|
+
const now = Date.now();
|
|
620
|
+
return this.deps.agentManager.list().filter(rec => this._isAgentStalled(rec, now)).length;
|
|
621
|
+
}
|
|
520
622
|
}
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
export type AgentInspectorEntryKind = 'user' | 'assistant' | 'tool_call' | 'tool_result' | 'session' | 'error';
|
|
2
2
|
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Shared agent status / stall constants
|
|
5
|
+
// Used by AgentInspectorPanel, AgentDetailModal, and cockpit read-model consumers.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/** Terminal statuses — cancel not offered; stall check skipped. */
|
|
9
|
+
export const AGENT_TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
10
|
+
|
|
11
|
+
/** Agents in a non-terminal state for longer than this are considered STALLED. */
|
|
12
|
+
export const AGENT_STALL_THRESHOLD_MS = 5 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Count stalled agents from a raw record list.
|
|
16
|
+
* An agent is stalled when it is non-terminal and has been running for at
|
|
17
|
+
* least AGENT_STALL_THRESHOLD_MS without completing.
|
|
18
|
+
*
|
|
19
|
+
* Extracted as a standalone export so read-models and panels can share the
|
|
20
|
+
* canonical stall-count logic (TASK-046).
|
|
21
|
+
*/
|
|
22
|
+
export function countStalledAgents(
|
|
23
|
+
records: ReadonlyArray<{ status: string; startedAt: number }>,
|
|
24
|
+
now: number = Date.now(),
|
|
25
|
+
): number {
|
|
26
|
+
return records.filter(
|
|
27
|
+
(r) => !AGENT_TERMINAL_STATUSES.has(r.status) && (now - r.startedAt) >= AGENT_STALL_THRESHOLD_MS,
|
|
28
|
+
).length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
3
32
|
export interface AgentTimelineEntry {
|
|
4
33
|
kind: AgentInspectorEntryKind;
|
|
5
34
|
timestamp: number;
|
|
@@ -51,6 +51,7 @@ export function registerDevelopmentPanels(manager: PanelManager, deps: ResolvedB
|
|
|
51
51
|
agentManager: ui.agents.agentManager,
|
|
52
52
|
agentMessageBus: ui.agents.agentMessageBus,
|
|
53
53
|
workingDirectory: ui.environment.workingDirectory,
|
|
54
|
+
cancelAgent: (agentId: string) => ui.agents.agentManager.cancel(agentId),
|
|
54
55
|
});
|
|
55
56
|
},
|
|
56
57
|
});
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import type { PanelManager } from '../panel-manager.ts';
|
|
2
2
|
import { MemoryPanel } from '../memory-panel.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { KnowledgeGraphPanel } from '../knowledge-graph-panel.ts';
|
|
4
4
|
import type { ResolvedBuiltinPanelDeps } from './shared.ts';
|
|
5
5
|
|
|
6
6
|
export function registerKnowledgePanels(manager: PanelManager, deps: ResolvedBuiltinPanelDeps): void {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const { memoryRegistry } = deps;
|
|
7
|
+
// KnowledgeGraphPanel is a no-arg panel — always register it regardless of memoryRegistry.
|
|
10
8
|
manager.registerType({
|
|
11
9
|
id: 'knowledge',
|
|
12
10
|
name: 'Knowledge',
|
|
13
11
|
icon: 'K',
|
|
14
12
|
category: 'agent',
|
|
15
13
|
description: 'Structured project knowledge: risks, runbooks, architecture notes, incidents, and durable facts',
|
|
16
|
-
factory: () => new
|
|
17
|
-
});
|
|
18
|
-
manager.registerType({
|
|
19
|
-
id: 'memory',
|
|
20
|
-
name: 'Memory',
|
|
21
|
-
icon: 'M',
|
|
22
|
-
category: 'agent',
|
|
23
|
-
description: 'Project memory: decisions, constraints, incidents, and patterns with provenance links',
|
|
24
|
-
factory: () => new MemoryPanel(memoryRegistry),
|
|
14
|
+
factory: () => new KnowledgeGraphPanel(),
|
|
25
15
|
});
|
|
16
|
+
if (deps.memoryRegistry) {
|
|
17
|
+
const { memoryRegistry } = deps;
|
|
18
|
+
manager.registerType({
|
|
19
|
+
id: 'memory',
|
|
20
|
+
name: 'Memory',
|
|
21
|
+
icon: 'M',
|
|
22
|
+
category: 'agent',
|
|
23
|
+
description: 'Project memory: decisions, constraints, incidents, and patterns with provenance links',
|
|
24
|
+
factory: () => new MemoryPanel(memoryRegistry),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
26
27
|
}
|