dev-cockpit 0.1.0 → 0.2.1
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/README.md +64 -29
- package/bin/dev-cockpit.mjs +26 -4
- package/dist/actions/builtin.d.ts +25 -0
- package/dist/actions/builtin.d.ts.map +1 -0
- package/dist/actions/dispatch.d.ts +21 -0
- package/dist/actions/dispatch.d.ts.map +1 -0
- package/dist/actions/registry.d.ts +11 -0
- package/dist/actions/registry.d.ts.map +1 -0
- package/dist/actions/types.d.ts +76 -0
- package/dist/actions/types.d.ts.map +1 -0
- package/dist/buildCli.d.ts.map +1 -1
- package/dist/chunk-6XGHLLYT.js +46 -0
- package/dist/chunk-6XGHLLYT.js.map +7 -0
- package/dist/chunk-Q6677JQF.js +32609 -0
- package/dist/chunk-Q6677JQF.js.map +7 -0
- package/dist/chunk-VN6UILQW.js +1460 -0
- package/dist/chunk-VN6UILQW.js.map +7 -0
- package/dist/cockpit/Cockpit.d.ts +6 -0
- package/dist/cockpit/Cockpit.d.ts.map +1 -1
- package/dist/cockpit/Footer.d.ts +6 -4
- package/dist/cockpit/Footer.d.ts.map +1 -1
- package/dist/cockpit/TabBar.d.ts.map +1 -1
- package/dist/cockpit/hooks/useGlobalKeys.d.ts +15 -15
- package/dist/cockpit/hooks/useGlobalKeys.d.ts.map +1 -1
- package/dist/cockpit/hooks/useTerminalWidth.d.ts +12 -0
- package/dist/cockpit/hooks/useTerminalWidth.d.ts.map +1 -0
- package/dist/cockpit/panes/CommandModal.d.ts +18 -0
- package/dist/cockpit/panes/CommandModal.d.ts.map +1 -0
- package/dist/cockpit/panes/Help.d.ts.map +1 -1
- package/dist/cockpit/panes/Output.d.ts +7 -0
- package/dist/cockpit/panes/Output.d.ts.map +1 -1
- package/dist/cockpit/panes/Repos.d.ts.map +1 -1
- package/dist/cockpit/state/store.d.ts +14 -11
- package/dist/cockpit/state/store.d.ts.map +1 -1
- package/dist/cockpit/tab-state.d.ts +12 -0
- package/dist/cockpit/tab-state.d.ts.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/init-config-wizard.d.ts +103 -2
- package/dist/commands/init-config-wizard.d.ts.map +1 -1
- package/dist/commands/init-config.d.ts.map +1 -1
- package/dist/commands/migrate-config.d.ts +18 -0
- package/dist/commands/migrate-config.d.ts.map +1 -0
- package/dist/commands/mount.d.ts +17 -32
- package/dist/commands/mount.d.ts.map +1 -1
- package/dist/core/config.d.ts +73 -5
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/migrations.d.ts +33 -0
- package/dist/core/migrations.d.ts.map +1 -0
- package/dist/core/subprocess.d.ts +20 -0
- package/dist/core/subprocess.d.ts.map +1 -1
- package/dist/core/types.d.ts +36 -12
- package/dist/core/types.d.ts.map +1 -1
- package/dist/devtools-YXMW6JJ6.js +3720 -0
- package/dist/devtools-YXMW6JJ6.js.map +7 -0
- package/dist/docker/highlights.d.ts +14 -4
- package/dist/docker/highlights.d.ts.map +1 -1
- package/dist/docker/logs.d.ts +3 -2
- package/dist/docker/logs.d.ts.map +1 -1
- package/dist/health/builtin.d.ts.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92837 -53
- package/dist/index.js.map +7 -0
- package/dist/ink.js +38 -1
- package/dist/ink.js.map +7 -0
- package/dist/mount/compose.d.ts +21 -0
- package/dist/mount/compose.d.ts.map +1 -0
- package/dist/mount/discovery.d.ts +35 -0
- package/dist/mount/discovery.d.ts.map +1 -0
- package/dist/mount/git-status.d.ts +12 -0
- package/dist/mount/git-status.d.ts.map +1 -0
- package/dist/mount/manifest.d.ts +16 -0
- package/dist/mount/manifest.d.ts.map +1 -0
- package/dist/mount/symlinks.d.ts +30 -0
- package/dist/mount/symlinks.d.ts.map +1 -0
- package/dist/mount/types.d.ts +60 -0
- package/dist/mount/types.d.ts.map +1 -0
- package/dist/react.js +35 -1
- package/dist/react.js.map +7 -0
- package/dist/runCockpit.d.ts +3 -0
- package/dist/runCockpit.d.ts.map +1 -1
- package/docs/commands.md +29 -16
- package/docs/config-reference.md +115 -11
- package/docs/getting-started.md +9 -6
- package/docs/index.md +5 -1
- package/docs/init-config.md +34 -8
- package/docs/mount.md +198 -25
- package/docs/notifications.md +14 -13
- package/docs/panes.md +36 -15
- package/docs/processes.md +42 -0
- package/package.json +93 -90
- package/dist/buildCli.js +0 -107
- package/dist/cli.js +0 -2
- package/dist/cockpit/Cockpit.js +0 -73
- package/dist/cockpit/Footer.js +0 -33
- package/dist/cockpit/TabBar.js +0 -12
- package/dist/cockpit/help/content.js +0 -22
- package/dist/cockpit/help/loader.js +0 -118
- package/dist/cockpit/help/renderer.js +0 -35
- package/dist/cockpit/help/types.js +0 -1
- package/dist/cockpit/hooks/useCockpitStore.js +0 -5
- package/dist/cockpit/hooks/useGlobalKeys.js +0 -173
- package/dist/cockpit/panes/FilterModal.js +0 -22
- package/dist/cockpit/panes/Health.js +0 -30
- package/dist/cockpit/panes/Help.js +0 -81
- package/dist/cockpit/panes/Output.js +0 -108
- package/dist/cockpit/panes/Repos.js +0 -48
- package/dist/cockpit/panes/SearchModal.js +0 -31
- package/dist/cockpit/state/store.js +0 -111
- package/dist/cockpit/tab-state.js +0 -7
- package/dist/commands/dev.js +0 -158
- package/dist/commands/doctor.js +0 -66
- package/dist/commands/init-config-wizard.js +0 -818
- package/dist/commands/init-config.js +0 -131
- package/dist/commands/mount.js +0 -150
- package/dist/core/config.js +0 -152
- package/dist/core/logger.js +0 -38
- package/dist/core/notifier.js +0 -100
- package/dist/core/paths.js +0 -18
- package/dist/core/subprocess.js +0 -82
- package/dist/core/types.js +0 -1
- package/dist/docker/highlights.js +0 -79
- package/dist/docker/logs.js +0 -172
- package/dist/docker/restart.js +0 -45
- package/dist/docker/stack-trace.js +0 -44
- package/dist/health/builtin.js +0 -144
- package/dist/health/context.js +0 -31
- package/dist/health/notify-resolver.js +0 -28
- package/dist/health/registry.js +0 -64
- package/dist/health/remediations.js +0 -41
- package/dist/health/runner.js +0 -22
- package/dist/health/scheduler.js +0 -107
- package/dist/health/types.js +0 -1
- package/dist/health/useHealth.js +0 -122
- package/dist/lint/reactive.js +0 -131
- package/dist/runCockpit.js +0 -75
- package/dist/watchers/manager.js +0 -239
- package/dist/watchers/path-mapper.js +0 -29
- package/dist/watchers/types.js +0 -9
- package/docs/watchers.md +0 -27
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global keyboard handler for the cockpit TUI.
|
|
3
|
-
*
|
|
4
|
-
* Globals (active in every tab):
|
|
5
|
-
* ← / → cycle tabs (backward / forward)
|
|
6
|
-
* tab / shift+tab cycle tabs (alias) — except inside Help, where these
|
|
7
|
-
* are repurposed for page navigation. Use ←/→ to leave.
|
|
8
|
-
* q onQuit()
|
|
9
|
-
* n toggleSessionNotifications
|
|
10
|
-
*
|
|
11
|
-
* Per-tab keys are dispatched to the relevant handler callback. Domain-aware
|
|
12
|
-
* behavior (rebuild, lint, remediation lookup, editor launch) lives in the
|
|
13
|
-
* profile / consumer; this hook just routes keystrokes to the handlers and
|
|
14
|
-
* manages the minimum-visibility-hold for in-flight banners.
|
|
15
|
-
*/
|
|
16
|
-
import { useInput } from 'ink';
|
|
17
|
-
import { cockpitStore } from '../state/store.js';
|
|
18
|
-
/**
|
|
19
|
-
* Minimum time an in-flight-action banner stays visible, even if the
|
|
20
|
-
* underlying work completes faster. Without this, fast-finishing actions
|
|
21
|
-
* (instant failures, no-op installs) flash by unnoticed.
|
|
22
|
-
*/
|
|
23
|
-
const MIN_ACTION_BANNER_MS = 1500;
|
|
24
|
-
function withMinHold(promise, onDone) {
|
|
25
|
-
const startedAt = Date.now();
|
|
26
|
-
void promise.finally(() => {
|
|
27
|
-
const elapsed = Date.now() - startedAt;
|
|
28
|
-
const remaining = Math.max(0, MIN_ACTION_BANNER_MS - elapsed);
|
|
29
|
-
if (remaining === 0)
|
|
30
|
-
onDone();
|
|
31
|
-
else
|
|
32
|
-
setTimeout(onDone, remaining);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
export function useGlobalKeys(opts) {
|
|
36
|
-
const { onQuit, runRepoAction, onWatchToggle, onLint, onOpenError, runRemediation } = opts;
|
|
37
|
-
useInput((input, key) => {
|
|
38
|
-
const state = cockpitStore.getState();
|
|
39
|
-
// Modal is open — don't process global keys.
|
|
40
|
-
if (state.activeModal !== null)
|
|
41
|
-
return;
|
|
42
|
-
// Tab navigation (always available)
|
|
43
|
-
if (key.leftArrow) {
|
|
44
|
-
state.cycleFocus('backward');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
if (key.rightArrow) {
|
|
48
|
-
state.cycleFocus('forward');
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
// Tab / Shift-Tab cycle tabs globally — but inside Help they're aliased
|
|
52
|
-
// to page navigation by the Help pane itself.
|
|
53
|
-
if (state.focus !== 'help') {
|
|
54
|
-
if (key.tab && key.shift) {
|
|
55
|
-
state.cycleFocus('backward');
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (key.tab) {
|
|
59
|
-
state.cycleFocus('forward');
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (input === 'q') {
|
|
64
|
-
onQuit();
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (input === 'n') {
|
|
68
|
-
state.toggleSessionNotifications();
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
// ── Repos ─────────────────────────────────────────────────────────────
|
|
72
|
-
if (state.focus === 'repos') {
|
|
73
|
-
if (key.upArrow) {
|
|
74
|
-
state.moveSelection('up');
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
if (key.downArrow) {
|
|
78
|
-
state.moveSelection('down');
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const selectedKey = state.repoOrder[state.selectedRepoIndex];
|
|
82
|
-
const selectedEntry = selectedKey ? state.repos[selectedKey] : undefined;
|
|
83
|
-
if (input === 'r') {
|
|
84
|
-
if (!selectedKey || !selectedEntry || !runRepoAction)
|
|
85
|
-
return;
|
|
86
|
-
const dispatch = runRepoAction(selectedKey, selectedEntry);
|
|
87
|
-
if (!dispatch)
|
|
88
|
-
return;
|
|
89
|
-
state.setActiveRepoAction({ repoKey: selectedKey, label: dispatch.label });
|
|
90
|
-
withMinHold(dispatch.promise, () => cockpitStore.getState().setActiveRepoAction(null));
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
if (input === 'w') {
|
|
94
|
-
if (selectedKey && selectedEntry && selectedEntry.kind === 'repo' && onWatchToggle) {
|
|
95
|
-
onWatchToggle(selectedKey, selectedEntry);
|
|
96
|
-
}
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (input === 'l') {
|
|
100
|
-
if (selectedKey && selectedEntry && selectedEntry.kind === 'repo' && onLint) {
|
|
101
|
-
onLint(selectedKey, selectedEntry);
|
|
102
|
-
}
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// ── Output ────────────────────────────────────────────────────────────
|
|
107
|
-
if (state.focus === 'output') {
|
|
108
|
-
if (input === 'c') {
|
|
109
|
-
state.clearOutput();
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (input === '/') {
|
|
113
|
-
state.setActiveModal('search');
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (input === 'f') {
|
|
117
|
-
state.setActiveModal('filter');
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (input === 'e' && onOpenError) {
|
|
121
|
-
const target = state.recentErrors.find((er) => er.file && typeof er.line === 'number');
|
|
122
|
-
if (target?.file && typeof target.line === 'number') {
|
|
123
|
-
onOpenError({
|
|
124
|
-
file: target.file,
|
|
125
|
-
line: target.line,
|
|
126
|
-
service: target.service,
|
|
127
|
-
text: target.text,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
onOpenError(null);
|
|
132
|
-
}
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
// ── Health ────────────────────────────────────────────────────────────
|
|
137
|
-
if (state.focus === 'health') {
|
|
138
|
-
if (key.upArrow) {
|
|
139
|
-
state.moveHealthSelection('up');
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (key.downArrow) {
|
|
143
|
-
state.moveHealthSelection('down');
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (key.return) {
|
|
147
|
-
const selectedCheck = state.health[state.selectedHealthIndex];
|
|
148
|
-
if (selectedCheck) {
|
|
149
|
-
state.toggleHealthExpand(selectedCheck.id);
|
|
150
|
-
}
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
// Any single letter is treated as a potential remediation key.
|
|
154
|
-
// The input is uppercased so users can press the key without Shift —
|
|
155
|
-
// a remediation declared with `key: 'R'` matches both `r` and `R`.
|
|
156
|
-
const upperInput = input.toUpperCase();
|
|
157
|
-
if (runRemediation &&
|
|
158
|
-
upperInput.length === 1 &&
|
|
159
|
-
upperInput >= 'A' &&
|
|
160
|
-
upperInput <= 'Z') {
|
|
161
|
-
const dispatch = runRemediation(upperInput);
|
|
162
|
-
if (!dispatch)
|
|
163
|
-
return;
|
|
164
|
-
state.setActiveRemediation({
|
|
165
|
-
healthId: dispatch.healthId,
|
|
166
|
-
healthLabel: dispatch.healthLabel,
|
|
167
|
-
});
|
|
168
|
-
withMinHold(dispatch.promise, () => cockpitStore.getState().setActiveRemediation(null));
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { cockpitStore } from '../state/store.js';
|
|
5
|
-
import { useCockpitStore } from '../hooks/useCockpitStore.js';
|
|
6
|
-
export function FilterModal() {
|
|
7
|
-
const outputFilter = useCockpitStore((s) => s.outputFilter);
|
|
8
|
-
const [severityDraft, setSeverityDraft] = useState(outputFilter.severity);
|
|
9
|
-
const SEVERITIES = [undefined, 'info', 'warn', 'error'];
|
|
10
|
-
useInput((input, key) => {
|
|
11
|
-
if (input === 's') {
|
|
12
|
-
const idx = SEVERITIES.indexOf(severityDraft);
|
|
13
|
-
const next = SEVERITIES[(idx + 1) % SEVERITIES.length];
|
|
14
|
-
setSeverityDraft(next);
|
|
15
|
-
}
|
|
16
|
-
if (key.return || key.escape) {
|
|
17
|
-
cockpitStore.getState().setFilter({ ...outputFilter, severity: severityDraft });
|
|
18
|
-
cockpitStore.getState().setActiveModal(null);
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
return (_jsx(Box, { flexGrow: 1, alignItems: "center", justifyContent: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, children: " Filter Output " }), _jsx(Text, { children: " " }), _jsxs(Box, { children: [_jsx(Text, { children: "Severity (s to cycle): " }), _jsx(Text, { color: "cyan", children: severityDraft ?? 'all' })] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Enter = apply \u00B7 Esc = cancel" })] }) }));
|
|
22
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import { useCockpitStore } from '../hooks/useCockpitStore.js';
|
|
4
|
-
const SEVERITY_GLYPH = { ok: '✓', warn: '⚠', error: '✗' };
|
|
5
|
-
const SEVERITY_COLOR = {
|
|
6
|
-
ok: 'green',
|
|
7
|
-
warn: 'yellow',
|
|
8
|
-
error: 'red',
|
|
9
|
-
};
|
|
10
|
-
export function Health() {
|
|
11
|
-
const health = useCockpitStore((s) => s.health);
|
|
12
|
-
const selectedHealthIndex = useCockpitStore((s) => s.selectedHealthIndex);
|
|
13
|
-
const expandedHealthId = useCockpitStore((s) => s.expandedHealthId);
|
|
14
|
-
const focus = useCockpitStore((s) => s.focus);
|
|
15
|
-
const notificationsEnabledSession = useCockpitStore((s) => s.notificationsEnabledSession);
|
|
16
|
-
const activeRemediation = useCockpitStore((s) => s.activeRemediation);
|
|
17
|
-
const isFocused = focus === 'health';
|
|
18
|
-
if (health.length === 0) {
|
|
19
|
-
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "running health checks\u2026" }) }));
|
|
20
|
-
}
|
|
21
|
-
return (_jsxs(Box, { flexDirection: "column", children: [activeRemediation && (_jsx(Box, { marginBottom: 1, paddingX: 1, children: _jsxs(Text, { color: "yellow", children: ['… ', activeRemediation.healthLabel, ' running — switch to Output (←) to watch'] }) })), health.map((item, idx) => {
|
|
22
|
-
const isSelected = isFocused && idx === selectedHealthIndex;
|
|
23
|
-
const isExpanded = expandedHealthId === item.id;
|
|
24
|
-
const isRunning = activeRemediation?.healthId === item.id;
|
|
25
|
-
const glyph = isRunning ? '…' : (SEVERITY_GLYPH[item.severity] ?? '?');
|
|
26
|
-
const color = isRunning ? 'yellow' : (SEVERITY_COLOR[item.severity] ?? 'white');
|
|
27
|
-
const remKey = item.remediationKey ? ` [${item.remediationKey}]` : '';
|
|
28
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? 'cyan' : undefined, children: isSelected ? '▶ ' : ' ' }), _jsxs(Text, { color: color, children: [glyph, " "] }), _jsx(Text, { bold: isSelected, children: item.label }), _jsx(Text, { dimColor: true, children: remKey }), isRunning && _jsx(Text, { color: "yellow", children: ' (running…)' })] }), isExpanded && (_jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, children: item.detail }) }))] }, item.id));
|
|
29
|
-
}), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ['notifications: ', _jsx(Text, { color: notificationsEnabledSession ? 'green' : 'yellow', children: notificationsEnabledSession ? 'on' : 'off (session)' })] }) })] }));
|
|
30
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* Help pane — reads pages merged via loader.ts (core docs + profile sources)
|
|
4
|
-
* and renders the active page through marked-terminal.
|
|
5
|
-
*
|
|
6
|
-
* Per-tab keys:
|
|
7
|
-
* ↑/↓ scroll body up/down
|
|
8
|
-
* j / k previous / next page
|
|
9
|
-
* shift+tab / tab same as j / k (alias for non-vim users; the global
|
|
10
|
-
* tab-cycle handler suspends on Help)
|
|
11
|
-
* g / G jump to top / bottom of body
|
|
12
|
-
*/
|
|
13
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
-
import { Box, Text, useInput, useStdout } from 'ink';
|
|
15
|
-
import { useCockpitStore } from '../hooks/useCockpitStore.js';
|
|
16
|
-
import { loadHelpPages } from '../help/loader.js';
|
|
17
|
-
import { renderMarkdown } from '../help/renderer.js';
|
|
18
|
-
import { FALLBACK_PAGE } from '../help/content.js';
|
|
19
|
-
export function Help() {
|
|
20
|
-
const focus = useCockpitStore((s) => s.focus);
|
|
21
|
-
const helpConfig = useCockpitStore((s) => s.helpConfig);
|
|
22
|
-
const { stdout } = useStdout();
|
|
23
|
-
const isActive = focus === 'help';
|
|
24
|
-
const initial = useMemo(() => {
|
|
25
|
-
const result = loadHelpPages({
|
|
26
|
-
sources: helpConfig.sources,
|
|
27
|
-
defaultPage: helpConfig.defaultPage,
|
|
28
|
-
});
|
|
29
|
-
if (result)
|
|
30
|
-
return result;
|
|
31
|
-
return { pages: [FALLBACK_PAGE], defaultIndex: 0 };
|
|
32
|
-
}, [helpConfig.sources, helpConfig.defaultPage]);
|
|
33
|
-
const pages = initial.pages;
|
|
34
|
-
const [pageIndex, setPageIndex] = useState(initial.defaultIndex);
|
|
35
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
36
|
-
const lastPageIndexRef = useRef(pageIndex);
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
if (lastPageIndexRef.current !== pageIndex) {
|
|
39
|
-
lastPageIndexRef.current = pageIndex;
|
|
40
|
-
setScrollOffset(0);
|
|
41
|
-
}
|
|
42
|
-
}, [pageIndex]);
|
|
43
|
-
const activePage = pages[pageIndex] ?? pages[0];
|
|
44
|
-
const renderedLines = useMemo(() => renderMarkdown(activePage.body).split('\n'), [activePage.body]);
|
|
45
|
-
const bodyHeight = Math.max(5, (stdout.rows ?? 24) - 8);
|
|
46
|
-
const maxOffset = Math.max(0, renderedLines.length - bodyHeight);
|
|
47
|
-
useInput((input, key) => {
|
|
48
|
-
if (key.upArrow) {
|
|
49
|
-
setScrollOffset((o) => Math.max(0, o - 1));
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (key.downArrow) {
|
|
53
|
-
setScrollOffset((o) => Math.min(maxOffset, o + 1));
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (input === 'j' || (key.tab && key.shift)) {
|
|
57
|
-
setPageIndex((i) => Math.max(0, i - 1));
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (input === 'k' || (key.tab && !key.shift)) {
|
|
61
|
-
setPageIndex((i) => Math.min(pages.length - 1, i + 1));
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (input === 'g') {
|
|
65
|
-
setScrollOffset(0);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (input === 'G') {
|
|
69
|
-
setScrollOffset(maxOffset);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
}, { isActive });
|
|
73
|
-
const visibleLines = renderedLines.slice(scrollOffset, scrollOffset + bodyHeight);
|
|
74
|
-
const scrollHint = renderedLines.length > bodyHeight
|
|
75
|
-
? `${scrollOffset + 1}-${Math.min(scrollOffset + bodyHeight, renderedLines.length)} / ${renderedLines.length}`
|
|
76
|
-
: `${renderedLines.length} lines`;
|
|
77
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingTop: 1, children: [_jsxs(Box, { flexDirection: "row", paddingX: 1, marginBottom: 1, children: [pages.map((p, idx) => {
|
|
78
|
-
const isCurrent = idx === pageIndex;
|
|
79
|
-
return (_jsx(Box, { marginRight: 2, children: _jsxs(Text, { inverse: isCurrent, bold: isCurrent, color: isCurrent ? 'cyan' : undefined, children: [' ', p.title, ' '] }) }, p.slug));
|
|
80
|
-
}), _jsx(Box, { flexGrow: 1, justifyContent: "flex-end", children: _jsxs(Text, { dimColor: true, children: [scrollHint, " \u00B7 [j/k or tab] page [\u2191\u2193] scroll [g/G] top/bot [\u2190\u2192] tabs"] }) })] }), _jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: _jsx(Text, { children: visibleLines.join('\n') }) })] }));
|
|
81
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* Output pane — streams source-tagged log lines.
|
|
4
|
-
*
|
|
5
|
-
* - Virtualized scrollback (hand-rolled slice against visible terminal height).
|
|
6
|
-
* - Per-source color prefixes (deterministic source → color hash).
|
|
7
|
-
* - outputFilter applied (sources, severity, search text).
|
|
8
|
-
* - Auto-scroll to bottom unless the user has scrolled up.
|
|
9
|
-
*
|
|
10
|
-
* Domain handlers (e.g. "open error in editor") are NOT imported here. They
|
|
11
|
-
* live in the global keybindings layer; this pane is purely presentational.
|
|
12
|
-
*/
|
|
13
|
-
import { useState } from 'react';
|
|
14
|
-
import { Box, Text, useStdout, useInput } from 'ink';
|
|
15
|
-
import chalk from 'chalk';
|
|
16
|
-
import { useCockpitStore } from '../hooks/useCockpitStore.js';
|
|
17
|
-
const SOURCE_COLORS = ['cyan', 'yellow', 'magenta', 'blue', 'green', 'red', 'white'];
|
|
18
|
-
const colorCache = new Map();
|
|
19
|
-
function sourceColor(source) {
|
|
20
|
-
const cached = colorCache.get(source);
|
|
21
|
-
if (cached)
|
|
22
|
-
return cached;
|
|
23
|
-
let hash = 0;
|
|
24
|
-
for (let i = 0; i < source.length; i++) {
|
|
25
|
-
hash = (hash * 31 + source.charCodeAt(i)) | 0;
|
|
26
|
-
}
|
|
27
|
-
const color = SOURCE_COLORS[Math.abs(hash) % SOURCE_COLORS.length] ?? 'white';
|
|
28
|
-
colorCache.set(source, color);
|
|
29
|
-
return color;
|
|
30
|
-
}
|
|
31
|
-
function applyFilter(lines, filter) {
|
|
32
|
-
let result = lines;
|
|
33
|
-
if (filter.sources && filter.sources.length > 0) {
|
|
34
|
-
const srcs = new Set(filter.sources);
|
|
35
|
-
result = result.filter((l) => srcs.has(l.source));
|
|
36
|
-
}
|
|
37
|
-
if (filter.severity) {
|
|
38
|
-
result = result.filter((l) => l.severity === filter.severity);
|
|
39
|
-
}
|
|
40
|
-
if (filter.search && filter.search.length > 0) {
|
|
41
|
-
const needle = filter.search.toLowerCase();
|
|
42
|
-
result = result.filter((l) => l.text.toLowerCase().includes(needle));
|
|
43
|
-
}
|
|
44
|
-
return result;
|
|
45
|
-
}
|
|
46
|
-
function severityColor(severity) {
|
|
47
|
-
switch (severity) {
|
|
48
|
-
case 'error':
|
|
49
|
-
return 'red';
|
|
50
|
-
case 'warn':
|
|
51
|
-
return 'yellow';
|
|
52
|
-
default:
|
|
53
|
-
return 'white';
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
function OutputLineRow({ line }) {
|
|
57
|
-
const color = sourceColor(line.source);
|
|
58
|
-
const prefix = chalk[color](`[${line.source}]`);
|
|
59
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { children: [prefix, " "] }), _jsx(Text, { color: severityColor(line.severity), children: line.text })] }));
|
|
60
|
-
}
|
|
61
|
-
const CHROME_LINES = 3;
|
|
62
|
-
const MIN_VISIBLE = 3;
|
|
63
|
-
export function Output() {
|
|
64
|
-
const { stdout } = useStdout();
|
|
65
|
-
const terminalHeight = stdout?.rows ?? 24;
|
|
66
|
-
const visibleLines = Math.max(MIN_VISIBLE, terminalHeight - CHROME_LINES * 3);
|
|
67
|
-
const output = useCockpitStore((s) => s.output);
|
|
68
|
-
const outputFilter = useCockpitStore((s) => s.outputFilter);
|
|
69
|
-
const focus = useCockpitStore((s) => s.focus);
|
|
70
|
-
const recentErrors = useCockpitStore((s) => s.recentErrors);
|
|
71
|
-
const isFocused = focus === 'output';
|
|
72
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
73
|
-
const filtered = applyFilter(output, outputFilter);
|
|
74
|
-
const total = filtered.length;
|
|
75
|
-
const effectiveOffset = Math.min(scrollOffset, Math.max(0, total - visibleLines));
|
|
76
|
-
const startIdx = Math.max(0, total - visibleLines - effectiveOffset);
|
|
77
|
-
const visible = filtered.slice(startIdx, startIdx + visibleLines);
|
|
78
|
-
const atBottom = effectiveOffset === 0 || total <= visibleLines;
|
|
79
|
-
useInput((_input, key) => {
|
|
80
|
-
if (!isFocused)
|
|
81
|
-
return;
|
|
82
|
-
if (key.upArrow) {
|
|
83
|
-
setScrollOffset((prev) => Math.min(prev + 1, Math.max(0, total - visibleLines)));
|
|
84
|
-
}
|
|
85
|
-
else if (key.downArrow) {
|
|
86
|
-
setScrollOffset((prev) => Math.max(0, prev - 1));
|
|
87
|
-
}
|
|
88
|
-
else if (key.pageUp) {
|
|
89
|
-
setScrollOffset((prev) => Math.min(prev + visibleLines, Math.max(0, total - visibleLines)));
|
|
90
|
-
}
|
|
91
|
-
else if (key.pageDown) {
|
|
92
|
-
setScrollOffset((prev) => Math.max(0, prev - visibleLines));
|
|
93
|
-
}
|
|
94
|
-
else if (key.return) {
|
|
95
|
-
setScrollOffset(0);
|
|
96
|
-
}
|
|
97
|
-
}, { isActive: isFocused });
|
|
98
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: isFocused, children: " Output " }), outputFilter.sources && outputFilter.sources.length > 0 && (_jsxs(Text, { dimColor: true, children: ["[src: ", outputFilter.sources.join(','), "] "] })), outputFilter.severity && _jsxs(Text, { dimColor: true, children: ["[", outputFilter.severity, "] "] }), outputFilter.search && _jsxs(Text, { dimColor: true, children: ["[/", outputFilter.search, "/] "] }), !atBottom && _jsxs(Text, { dimColor: true, children: [" \u2191 scrolled (", effectiveOffset, " lines up)"] })] }), recentErrors.length > 0 && _jsx(RecentErrorsStrip, { errors: recentErrors.slice(0, 3) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visible.length === 0 ? (_jsx(Text, { dimColor: true, children: " no output yet" })) : (visible.map((line, idx) => _jsx(OutputLineRow, { line: line }, `${line.ts}-${idx}`))) }), isFocused && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [' ↑↓ scroll · PgUp/PgDn · Enter=bottom · c=clear · /=search · f=filter', recentErrors.length > 0 ? ' · e=open error' : ''] }) }))] }));
|
|
99
|
-
}
|
|
100
|
-
function RecentErrorsStrip({ errors }) {
|
|
101
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "red", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Recent errors" }), errors.map((err, idx) => (_jsx(RecentErrorRow, { err: err }, `${err.ts}-${idx}`)))] }));
|
|
102
|
-
}
|
|
103
|
-
function RecentErrorRow({ err }) {
|
|
104
|
-
const sevColor = err.severity === 'error' ? 'red' : 'yellow';
|
|
105
|
-
const glyph = err.severity === 'error' ? '✗' : '⚠';
|
|
106
|
-
const trailer = err.file && typeof err.line === 'number' ? ` ↳ ${err.file}:${err.line}` : '';
|
|
107
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: sevColor, children: [glyph, " "] }), _jsxs(Text, { dimColor: true, children: ["[", err.service, "] "] }), _jsx(Text, { children: err.text }), trailer && _jsx(Text, { dimColor: true, children: trailer })] }));
|
|
108
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import { useCockpitStore } from '../hooks/useCockpitStore.js';
|
|
4
|
-
function statusGlyph(status) {
|
|
5
|
-
switch (status) {
|
|
6
|
-
case 'running':
|
|
7
|
-
return { glyph: '●', color: 'green' };
|
|
8
|
-
case 'failing':
|
|
9
|
-
return { glyph: '✗', color: 'red' };
|
|
10
|
-
case 'idle':
|
|
11
|
-
default:
|
|
12
|
-
return { glyph: '○', color: 'gray' };
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
function lintGlyph(lint) {
|
|
16
|
-
switch (lint) {
|
|
17
|
-
case 'pass':
|
|
18
|
-
return { glyph: '✓', color: 'green' };
|
|
19
|
-
case 'fail':
|
|
20
|
-
return { glyph: '!', color: 'red' };
|
|
21
|
-
case 'unknown':
|
|
22
|
-
default:
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function RepoRow({ entry, lint, isSelected, isRunningAction }) {
|
|
27
|
-
const { glyph, color } = isRunningAction
|
|
28
|
-
? { glyph: '…', color: 'yellow' }
|
|
29
|
-
: statusGlyph(entry.status);
|
|
30
|
-
const lg = lintGlyph(lint);
|
|
31
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: isSelected ? '>' : ' ' }), _jsxs(Text, { color: color, children: [glyph, " "] }), _jsx(Text, { bold: isSelected, children: entry.name }), entry.kind === 'docker' && _jsx(Text, { dimColor: true, children: " [docker]" }), lg && (_jsxs(Text, { dimColor: true, color: lg.color, children: [' ', lg.glyph] })), isRunningAction && _jsx(Text, { color: "yellow", children: ' (running…)' })] }));
|
|
32
|
-
}
|
|
33
|
-
export function Repos() {
|
|
34
|
-
const repos = useCockpitStore((s) => s.repos);
|
|
35
|
-
const repoLintStatus = useCockpitStore((s) => s.repoLintStatus);
|
|
36
|
-
const repoOrder = useCockpitStore((s) => s.repoOrder);
|
|
37
|
-
const focus = useCockpitStore((s) => s.focus);
|
|
38
|
-
const selectedRepoIndex = useCockpitStore((s) => s.selectedRepoIndex);
|
|
39
|
-
const activeRepoAction = useCockpitStore((s) => s.activeRepoAction);
|
|
40
|
-
const isFocused = focus === 'repos';
|
|
41
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "single", borderColor: isFocused ? 'cyan' : undefined, children: [_jsx(Text, { bold: isFocused, children: " Repos " }), activeRepoAction && (_jsx(Box, { marginBottom: 1, paddingX: 1, children: _jsxs(Text, { color: "yellow", children: ['… ', activeRepoAction.label, ' — switch to Output (→) to watch'] }) })), repoOrder.map((key, idx) => {
|
|
42
|
-
const entry = repos[key];
|
|
43
|
-
if (!entry)
|
|
44
|
-
return null;
|
|
45
|
-
const lint = repoLintStatus[key] ?? 'unknown';
|
|
46
|
-
return (_jsx(RepoRow, { entry: entry, lint: lint, isSelected: isFocused && idx === selectedRepoIndex, isRunningAction: activeRepoAction?.repoKey === key }, key));
|
|
47
|
-
})] }));
|
|
48
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { cockpitStore } from '../state/store.js';
|
|
5
|
-
import { useCockpitStore } from '../hooks/useCockpitStore.js';
|
|
6
|
-
export function SearchModal() {
|
|
7
|
-
const outputFilter = useCockpitStore((s) => s.outputFilter);
|
|
8
|
-
const [query, setQuery] = useState(outputFilter.search ?? '');
|
|
9
|
-
useInput((input, key) => {
|
|
10
|
-
if (key.escape) {
|
|
11
|
-
cockpitStore.getState().setActiveModal(null);
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
if (key.return) {
|
|
15
|
-
cockpitStore.getState().setFilter({
|
|
16
|
-
...outputFilter,
|
|
17
|
-
search: query.length > 0 ? query : undefined,
|
|
18
|
-
});
|
|
19
|
-
cockpitStore.getState().setActiveModal(null);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
if (key.backspace || key.delete) {
|
|
23
|
-
setQuery((prev) => prev.slice(0, -1));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (input && !key.ctrl && !key.meta) {
|
|
27
|
-
setQuery((prev) => prev + input);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
return (_jsx(Box, { flexGrow: 1, alignItems: "center", justifyContent: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, children: " Search Output " }), _jsx(Text, { children: " " }), _jsxs(Box, { children: [_jsx(Text, { children: "Query: " }), _jsxs(Text, { color: "cyan", children: [query, _jsx(Text, { color: "gray", children: "\u2588" })] })] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Enter = apply \u00B7 Esc = cancel \u00B7 Backspace = delete" })] }) }));
|
|
31
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Zustand store — single source of truth for cockpit state.
|
|
3
|
-
*
|
|
4
|
-
* Slices: repos, repoLintStatus, repoOrder, focus, selectedRepoIndex, output,
|
|
5
|
-
* outputFilter, activeModal, health, previousHealth, selectedHealthIndex,
|
|
6
|
-
* notificationsEnabledSession, expandedHealthId, recentErrors, activeRemediation,
|
|
7
|
-
* activeRepoAction, helpConfig.
|
|
8
|
-
*
|
|
9
|
-
* No per-pane local stores. All cockpit state lives here. Domain-specific
|
|
10
|
-
* extensions (e.g. profile-specific repo metadata) flow in through the
|
|
11
|
-
* pluggable repo `kind` and the helpConfig sources mechanism.
|
|
12
|
-
*/
|
|
13
|
-
import { createStore } from 'zustand/vanilla';
|
|
14
|
-
const MAX_RECENT_ERRORS = 20;
|
|
15
|
-
const MAX_OUTPUT_LINES = 5000;
|
|
16
|
-
const FOCUS_ORDER = ['repos', 'output', 'health', 'help'];
|
|
17
|
-
export const cockpitStore = createStore()((set, get) => ({
|
|
18
|
-
repos: {},
|
|
19
|
-
repoLintStatus: {},
|
|
20
|
-
repoOrder: [],
|
|
21
|
-
focus: 'repos',
|
|
22
|
-
selectedRepoIndex: 0,
|
|
23
|
-
output: [],
|
|
24
|
-
outputFilter: {},
|
|
25
|
-
activeModal: null,
|
|
26
|
-
health: [],
|
|
27
|
-
previousHealth: [],
|
|
28
|
-
selectedHealthIndex: 0,
|
|
29
|
-
notificationsEnabledSession: true,
|
|
30
|
-
expandedHealthId: null,
|
|
31
|
-
recentErrors: [],
|
|
32
|
-
selectedRecentErrorIndex: 0,
|
|
33
|
-
activeRemediation: null,
|
|
34
|
-
activeRepoAction: null,
|
|
35
|
-
helpConfig: {},
|
|
36
|
-
setRepoStatus: (key, status) => set((state) => {
|
|
37
|
-
const existing = state.repos[key];
|
|
38
|
-
if (!existing)
|
|
39
|
-
return state;
|
|
40
|
-
return { repos: { ...state.repos, [key]: { ...existing, status } } };
|
|
41
|
-
}),
|
|
42
|
-
setLintStatus: (key, lintStatus) => set((state) => ({
|
|
43
|
-
repoLintStatus: { ...state.repoLintStatus, [key]: lintStatus },
|
|
44
|
-
})),
|
|
45
|
-
cycleFocus: (direction) => set((state) => {
|
|
46
|
-
const idx = FOCUS_ORDER.indexOf(state.focus);
|
|
47
|
-
const len = FOCUS_ORDER.length;
|
|
48
|
-
const nextIdx = direction === 'forward' ? (idx + 1) % len : (idx - 1 + len) % len;
|
|
49
|
-
const next = FOCUS_ORDER[nextIdx];
|
|
50
|
-
if (!next)
|
|
51
|
-
return state;
|
|
52
|
-
return { focus: next };
|
|
53
|
-
}),
|
|
54
|
-
initRepos: (repos, repoOrder) => {
|
|
55
|
-
const repoLintStatus = {};
|
|
56
|
-
for (const key of repoOrder) {
|
|
57
|
-
repoLintStatus[key] = 'unknown';
|
|
58
|
-
}
|
|
59
|
-
return set({ repos, repoOrder, repoLintStatus, selectedRepoIndex: 0 });
|
|
60
|
-
},
|
|
61
|
-
moveSelection: (direction) => set((state) => {
|
|
62
|
-
const len = state.repoOrder.length;
|
|
63
|
-
if (len === 0)
|
|
64
|
-
return state;
|
|
65
|
-
const current = state.selectedRepoIndex;
|
|
66
|
-
const next = direction === 'up' ? Math.max(0, current - 1) : Math.min(len - 1, current + 1);
|
|
67
|
-
return { selectedRepoIndex: next };
|
|
68
|
-
}),
|
|
69
|
-
appendOutput: (line) => set((state) => {
|
|
70
|
-
const next = [...state.output, line];
|
|
71
|
-
const capped = next.length > MAX_OUTPUT_LINES ? next.slice(next.length - MAX_OUTPUT_LINES) : next;
|
|
72
|
-
return { output: capped };
|
|
73
|
-
}),
|
|
74
|
-
clearOutput: () => set({ output: [] }),
|
|
75
|
-
setFilter: (filter) => set({ outputFilter: filter }),
|
|
76
|
-
setActiveModal: (modal) => set({ activeModal: modal }),
|
|
77
|
-
setHealth: (next, onTransition) => {
|
|
78
|
-
const prev = get().health;
|
|
79
|
-
if (onTransition)
|
|
80
|
-
onTransition(prev, next);
|
|
81
|
-
set({ health: next, previousHealth: prev });
|
|
82
|
-
},
|
|
83
|
-
moveHealthSelection: (direction) => set((state) => {
|
|
84
|
-
const len = state.health.length;
|
|
85
|
-
if (len === 0)
|
|
86
|
-
return state;
|
|
87
|
-
const current = state.selectedHealthIndex;
|
|
88
|
-
const next = direction === 'up' ? Math.max(0, current - 1) : Math.min(len - 1, current + 1);
|
|
89
|
-
return { selectedHealthIndex: next };
|
|
90
|
-
}),
|
|
91
|
-
toggleSessionNotifications: () => set((state) => ({ notificationsEnabledSession: !state.notificationsEnabledSession })),
|
|
92
|
-
toggleHealthExpand: (id) => set((state) => ({
|
|
93
|
-
expandedHealthId: state.expandedHealthId === id ? null : id,
|
|
94
|
-
})),
|
|
95
|
-
pushRecentError: (err) => set((state) => {
|
|
96
|
-
const next = [err, ...state.recentErrors].slice(0, MAX_RECENT_ERRORS);
|
|
97
|
-
const selectedRecentErrorIndex = Math.min(state.selectedRecentErrorIndex, Math.max(0, next.length - 1));
|
|
98
|
-
return { recentErrors: next, selectedRecentErrorIndex };
|
|
99
|
-
}),
|
|
100
|
-
moveRecentErrorSelection: (direction) => set((state) => {
|
|
101
|
-
const len = state.recentErrors.length;
|
|
102
|
-
if (len === 0)
|
|
103
|
-
return state;
|
|
104
|
-
const current = state.selectedRecentErrorIndex;
|
|
105
|
-
const next = direction === 'up' ? Math.max(0, current - 1) : Math.min(len - 1, current + 1);
|
|
106
|
-
return { selectedRecentErrorIndex: next };
|
|
107
|
-
}),
|
|
108
|
-
setActiveRemediation: (remediation) => set({ activeRemediation: remediation }),
|
|
109
|
-
setActiveRepoAction: (action) => set({ activeRepoAction: action }),
|
|
110
|
-
setHelpConfig: (config) => set({ helpConfig: config }),
|
|
111
|
-
}));
|