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',
@@ -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({ busy: false, sid: null, status: 'gateway exited' });
456
- turnController.pushActivity('gateway exited · /logs to inspect', 'error');
457
- sys('error: gateway exited');
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 }), row.msg.info && _jsx(SessionPanel, { info: row.msg.info, 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
+ }, 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 })) : (_jsxs(Text, { bold: true, color: t.color.primary, children: [t.brand.icon, " CVC"] })), _jsxs(Text, { color: t.color.muted, children: [t.brand.icon, " Cognitive Version Control \u00B7 git for your AI's mind"] })] }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cvc-tui",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "CVC \u2014 Cognitive Version Control terminal UI (Ink + React 19). Sidecar binary embedded in the cvc Python wheel.",