@pellux/goodvibes-tui 0.21.0 → 0.23.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 +45 -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/management-commands.ts +1 -1
- package/src/cli/management-utils.ts +352 -0
- package/src/cli/management.ts +36 -334
- package/src/cli/parser.ts +17 -0
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/types.ts +2 -0
- package/src/config/goodvibes-home-audit.ts +2 -0
- package/src/core/context-auto-compact.ts +110 -0
- package/src/core/conversation-rendering.ts +5 -2
- package/src/core/conversation-types.ts +24 -0
- package/src/core/conversation.ts +7 -12
- package/src/core/stream-event-wiring.ts +125 -7
- 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/channel-runtime.ts +139 -0
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/provider.ts +57 -3
- package/src/input/commands/runtime-services.ts +30 -1
- package/src/input/commands/session-workflow.ts +8 -16
- package/src/input/commands/session.ts +70 -20
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +54 -4
- package/src/input/commands.ts +2 -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 +58 -50
- 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/compaction-history-modal.ts +55 -0
- package/src/renderer/compaction-preview.ts +146 -0
- package/src/renderer/context-status-hint.ts +54 -0
- package/src/renderer/settings-modal-helpers.ts +2 -2
- 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 +116 -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/utils/browser.ts +29 -0
- package/src/version.ts +1 -1
- package/src/panels/knowledge-panel.ts +0 -343
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { CONFIG_SCHEMA, type ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
10
|
-
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
10
|
+
import type { ConfigManager, ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
|
|
11
11
|
import { getResolvedSettingLookup } from '@/runtime/index.ts';
|
|
12
12
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
13
13
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
@@ -114,9 +114,63 @@ export function buildSettingGroups(
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// Inject the synthetic tts.speed entry into the tts category.
|
|
118
|
+
// tts.speed is not yet a ConfigKey in the SDK schema (pending SDK addition).
|
|
119
|
+
// The entry is surfaced here with an honest description caveat so users can
|
|
120
|
+
// see and understand the setting before the SDK schema catches up.
|
|
121
|
+
if (ttsEntries && !ttsEntries.some((e) => e.setting.key === ('tts.speed' as ConfigKey))) {
|
|
122
|
+
ttsEntries.push(buildTtsSpeedSyntheticEntry(configManager));
|
|
123
|
+
}
|
|
124
|
+
|
|
117
125
|
return groups;
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// TTS_SPEED_DEFAULT — the pending-SDK default for tts.speed
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Pending default for tts.speed. Matches the value the SDK will use once
|
|
134
|
+
* the schema field is added: 1 (normal speed, provider default).
|
|
135
|
+
* Used for the synthetic settings-modal entry and isDefault comparisons.
|
|
136
|
+
*/
|
|
137
|
+
export const TTS_SPEED_DEFAULT = 1;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The synthetic ConfigSetting descriptor for tts.speed.
|
|
141
|
+
* `tts.speed` is not yet a ConfigKey in the SDK schema. This descriptor is
|
|
142
|
+
* TUI-local and is injected into the tts settings group so users can see
|
|
143
|
+
* and interact with the setting before the SDK schema catches up.
|
|
144
|
+
*
|
|
145
|
+
* The key is cast to ConfigKey because ConfigSetting requires it and the SDK
|
|
146
|
+
* will add this key in a future release. The cast is safe: configManager.get
|
|
147
|
+
* returns undefined for unknown keys rather than throwing.
|
|
148
|
+
*/
|
|
149
|
+
export const TTS_SPEED_SYNTHETIC_SETTING: ConfigSetting = {
|
|
150
|
+
key: 'tts.speed' as ConfigKey,
|
|
151
|
+
type: 'number',
|
|
152
|
+
default: TTS_SPEED_DEFAULT,
|
|
153
|
+
description: 'Playback speed multiplier passed to the TTS provider (1.0 = normal). Takes effect immediately via the TUI bridge; SDK schema registration is pending (native typing only).',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build the synthetic SettingEntry for tts.speed.
|
|
158
|
+
*
|
|
159
|
+
* Reads the raw value from configManager using a cast key (tts.speed is not
|
|
160
|
+
* yet a valid ConfigKey). If the value is absent or not a positive finite
|
|
161
|
+
* number, falls back to TTS_SPEED_DEFAULT and marks isDefault true.
|
|
162
|
+
*/
|
|
163
|
+
export function buildTtsSpeedSyntheticEntry(configManager: Pick<ConfigManager, 'get'>): SettingEntry {
|
|
164
|
+
const raw = configManager.get('tts.speed' as ConfigKey);
|
|
165
|
+
const parsed = typeof raw === 'number' ? raw : parseFloat(String(raw ?? ''));
|
|
166
|
+
const currentValue: number = isFinite(parsed) && parsed > 0 ? parsed : TTS_SPEED_DEFAULT;
|
|
167
|
+
return {
|
|
168
|
+
setting: TTS_SPEED_SYNTHETIC_SETTING,
|
|
169
|
+
currentValue,
|
|
170
|
+
isDefault: deepEqual(currentValue, TTS_SPEED_DEFAULT),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
120
174
|
// ---------------------------------------------------------------------------
|
|
121
175
|
// buildFlagEntries — snapshot of current feature flag states
|
|
122
176
|
// ---------------------------------------------------------------------------
|
|
@@ -177,13 +231,31 @@ export function buildNetworkFilteredItems(
|
|
|
177
231
|
// refreshEntryValues — re-reads currentValue/isDefault for all loaded entries
|
|
178
232
|
// ---------------------------------------------------------------------------
|
|
179
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Normalize a raw config value for the tts.speed synthetic entry.
|
|
236
|
+
* Returns the raw value if it is a positive finite number, otherwise falls
|
|
237
|
+
* back to TTS_SPEED_DEFAULT. Mirrors the logic in buildTtsSpeedSyntheticEntry.
|
|
238
|
+
*/
|
|
239
|
+
function normalizeTtsSpeedValue(raw: unknown): number {
|
|
240
|
+
const parsed = typeof raw === 'number' ? raw : parseFloat(String(raw ?? ''));
|
|
241
|
+
return isFinite(parsed) && parsed > 0 ? parsed : TTS_SPEED_DEFAULT;
|
|
242
|
+
}
|
|
243
|
+
|
|
180
244
|
export function refreshEntryValues(
|
|
181
245
|
groups: Map<SettingsCategory, SettingEntry[]>,
|
|
182
246
|
configManager: ConfigManager,
|
|
183
247
|
): void {
|
|
184
248
|
for (const entries of groups.values()) {
|
|
185
249
|
for (const entry of entries) {
|
|
186
|
-
|
|
250
|
+
const raw = configManager.get(entry.setting.key as ConfigKey);
|
|
251
|
+
// Synthetic entries (e.g. tts.speed) that have no SDK schema key return
|
|
252
|
+
// undefined from configManager. Normalize using the same logic used at
|
|
253
|
+
// construction time so isDefault stays accurate.
|
|
254
|
+
if (entry.setting.key === ('tts.speed' as ConfigKey)) {
|
|
255
|
+
entry.currentValue = normalizeTtsSpeedValue(raw);
|
|
256
|
+
} else {
|
|
257
|
+
entry.currentValue = raw;
|
|
258
|
+
}
|
|
187
259
|
entry.isDefault = deepEqual(entry.currentValue, entry.setting.default);
|
|
188
260
|
}
|
|
189
261
|
}
|
|
@@ -201,7 +273,9 @@ export function updateEntryForKey(
|
|
|
201
273
|
for (const entries of groups.values()) {
|
|
202
274
|
const entry = entries.find((candidate) => candidate.setting.key === key);
|
|
203
275
|
if (entry) {
|
|
204
|
-
|
|
276
|
+
const raw = configManager.get(key);
|
|
277
|
+
// Synthetic tts.speed entry: normalize using the same fallback logic.
|
|
278
|
+
entry.currentValue = key === ('tts.speed' as ConfigKey) ? normalizeTtsSpeedValue(raw) : raw;
|
|
205
279
|
entry.isDefault = deepEqual(entry.currentValue, entry.setting.default);
|
|
206
280
|
}
|
|
207
281
|
}
|
|
@@ -55,6 +55,9 @@ export function applySettingValue({
|
|
|
55
55
|
refreshGroups: () => void;
|
|
56
56
|
}): ApplyValueResult {
|
|
57
57
|
const previousValue = configManager.get(key);
|
|
58
|
+
// REQUIRES_RESTART: SDK's ConfigSetting has no requiresRestart field yet (see
|
|
59
|
+
// goodvibes-sdk HANDOFF-FROM-TUI-SESSION-20260611.md §Item 8). Until it does,
|
|
60
|
+
// we detect restart-triggering keys by sub-key name heuristic below.
|
|
58
61
|
const isRestartKey = ['host', 'port', 'hostMode', 'enabled'].includes(key.split('.')[1] ?? '');
|
|
59
62
|
|
|
60
63
|
try {
|
|
@@ -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';
|
|
@@ -56,19 +54,16 @@ import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/ter
|
|
|
56
54
|
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
|
-
import { wireStreamEventMetrics, type StreamMetrics } from './core/stream-event-wiring.ts';
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const KEYBOARD_EXT_DISABLE = '\x1b[>4;0m' + '\x1b[?1l';
|
|
70
|
-
const PASTE_ENABLE = '\x1b[?2004h';
|
|
71
|
-
const PASTE_DISABLE = '\x1b[?2004l';
|
|
57
|
+
import { wireStreamEventMetrics, type StreamMetrics, type WireStreamEventMetricsResult } 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';
|
|
61
|
+
|
|
62
|
+
const ALT_SCREEN_ENTER = '\x1b[?1049h'; const ALT_SCREEN_EXIT = '\x1b[?1049l';
|
|
63
|
+
const MOUSE_ENABLE = '\x1b[?1000h\x1b[?1002h\x1b[?1006h'; const MOUSE_DISABLE = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
|
|
64
|
+
const CURSOR_HIDE = '\x1b[?25l'; const CURSOR_SHOW = '\x1b[?25h'; const CLEAR_SCREEN = '\x1b[2J\x1b[3J\x1b[H';
|
|
65
|
+
const KEYBOARD_EXT_ENABLE = '\x1b[>4;2m' + '\x1b[?1u'; const KEYBOARD_EXT_DISABLE = '\x1b[>4;0m' + '\x1b[?1l';
|
|
66
|
+
const PASTE_ENABLE = '\x1b[?2004h'; const PASTE_DISABLE = '\x1b[?2004l';
|
|
72
67
|
|
|
73
68
|
async function main() {
|
|
74
69
|
const stdout = process.stdout;
|
|
@@ -104,6 +99,7 @@ async function main() {
|
|
|
104
99
|
permissionPromptRef,
|
|
105
100
|
_writeLastSessionPointer: writeLastSessionPointer,
|
|
106
101
|
systemMessageRouter,
|
|
102
|
+
setOpenAgentDetail,
|
|
107
103
|
} = ctx;
|
|
108
104
|
const workingDir = ctx.services.workingDirectory;
|
|
109
105
|
const homeDirectory = ctx.services.homeDirectory;
|
|
@@ -313,10 +309,11 @@ async function main() {
|
|
|
313
309
|
}
|
|
314
310
|
if (processedText || content) {
|
|
315
311
|
void (async () => {
|
|
316
|
-
|
|
317
|
-
if (options.spokenOutput && processedText) {
|
|
318
|
-
|
|
319
|
-
}
|
|
312
|
+
const inputOptions = options.spokenOutput ? createSpokenTurnInputOptions() : undefined;
|
|
313
|
+
if (options.spokenOutput && processedText) { spokenTurns.submitNextTurn(processedText); }
|
|
314
|
+
// Snapshot pre-submission state for failover retryTurn; also clears visited set.
|
|
315
|
+
retryCtx = { count: conversation.getMessageCount(), text: processedText, content, opts: inputOptions };
|
|
316
|
+
streamResult.clearFailoverVisited();
|
|
320
317
|
orchestrator.handleUserInput(processedText, content, inputOptions).catch((err: unknown) => {
|
|
321
318
|
logger.debug('handleUserInput safety catch (already handled by runTurn)', { error: summarizeError(err) });
|
|
322
319
|
});
|
|
@@ -439,6 +436,7 @@ async function main() {
|
|
|
439
436
|
input.filePicker.setOnUpdate(() => render());
|
|
440
437
|
input.agentDetailModal.setOnRefresh(() => render());
|
|
441
438
|
input.processModal.setOnRefresh(() => render());
|
|
439
|
+
setOpenAgentDetail((id) => input.agentDetailModal.open(id));
|
|
442
440
|
|
|
443
441
|
// Model picker callback is handled in bootstrap.ts — do not duplicate here.
|
|
444
442
|
input.setHistory(inputHistory);
|
|
@@ -479,6 +477,17 @@ async function main() {
|
|
|
479
477
|
hasAttachments: input.getImageAttachments().size > 0,
|
|
480
478
|
turnState: sessionSnapshot.turnState,
|
|
481
479
|
});
|
|
480
|
+
const maintenanceStatus = evaluateSessionMaintenance({
|
|
481
|
+
configManager,
|
|
482
|
+
currentTokens: orchestrator.lastInputTokens,
|
|
483
|
+
contextWindow: currentModel.contextWindow,
|
|
484
|
+
sessionMemoryCount: ctx.services.sessionMemoryStore.list().length,
|
|
485
|
+
});
|
|
486
|
+
const contextStatusHint = buildContextStatusHint({
|
|
487
|
+
level: maintenanceStatus.level,
|
|
488
|
+
autoCompactEnabled: maintenanceStatus.autoCompactEnabled,
|
|
489
|
+
usagePct: maintenanceStatus.usagePct,
|
|
490
|
+
});
|
|
482
491
|
const footerLines = buildShellFooter({
|
|
483
492
|
width,
|
|
484
493
|
promptText: promptInfo.visibleLines.join('\n'),
|
|
@@ -496,6 +505,7 @@ async function main() {
|
|
|
496
505
|
workingDir,
|
|
497
506
|
provider: runtime.provider,
|
|
498
507
|
contextWindow: currentModel.contextWindow,
|
|
508
|
+
contextStatusHint,
|
|
499
509
|
// behavior.autoCompactThreshold is stored as a percent integer (e.g. 80);
|
|
500
510
|
// the meter expects a fraction [0..1]. Clamp to [0,1] to guard nonsense values.
|
|
501
511
|
compactThreshold: Math.min(1, Math.max(0, (configManager.get('behavior.autoCompactThreshold') as number) / 100)),
|
|
@@ -674,43 +684,41 @@ async function main() {
|
|
|
674
684
|
render,
|
|
675
685
|
});
|
|
676
686
|
|
|
677
|
-
|
|
678
|
-
const refreshGit = () => gitStatusProvider.refresh().then((info) => { lastGitInfoRef.value = info; render(); }).catch(() => { /* non-fatal */ });
|
|
679
|
-
// Refresh git status after each turn completes or after tool results arrive
|
|
680
|
-
unsubs.push(uiServices.events.turns.on('TURN_COMPLETED', () => {
|
|
681
|
-
// Auto-save after every LLM turn so kills don't lose the session
|
|
682
|
-
try {
|
|
683
|
-
const snapshot = conversation.toJSON() as { messages: Array<import('./core/conversation.ts').ConversationMessageSnapshot>; timestamp?: number };
|
|
684
|
-
const persisted = buildPersistedSessionContext(snapshot.messages, conversation.getTitleSource(), buildSessionContinuityHints());
|
|
685
|
-
persistConversation(
|
|
686
|
-
runtime.sessionId,
|
|
687
|
-
{ ...snapshot, ...persisted },
|
|
688
|
-
runtime.model,
|
|
689
|
-
runtime.provider,
|
|
690
|
-
conversation.title || '',
|
|
691
|
-
{ workingDirectory: workingDir, homeDirectory, sessionManager: ctx.services.sessionManager },
|
|
692
|
-
);
|
|
693
|
-
hookDispatcher.fire({ path: 'Lifecycle:session:save' as HookEventPath, phase: 'Lifecycle' as HookPhase, category: 'session' as HookCategory, specific: 'save', sessionId: runtime.sessionId, timestamp: Date.now(), payload: { sessionId: runtime.sessionId } }).catch((err: unknown) => logger.debug('hook fire error', { error: summarizeError(err) }));
|
|
694
|
-
} catch (e) { logger.debug('auto-save on turn:complete failed', { error: summarizeError(e) }); }
|
|
695
|
-
refreshGit();
|
|
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
|
-
}));
|
|
703
|
-
|
|
704
|
-
// --- Stream metrics + tool-timer event wiring ---
|
|
705
|
-
const streamUnsubs = wireStreamEventMetrics({
|
|
687
|
+
const { refreshGit, unsubs: turnUnsubs } = wireTurnEventHandlers({
|
|
706
688
|
events: uiServices.events,
|
|
689
|
+
conversation,
|
|
690
|
+
runtime,
|
|
707
691
|
orchestrator,
|
|
692
|
+
configManager,
|
|
708
693
|
providerRegistry,
|
|
709
694
|
systemMessageRouter,
|
|
695
|
+
hookDispatcher,
|
|
696
|
+
workingDir,
|
|
697
|
+
homeDirectory,
|
|
698
|
+
sessionManager: ctx.services.sessionManager,
|
|
699
|
+
gitStatusProvider,
|
|
700
|
+
lastGitInfoRef,
|
|
701
|
+
buildSessionContinuityHints,
|
|
710
702
|
render,
|
|
711
|
-
metrics: streamMetrics,
|
|
712
703
|
});
|
|
713
|
-
unsubs.push(...
|
|
704
|
+
unsubs.push(...turnUnsubs);
|
|
705
|
+
|
|
706
|
+
// Stable turn context for failover retry — set in submitInput, read by retryTurn.
|
|
707
|
+
let retryCtx: { count: number; text: string; content?: ContentPart[]; opts?: Parameters<typeof orchestrator.handleUserInput>[2] } | null = null;
|
|
708
|
+
const streamResult: WireStreamEventMetricsResult = wireStreamEventMetrics({
|
|
709
|
+
events: uiServices.events, orchestrator, providerRegistry,
|
|
710
|
+
systemMessageRouter, render, metrics: streamMetrics,
|
|
711
|
+
providerOptimizer: ctx.services.providerOptimizer,
|
|
712
|
+
retryTurn: () => {
|
|
713
|
+
if (!retryCtx) return;
|
|
714
|
+
const { count, text, content: rContent, opts: rOpts } = retryCtx;
|
|
715
|
+
// Roll back to pre-submission count (strips error system messages), then
|
|
716
|
+
// re-submit. SDK gap — no retry-in-place; see HANDOFF item (Issue 2).
|
|
717
|
+
conversation.removeMessagesAfter(count);
|
|
718
|
+
orchestrator.handleUserInput(text, rContent, rOpts).catch((e: unknown) => logger.debug('retryTurn', { error: summarizeError(e) }));
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
unsubs.push(...streamResult.unsubs);
|
|
714
722
|
|
|
715
723
|
// --- Terminal setup ---
|
|
716
724
|
stdin.setRawMode(true);
|