@pellux/goodvibes-tui 0.19.65 → 0.19.67
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 +31 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/service-posture.ts +73 -3
- package/src/input/settings-modal-types.ts +13 -31
- package/src/input/settings-modal.ts +3 -1
- package/src/renderer/settings-modal.ts +51 -18
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,37 @@ All notable changes to GoodVibes TUI.
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## [0.19.67] — 2026-05-06
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Styled `/config` category group headings in bold and indented category rows so the category rail hierarchy is easier to scan.
|
|
15
|
+
|
|
16
|
+
### Verified
|
|
17
|
+
- `bun test src/test/renderer/settings-modal.test.ts`
|
|
18
|
+
- `bunx tsc --noEmit`
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [0.19.66] — 2026-05-06
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Changed `/config` to open with focus on the category rail at the first category instead of dropping users directly into the selected setting list.
|
|
26
|
+
- Reorganized the `/config` category rail into logical groups so related settings are easier to scan.
|
|
27
|
+
- Corrected `goodvibes service status` / service posture reporting for systemd-managed daemons by reconciling status against `systemctl --user show`, so running/enabled OS services are no longer reported as stopped because of stale pid-file state.
|
|
28
|
+
|
|
29
|
+
### Verified
|
|
30
|
+
- `bun test src/test/input/settings-modal.test.ts src/test/renderer/settings-modal.test.ts`
|
|
31
|
+
- `bun test src/test/input/modal-space-actions.test.ts`
|
|
32
|
+
- `bun test src/test/cli/service-posture.test.ts`
|
|
33
|
+
- `bun run test`
|
|
34
|
+
- `bunx tsc --noEmit`
|
|
35
|
+
- `bun run build:prod`
|
|
36
|
+
- `bun run smoke:tui`
|
|
37
|
+
- `bun run smoke:daemon`
|
|
38
|
+
- `git diff --check`
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
11
42
|
## [0.19.65] — 2026-05-05
|
|
12
43
|
|
|
13
44
|
### Fixed
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
6
|
|
|
7
7
|
A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.67",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
1
2
|
import { accessSync, closeSync, constants, existsSync, openSync, readSync, realpathSync, statSync } from 'node:fs';
|
|
2
3
|
import net from 'node:net';
|
|
3
4
|
import { basename, delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
@@ -82,6 +83,23 @@ interface ServiceDefinitionOverride {
|
|
|
82
83
|
readonly restartOnFailure: boolean;
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
interface CliServiceStatusCommandResult {
|
|
87
|
+
readonly status: number | null;
|
|
88
|
+
readonly stdout?: string;
|
|
89
|
+
readonly stderr?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface CliServiceStatusManager {
|
|
93
|
+
status(): ManagedServiceStatus;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface CliServicePostureOptions {
|
|
97
|
+
readonly probe?: boolean;
|
|
98
|
+
readonly logTailBytes?: number;
|
|
99
|
+
readonly manager?: CliServiceStatusManager;
|
|
100
|
+
readonly runCommand?: (command: string, args: readonly string[]) => CliServiceStatusCommandResult;
|
|
101
|
+
}
|
|
102
|
+
|
|
85
103
|
function connectHostForBindHost(host: string): string {
|
|
86
104
|
if (host === '0.0.0.0' || host === '::') return '127.0.0.1';
|
|
87
105
|
return host || '127.0.0.1';
|
|
@@ -227,6 +245,58 @@ function pidFilePath(runtime: CliServiceRuntime, platform: ManagedServiceStatus[
|
|
|
227
245
|
return join(getServiceStateRoot(runtime), 'service', `${platform}.pid`);
|
|
228
246
|
}
|
|
229
247
|
|
|
248
|
+
function runStatusCommand(
|
|
249
|
+
command: string,
|
|
250
|
+
args: readonly string[],
|
|
251
|
+
options: CliServicePostureOptions,
|
|
252
|
+
): CliServiceStatusCommandResult {
|
|
253
|
+
if (options.runCommand) return options.runCommand(command, args);
|
|
254
|
+
return spawnSync(command, [...args], {
|
|
255
|
+
stdio: 'pipe',
|
|
256
|
+
encoding: 'utf-8',
|
|
257
|
+
timeout: 1500,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parseSystemdShowValue(lines: readonly string[], key: string): string | null {
|
|
262
|
+
const prefix = `${key}=`;
|
|
263
|
+
const match = lines.find((line) => line.startsWith(prefix));
|
|
264
|
+
return match ? match.slice(prefix.length).trim() : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function reconcileSystemdServiceStatus(
|
|
268
|
+
runtime: CliServiceRuntime,
|
|
269
|
+
status: ManagedServiceStatus,
|
|
270
|
+
options: CliServicePostureOptions,
|
|
271
|
+
): ManagedServiceStatus {
|
|
272
|
+
if (status.platform !== 'systemd') return status;
|
|
273
|
+
|
|
274
|
+
const name = String(runtime.configManager.get('service.serviceName') ?? 'goodvibes').trim() || 'goodvibes';
|
|
275
|
+
const result = runStatusCommand('systemctl', [
|
|
276
|
+
'--user',
|
|
277
|
+
'show',
|
|
278
|
+
`${name}.service`,
|
|
279
|
+
'--property=LoadState,ActiveState,UnitFileState,MainPID',
|
|
280
|
+
'--no-page',
|
|
281
|
+
], options);
|
|
282
|
+
if ((result.status ?? 1) !== 0) return status;
|
|
283
|
+
|
|
284
|
+
const lines = (result.stdout ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
285
|
+
const loadState = parseSystemdShowValue(lines, 'LoadState');
|
|
286
|
+
const activeState = parseSystemdShowValue(lines, 'ActiveState');
|
|
287
|
+
const unitFileState = parseSystemdShowValue(lines, 'UnitFileState');
|
|
288
|
+
const rawPid = Number.parseInt(parseSystemdShowValue(lines, 'MainPID') ?? '', 10);
|
|
289
|
+
const pid = Number.isFinite(rawPid) && rawPid > 0 ? rawPid : undefined;
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
...status,
|
|
293
|
+
installed: loadState === 'loaded' || status.installed,
|
|
294
|
+
autostart: unitFileState === 'enabled' || unitFileState === 'linked' || status.autostart,
|
|
295
|
+
running: activeState === 'active',
|
|
296
|
+
...(pid === undefined ? {} : { pid }),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
230
300
|
function readLogPosture(path: string | undefined, tailBytes: number): CliServiceLogPosture {
|
|
231
301
|
if (!path) return { path: null, exists: false, size: 0, modifiedAt: null };
|
|
232
302
|
if (!existsSync(path)) return { path, exists: false, size: 0, modifiedAt: null };
|
|
@@ -298,10 +368,10 @@ export function createPlatformServiceManager(runtime: CliServiceRuntime): Platfo
|
|
|
298
368
|
|
|
299
369
|
export async function buildCliServicePosture(
|
|
300
370
|
runtime: CliServiceRuntime,
|
|
301
|
-
options:
|
|
371
|
+
options: CliServicePostureOptions = {},
|
|
302
372
|
): Promise<CliServicePosture> {
|
|
303
|
-
const manager = createPlatformServiceManager(runtime);
|
|
304
|
-
const status = manager.status();
|
|
373
|
+
const manager = options.manager ?? createPlatformServiceManager(runtime);
|
|
374
|
+
const status = reconcileSystemdServiceStatus(runtime, manager.status(), options);
|
|
305
375
|
const endpoints = await Promise.all(ENDPOINTS.map(async (endpoint): Promise<CliServiceEndpointPosture> => {
|
|
306
376
|
const enabled = runtime.configManager.get(endpoint.enabledKey as never) === true;
|
|
307
377
|
const binding = resolveRuntimeEndpointBinding(runtime.configManager, endpoint.id);
|
|
@@ -36,39 +36,21 @@ export type SettingsCategory =
|
|
|
36
36
|
|
|
37
37
|
export type SettingsFocusPane = 'categories' | 'settings';
|
|
38
38
|
|
|
39
|
-
export const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'orchestration',
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'helper',
|
|
51
|
-
'tts',
|
|
52
|
-
'service',
|
|
53
|
-
'controlPlane',
|
|
54
|
-
'httpListener',
|
|
55
|
-
'web',
|
|
56
|
-
'network',
|
|
57
|
-
'mcp',
|
|
58
|
-
'sandbox',
|
|
59
|
-
'surfaces',
|
|
60
|
-
'cloudflare',
|
|
61
|
-
'batch',
|
|
62
|
-
'automation',
|
|
63
|
-
'watchers',
|
|
64
|
-
'runtime',
|
|
65
|
-
'telemetry',
|
|
66
|
-
'cache',
|
|
67
|
-
'danger',
|
|
68
|
-
'flags',
|
|
69
|
-
'release',
|
|
39
|
+
export const SETTINGS_CATEGORY_GROUPS: ReadonlyArray<{
|
|
40
|
+
readonly label: string;
|
|
41
|
+
readonly categories: readonly SettingsCategory[];
|
|
42
|
+
}> = [
|
|
43
|
+
{ label: 'Interface', categories: ['display', 'ui', 'behavior', 'permissions'] },
|
|
44
|
+
{ label: 'AI Routing', categories: ['provider', 'subscriptions', 'helper', 'tools', 'tts'] },
|
|
45
|
+
{ label: 'Service & Network', categories: ['service', 'network', 'controlPlane', 'httpListener', 'web'] },
|
|
46
|
+
{ label: 'Surfaces & Cloud', categories: ['surfaces', 'mcp', 'cloudflare'] },
|
|
47
|
+
{ label: 'Automation', categories: ['batch', 'automation', 'watchers', 'orchestration', 'wrfc'] },
|
|
48
|
+
{ label: 'Runtime & Data', categories: ['storage', 'sandbox', 'runtime', 'cache', 'telemetry'] },
|
|
49
|
+
{ label: 'Advanced', categories: ['flags', 'release', 'danger'] },
|
|
70
50
|
];
|
|
71
51
|
|
|
52
|
+
export const SETTINGS_CATEGORIES: SettingsCategory[] = SETTINGS_CATEGORY_GROUPS.flatMap(group => group.categories);
|
|
53
|
+
|
|
72
54
|
export interface SettingEntry {
|
|
73
55
|
setting: ConfigSetting;
|
|
74
56
|
currentValue: unknown;
|
|
@@ -34,6 +34,7 @@ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
34
34
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
35
35
|
import {
|
|
36
36
|
SETTINGS_CATEGORIES,
|
|
37
|
+
SETTINGS_CATEGORY_GROUPS,
|
|
37
38
|
type FlagEntry,
|
|
38
39
|
type McpEntry,
|
|
39
40
|
type SettingEntry,
|
|
@@ -60,6 +61,7 @@ export interface SettingsModalOpenOptions {
|
|
|
60
61
|
|
|
61
62
|
export {
|
|
62
63
|
SETTINGS_CATEGORIES,
|
|
64
|
+
SETTINGS_CATEGORY_GROUPS,
|
|
63
65
|
type FlagEntry,
|
|
64
66
|
type McpEntry,
|
|
65
67
|
type SettingEntry,
|
|
@@ -158,7 +160,7 @@ export class SettingsModal {
|
|
|
158
160
|
this._loadSubscriptionEntries();
|
|
159
161
|
this.categoryIndex = 0;
|
|
160
162
|
this.selectedIndex = 0;
|
|
161
|
-
this.focusPane = '
|
|
163
|
+
this.focusPane = 'categories';
|
|
162
164
|
this.editingMode = false;
|
|
163
165
|
this.editBuffer = '';
|
|
164
166
|
this.pendingModelPickerTarget = null;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { Line } from '../types/grid.ts';
|
|
9
9
|
import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
10
10
|
import type { SettingsModal, SettingEntry, FlagEntry, McpEntry, SubscriptionEntry, SettingsCategory } from '../input/settings-modal.ts';
|
|
11
|
-
import { SETTINGS_CATEGORIES } from '../input/settings-modal.ts';
|
|
11
|
+
import { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_GROUPS } from '../input/settings-modal.ts';
|
|
12
12
|
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
13
13
|
import { CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
|
|
14
14
|
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
@@ -413,19 +413,53 @@ function categoryItemCount(modal: SettingsModal, category: SettingsCategory): nu
|
|
|
413
413
|
return modal.groups.get(category)?.length ?? 0;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
416
|
+
type CategoryRailEntry =
|
|
417
|
+
| { readonly type: 'group'; readonly label: string }
|
|
418
|
+
| { readonly type: 'category'; readonly category: SettingsCategory; readonly index: number };
|
|
419
|
+
|
|
420
|
+
type CategoryRailRow = {
|
|
421
|
+
readonly text: string;
|
|
422
|
+
readonly type: CategoryRailEntry['type'] | 'more' | 'empty';
|
|
423
|
+
readonly selected: boolean;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
function buildCategoryRailEntries(): CategoryRailEntry[] {
|
|
427
|
+
const entries: CategoryRailEntry[] = [];
|
|
428
|
+
for (const group of SETTINGS_CATEGORY_GROUPS) {
|
|
429
|
+
const categories = group.categories.filter(category => SETTINGS_CATEGORIES.includes(category));
|
|
430
|
+
if (categories.length === 0) continue;
|
|
431
|
+
entries.push({ type: 'group', label: group.label });
|
|
432
|
+
for (const category of categories) {
|
|
433
|
+
entries.push({
|
|
434
|
+
type: 'category',
|
|
435
|
+
category,
|
|
436
|
+
index: SETTINGS_CATEGORIES.indexOf(category),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return entries;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function renderCategories(modal: SettingsModal, width: number, height: number): CategoryRailRow[] {
|
|
444
|
+
const rows: CategoryRailRow[] = [];
|
|
445
|
+
const entries = buildCategoryRailEntries();
|
|
446
|
+
const selectedEntryIndex = Math.max(0, entries.findIndex(entry => entry.type === 'category' && entry.index === modal.categoryIndex));
|
|
447
|
+
const window = stableWindow(entries.length, selectedEntryIndex, height);
|
|
448
|
+
if (window.start > 0) rows.push({ text: `${GLYPHS.navigation.moreAbove} ${window.start} more row(s) above`, type: 'more', selected: false });
|
|
449
|
+
for (let railIndex = window.start; railIndex < window.end; railIndex += 1) {
|
|
450
|
+
const entry = entries[railIndex]!;
|
|
451
|
+
if (entry.type === 'group') {
|
|
452
|
+
rows.push({ text: entry.label.toUpperCase(), type: 'group', selected: false });
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const category = entry.category;
|
|
456
|
+
const active = entry.index === modal.categoryIndex;
|
|
423
457
|
const count = categoryItemCount(modal, category);
|
|
424
458
|
const cursor = active ? (modal.focusPane === 'categories' ? GLYPHS.navigation.selected : '•') : ' ';
|
|
425
|
-
rows.push(
|
|
459
|
+
rows.push({ text: ` ${cursor} ${CATEGORY_LABELS[category]} (${count})`, type: 'category', selected: active });
|
|
426
460
|
}
|
|
427
|
-
if (window.end <
|
|
428
|
-
while (rows.length < height) rows.push('');
|
|
461
|
+
if (window.end < entries.length) rows.push({ text: `${GLYPHS.navigation.moreBelow} ${entries.length - window.end} more row(s) below`, type: 'more', selected: false });
|
|
462
|
+
while (rows.length < height) rows.push({ text: '', type: 'empty', selected: false });
|
|
429
463
|
return rows.slice(0, height);
|
|
430
464
|
}
|
|
431
465
|
|
|
@@ -615,13 +649,12 @@ export function renderSettingsModal(
|
|
|
615
649
|
fillRange(line, 1, dividerX - 1, PALETTE.categoryBg);
|
|
616
650
|
drawVertical(line, dividerX, bg);
|
|
617
651
|
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
bold: categoryActive,
|
|
652
|
+
const categoryRow = categoryRows[row] ?? { text: '', type: 'empty' as const, selected: false };
|
|
653
|
+
if (categoryRow.selected) fillRange(line, leftStart, dividerX - 1, PALETTE.selectedBg);
|
|
654
|
+
writeText(line, leftStart + 1, leftWidth - 3, categoryRow.text, {
|
|
655
|
+
fg: categoryRow.selected ? PALETTE.text : categoryRow.type === 'group' ? PALETTE.subtitle : PALETTE.muted,
|
|
656
|
+
bg: categoryRow.selected ? PALETTE.selectedBg : PALETTE.categoryBg,
|
|
657
|
+
bold: categoryRow.selected || categoryRow.type === 'group',
|
|
625
658
|
});
|
|
626
659
|
|
|
627
660
|
if (inSeparator) {
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.19.
|
|
9
|
+
let _version = '0.19.67';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|