cvc-tui 0.4.5 → 0.4.7
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.
|
@@ -2,6 +2,20 @@ import { applyDelegationStatus, getDelegationState } from '../../delegationStore
|
|
|
2
2
|
import { patchOverlayState } from '../../overlayStore.js';
|
|
3
3
|
import { getSpawnHistory, pushDiskSnapshot, setDiffPair } from '../../spawnHistoryStore.js';
|
|
4
4
|
export const opsCommands = [
|
|
5
|
+
{
|
|
6
|
+
help: 'start (or restart) the local gateway',
|
|
7
|
+
name: 'start',
|
|
8
|
+
run: (_arg, ctx) => {
|
|
9
|
+
ctx.transcript.sys('Starting gateway…');
|
|
10
|
+
try {
|
|
11
|
+
ctx.gateway.gw.start();
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
15
|
+
ctx.transcript.sys(`Failed to start gateway: ${msg}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
5
19
|
{
|
|
6
20
|
help: 'stop background processes',
|
|
7
21
|
name: 'stop',
|
package/dist/app/useMainApp.js
CHANGED
|
@@ -450,11 +450,20 @@ export function useMainApp(gw) {
|
|
|
450
450
|
onEventRef.current = onEvent;
|
|
451
451
|
useEffect(() => {
|
|
452
452
|
const handler = (ev) => onEventRef.current(ev);
|
|
453
|
+
// Track whether we've already surfaced a gateway-exit notice so we don't
|
|
454
|
+
// spam the activity log with red walls when the gateway is offline.
|
|
455
|
+
let gatewayExitedShown = false;
|
|
453
456
|
const exitHandler = () => {
|
|
454
457
|
turnController.reset();
|
|
455
|
-
patchUiState({
|
|
456
|
-
|
|
457
|
-
|
|
458
|
+
patchUiState({
|
|
459
|
+
busy: false,
|
|
460
|
+
sid: null,
|
|
461
|
+
status: 'offline — type /start to launch gateway'
|
|
462
|
+
});
|
|
463
|
+
if (!gatewayExitedShown) {
|
|
464
|
+
gatewayExitedShown = true;
|
|
465
|
+
turnController.pushActivity('gateway offline · type /start to launch · /logs to inspect', 'info');
|
|
466
|
+
}
|
|
458
467
|
};
|
|
459
468
|
gw.on('event', handler);
|
|
460
469
|
gw.on('exit', exitHandler);
|
|
@@ -18,6 +18,7 @@ import { AgentsOverlay } from './agentsOverlay.js';
|
|
|
18
18
|
import { GoodVibesHeart, StatusRule, StickyPromptTracker, TranscriptScrollbar } from './appChrome.js';
|
|
19
19
|
import { FloatingOverlays, PromptZone } from './appOverlays.js';
|
|
20
20
|
import { Banner, Panel, SessionPanel } from './branding.js';
|
|
21
|
+
import { buildLocalSessionInfo } from '../lib/localSessionInfo.js';
|
|
21
22
|
import { FpsOverlay } from './fpsOverlay.js';
|
|
22
23
|
import { HelpHint } from './helpHint.js';
|
|
23
24
|
import { MessageLine } from './messageLine.js';
|
|
@@ -51,7 +52,7 @@ const TranscriptPane = memo(function TranscriptPane({ actions, composer, progres
|
|
|
51
52
|
if (e.cellIsBlank) {
|
|
52
53
|
actions.clearSelection();
|
|
53
54
|
}
|
|
54
|
-
}, ref: transcript.scrollRef, stickyScroll: true, children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [transcript.virtualHistory.topSpacer > 0 ? _jsx(Box, { height: transcript.virtualHistory.topSpacer }) : null, transcript.virtualRows.slice(transcript.virtualHistory.start, transcript.virtualHistory.end).map(row => (_jsxs(Box, { flexDirection: "column", ref: transcript.virtualHistory.measureRef(row.key), children: [row.msg.role === 'user' && firstUserIdx >= 0 && row.index > firstUserIdx && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: ui.theme.color.border, children: "\u2500\u2500\u2500" }) })), row.msg.kind === 'intro' ? (_jsxs(Box, { flexDirection: "column", paddingTop: 1, children: [_jsx(Banner, { t: ui.theme }),
|
|
55
|
+
}, ref: transcript.scrollRef, stickyScroll: true, children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [transcript.virtualHistory.topSpacer > 0 ? _jsx(Box, { height: transcript.virtualHistory.topSpacer }) : null, transcript.virtualRows.slice(transcript.virtualHistory.start, transcript.virtualHistory.end).map(row => (_jsxs(Box, { flexDirection: "column", ref: transcript.virtualHistory.measureRef(row.key), children: [row.msg.role === 'user' && firstUserIdx >= 0 && row.index > firstUserIdx && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: ui.theme.color.border, children: "\u2500\u2500\u2500" }) })), row.msg.kind === 'intro' ? (_jsxs(Box, { flexDirection: "column", paddingTop: 1, children: [_jsx(Banner, { t: ui.theme }), _jsx(SessionPanel, { info: row.msg.info ?? buildLocalSessionInfo(), sid: ui.sid, t: ui.theme })] })) : row.msg.kind === 'panel' && row.msg.panelData ? (_jsx(Panel, { sections: row.msg.panelData.sections, t: ui.theme, title: row.msg.panelData.title })) : (_jsx(MessageLine, { cols: composer.cols, compact: ui.compact, detailsMode: ui.detailsMode, detailsModeCommandOverride: ui.detailsModeCommandOverride, limitHistoryRender: row.index < transcript.historyItems.length - FULL_RENDER_TAIL_ITEMS, msg: row.msg, sections: ui.sections, t: ui.theme })), row.index === lastUserIdx && _jsx(LiveTodoPanel, {})] }, row.key))), transcript.virtualHistory.bottomSpacer > 0 ? _jsx(Box, { height: transcript.virtualHistory.bottomSpacer }) : null, _jsx(StreamingAssistant, { cols: composer.cols, compact: ui.compact, detailsMode: ui.detailsMode, detailsModeCommandOverride: ui.detailsModeCommandOverride, progress: progress, sections: ui.sections })] }) }), _jsx(NoSelect, { flexShrink: 0, marginLeft: 1, children: _jsx(TranscriptScrollbar, { scrollRef: transcript.scrollRef, t: ui.theme }) }), _jsx(StickyPromptTracker, { messages: transcript.historyItems, offsets: transcript.virtualHistory.offsets, onChange: actions.setStickyPrompt, scrollRef: transcript.scrollRef })] }));
|
|
55
56
|
});
|
|
56
57
|
const ComposerPane = memo(function ComposerPane({ actions, composer, status }) {
|
|
57
58
|
const ui = useStore($uiState);
|
|
@@ -25,7 +25,7 @@ export function ArtLines({ lines }) {
|
|
|
25
25
|
export function Banner({ t }) {
|
|
26
26
|
const cols = useStdout().stdout?.columns ?? 80;
|
|
27
27
|
const logoLines = logo(t.color, t.bannerLogo || undefined);
|
|
28
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [cols >= (t.bannerLogo ? artWidth(logoLines) : LOGO_WIDTH) ? (_jsx(ArtLines, { lines: logoLines })) : (
|
|
28
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [cols >= (t.bannerLogo ? artWidth(logoLines) : LOGO_WIDTH) ? (_jsx(ArtLines, { lines: logoLines })) : (_jsx(Text, { bold: true, color: t.color.primary, children: `${t.brand.icon} CVC` })), _jsx(Text, { color: t.color.muted, children: `${t.brand.icon} Cognitive Version Control · git for your AI's mind` })] }));
|
|
29
29
|
}
|
|
30
30
|
// ── Collapsible helpers ──────────────────────────────────────────────
|
|
31
31
|
function CollapseToggle({ count, open, suffix, t, title, onToggle }) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Builds an offline SessionInfo from local config + filesystem so the TUI
|
|
3
|
+
// welcome screen renders rich content even when the gateway is down.
|
|
4
|
+
//
|
|
5
|
+
// The gateway's session.info RPC is the ideal source of truth (it knows the
|
|
6
|
+
// active model, full skill registry, MCP statuses, etc.). When it isn't
|
|
7
|
+
// reachable we degrade gracefully instead of showing a wasteland.
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
const SAFE_TOOL_DIRS = new Set(['__pycache__']);
|
|
12
|
+
function readYamlMinimal(path) {
|
|
13
|
+
// Tiny single-level YAML reader — enough for `key: value` lines.
|
|
14
|
+
// We deliberately avoid pulling in `js-yaml` here; the welcome screen
|
|
15
|
+
// is rendered before any heavy deps load.
|
|
16
|
+
try {
|
|
17
|
+
const raw = readFileSync(path, 'utf-8');
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const line of raw.split('\n')) {
|
|
20
|
+
const m = line.match(/^([a-z_]+):\s*(.+?)\s*$/i);
|
|
21
|
+
if (!m)
|
|
22
|
+
continue;
|
|
23
|
+
const [, k, v] = m;
|
|
24
|
+
const cleaned = v.replace(/^['"]|['"]$/g, '');
|
|
25
|
+
out[k] = cleaned;
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function listDirs(path) {
|
|
34
|
+
try {
|
|
35
|
+
return readdirSync(path)
|
|
36
|
+
.filter(name => !name.startsWith('.') && !SAFE_TOOL_DIRS.has(name))
|
|
37
|
+
.filter(name => {
|
|
38
|
+
try {
|
|
39
|
+
return statSync(join(path, name)).isDirectory();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
.sort();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function listToolFiles(path) {
|
|
52
|
+
try {
|
|
53
|
+
return readdirSync(path)
|
|
54
|
+
.filter(name => name.endsWith('.py') && !name.startsWith('_'))
|
|
55
|
+
.map(name => name.slice(0, -3))
|
|
56
|
+
.sort();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function findCvcSitePackages() {
|
|
63
|
+
// Prefer uv-tool install layout, then a couple of common fallbacks.
|
|
64
|
+
const candidates = [
|
|
65
|
+
join(homedir(), '.local/share/uv/tools/tm-ai/lib/python3.12/site-packages/cvc'),
|
|
66
|
+
join(homedir(), '.local/share/uv/tools/tm-ai/lib/python3.13/site-packages/cvc'),
|
|
67
|
+
join(homedir(), '.local/share/uv/tools/tm-ai/lib/python3.11/site-packages/cvc'),
|
|
68
|
+
];
|
|
69
|
+
for (const c of candidates) {
|
|
70
|
+
if (existsSync(c))
|
|
71
|
+
return c;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function readCvcVersion() {
|
|
76
|
+
// CVC_VERSION is set by the Python launcher (cvc/cli/agent.py).
|
|
77
|
+
const envV = process.env.CVC_VERSION;
|
|
78
|
+
if (envV)
|
|
79
|
+
return envV;
|
|
80
|
+
return '0.0.0+unknown';
|
|
81
|
+
}
|
|
82
|
+
export function buildLocalSessionInfo() {
|
|
83
|
+
const configDir = process.env.CVC_CONFIG_DIR || join(homedir(), '.cvc');
|
|
84
|
+
const cfg = readYamlMinimal(join(configDir, 'config.yaml'));
|
|
85
|
+
const sitePkg = findCvcSitePackages();
|
|
86
|
+
const tools = {};
|
|
87
|
+
const skills = {};
|
|
88
|
+
if (sitePkg) {
|
|
89
|
+
// Top-level tool modules grouped under `core`
|
|
90
|
+
const coreTools = listToolFiles(join(sitePkg, 'tools'));
|
|
91
|
+
if (coreTools.length)
|
|
92
|
+
tools.core = coreTools;
|
|
93
|
+
// Tool subdirectories (browser, file_ops, web, messaging, media, skills)
|
|
94
|
+
for (const sub of listDirs(join(sitePkg, 'tools'))) {
|
|
95
|
+
const items = listToolFiles(join(sitePkg, 'tools', sub));
|
|
96
|
+
if (items.length)
|
|
97
|
+
tools[sub] = items;
|
|
98
|
+
}
|
|
99
|
+
// Bundled skills — each subdir is a skill name; group under `bundled`.
|
|
100
|
+
const bundled = listDirs(join(sitePkg, 'bundled_skills'));
|
|
101
|
+
if (bundled.length)
|
|
102
|
+
skills.bundled = bundled;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
cwd: process.cwd(),
|
|
106
|
+
lazy: false,
|
|
107
|
+
mcp_servers: [],
|
|
108
|
+
model: cfg.default_model
|
|
109
|
+
? `${cfg.primary_provider ?? 'local'}/${cfg.default_model}`
|
|
110
|
+
: 'unknown/offline',
|
|
111
|
+
skills,
|
|
112
|
+
tools,
|
|
113
|
+
update_command: 'cvc update',
|
|
114
|
+
version: readCvcVersion(),
|
|
115
|
+
};
|
|
116
|
+
}
|
package/package.json
CHANGED