@axplusb/kepler 1.0.10 → 2.0.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/package.json +5 -2
- package/src/context/retriever.mjs +42 -4
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/approval.mjs +154 -95
- package/src/core/backend-url.mjs +2 -2
- package/src/core/headless.mjs +5 -0
- package/src/core/risk-tier.mjs +239 -0
- package/src/core/tool-executor.mjs +49 -3
- package/src/onboarding/preflight.mjs +274 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/terminal/ansi.mjs +44 -22
- package/src/terminal/repl.mjs +395 -108
- package/src/ui/approval.mjs +167 -0
- package/src/ui/banner.mjs +133 -122
- package/src/ui/dock.mjs +88 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/mission-report.mjs +264 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/status-bar.mjs +275 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/tool-card.mjs +314 -0
- package/src/ui/tool-details.mjs +277 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval prompt UI — Mission Control (PRD-055 §8.2).
|
|
3
|
+
*
|
|
4
|
+
* ┃ AWAITING APPROVAL ┃
|
|
5
|
+
* ┃ ┃
|
|
6
|
+
* ┃ ⚙️ shell "rm -rf node_modules" ┃
|
|
7
|
+
* ┃ ┃
|
|
8
|
+
* ┃ Tier: SHELL-DANGEROUS ┃
|
|
9
|
+
* ┃ Why: Recursive delete in project directory ┃
|
|
10
|
+
* ┃ ┃
|
|
11
|
+
* ┃ [Enter] approve [e] edit [r] re-plan [n] reject [?] why
|
|
12
|
+
*
|
|
13
|
+
* The border colour is `brand.accent` (magenta) for explicit-approval
|
|
14
|
+
* tiers; safe-default prompts use `brand.data` so they read as advisory.
|
|
15
|
+
*
|
|
16
|
+
* Pure — caller writes the returned string to stderr.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { paint, width as visibleWidth } from './palette.mjs';
|
|
20
|
+
import { icon } from './icons.mjs';
|
|
21
|
+
import { toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
22
|
+
import { label as tierLabel, requiresExplicitApproval, TIERS } from '../core/risk-tier.mjs';
|
|
23
|
+
|
|
24
|
+
const VBAR = '┃';
|
|
25
|
+
const PAD_X = 3;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default option set per tier. Caller can override via `opts.options`.
|
|
29
|
+
*
|
|
30
|
+
* Each option is `{ key, label, value, hint? }`:
|
|
31
|
+
* key — single-letter shortcut (case-insensitive)
|
|
32
|
+
* label — text shown in the menu
|
|
33
|
+
* value — return value from the menu loop
|
|
34
|
+
* hint — secondary description shown to the right of the label
|
|
35
|
+
*/
|
|
36
|
+
export function defaultOptions(tier) {
|
|
37
|
+
if (requiresExplicitApproval(tier)) {
|
|
38
|
+
return [
|
|
39
|
+
{ key: 'y', label: 'approve', value: 'approve', hint: 'run this once' },
|
|
40
|
+
{ key: 'e', label: 'edit', value: 'edit', hint: 'tweak the args before running' },
|
|
41
|
+
{ key: 'r', label: 're-plan', value: 'replan', hint: 'send back to the agent' },
|
|
42
|
+
{ key: 'n', label: 'reject', value: 'reject', hint: 'do not run' },
|
|
43
|
+
{ key: '?', label: 'why', value: 'why', hint: 'show reasoning' },
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
return [
|
|
47
|
+
{ key: 'y', label: 'approve', value: 'approve', hint: 'run this once' },
|
|
48
|
+
{ key: 't', label: 'always allow', value: 'allow-type', hint: 'auto-approve future calls to this tool' },
|
|
49
|
+
{ key: 'n', label: 'reject', value: 'reject', hint: 'do not run' },
|
|
50
|
+
{ key: '?', label: 'why', value: 'why', hint: 'show reasoning' },
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Render the bordered prompt with vertical, arrow-navigable options.
|
|
56
|
+
*
|
|
57
|
+
* ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
|
|
58
|
+
* ┃ AWAITING APPROVAL ┃
|
|
59
|
+
* ┃ ┃
|
|
60
|
+
* ┃ ⚙️ shell "rm -rf …" ┃
|
|
61
|
+
* ┃ ┃
|
|
62
|
+
* ┃ Tier: SHELL-DANGEROUS ┃
|
|
63
|
+
* ┃ Why: Recursive delete… ┃
|
|
64
|
+
* ┃ ┃
|
|
65
|
+
* ┃ ▸ [y] approve ┃ ← selected (accent)
|
|
66
|
+
* ┃ [e] edit ┃
|
|
67
|
+
* ┃ [r] re-plan ┃
|
|
68
|
+
* ┃ [n] reject ┃
|
|
69
|
+
* ┃ [?] why ┃
|
|
70
|
+
* ┃ ┃
|
|
71
|
+
* ┃ ↑↓ move · Enter pick ┃
|
|
72
|
+
* ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
|
|
73
|
+
*
|
|
74
|
+
* @param {object} opts
|
|
75
|
+
* @param {string} opts.tool
|
|
76
|
+
* @param {object} opts.args
|
|
77
|
+
* @param {string} opts.tier
|
|
78
|
+
* @param {string} [opts.why]
|
|
79
|
+
* @param {number} [opts.width]
|
|
80
|
+
* @param {Array} [opts.options] — override default option set
|
|
81
|
+
* @param {number} [opts.selected] — index of the highlighted option
|
|
82
|
+
*/
|
|
83
|
+
export function renderApprovalPrompt({
|
|
84
|
+
tool, args = {}, tier, why = '', width,
|
|
85
|
+
options, selected = 0,
|
|
86
|
+
} = {}) {
|
|
87
|
+
const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
|
|
88
|
+
const explicit = requiresExplicitApproval(tier);
|
|
89
|
+
const accent = explicit ? paint.brand.accent : paint.brand.data;
|
|
90
|
+
const opts = options || defaultOptions(tier);
|
|
91
|
+
|
|
92
|
+
const summary = toolDisplaySummary(tool, args, {});
|
|
93
|
+
const toolLine = `${icon(tool)} ${paint.text.primary(toolDisplayLabel(tool))} ${paint.text.muted(summary ? `"${truncate(summary, cols - 20)}"` : '')}`;
|
|
94
|
+
|
|
95
|
+
const lines = [
|
|
96
|
+
bar(cols, accent),
|
|
97
|
+
fill(' AWAITING APPROVAL', cols, accent, paint.bold(accent('AWAITING APPROVAL'))),
|
|
98
|
+
fill('', cols, accent),
|
|
99
|
+
fill(' ' + ' '.repeat(PAD_X - 1) + toolLine, cols, accent),
|
|
100
|
+
fill('', cols, accent),
|
|
101
|
+
fill(' ' + ' '.repeat(PAD_X - 1) + paint.text.dim('Tier: ') + accent(tierLabel(tier)), cols, accent),
|
|
102
|
+
];
|
|
103
|
+
if (why) {
|
|
104
|
+
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) + paint.text.dim('Why: ') + paint.text.primary(truncate(why, cols - PAD_X - 12)), cols, accent));
|
|
105
|
+
}
|
|
106
|
+
lines.push(fill('', cols, accent));
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < opts.length; i++) {
|
|
109
|
+
const o = opts[i];
|
|
110
|
+
const isSel = i === selected;
|
|
111
|
+
const cursor = isSel ? accent('▸ ') : paint.text.dim(' ');
|
|
112
|
+
const keyTag = paint.text.dim('[') + (isSel ? accent(o.key) : paint.brand.data(o.key)) + paint.text.dim('] ');
|
|
113
|
+
const label = isSel ? paint.bold(accent(o.label)) : paint.text.primary(o.label);
|
|
114
|
+
const hint = o.hint ? ' ' + paint.text.muted(o.hint) : '';
|
|
115
|
+
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) + cursor + keyTag + label + hint, cols, accent));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
lines.push(fill('', cols, accent));
|
|
119
|
+
lines.push(fill(' ' + ' '.repeat(PAD_X - 1) +
|
|
120
|
+
paint.text.dim('↑↓ ') + paint.brand.data('move') +
|
|
121
|
+
paint.text.dim(' · Enter ') + paint.brand.data('pick') +
|
|
122
|
+
paint.text.dim(' · or press a letter'), cols, accent));
|
|
123
|
+
lines.push(bar(cols, accent));
|
|
124
|
+
|
|
125
|
+
return '\n' + lines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function bar(width, painter) {
|
|
129
|
+
// Top / bottom rule: a magenta vertical-stack line spanning the full width.
|
|
130
|
+
return painter('▔'.repeat(width));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function fill(text, width, painter, override) {
|
|
134
|
+
const visible = override ?? text;
|
|
135
|
+
const pad = Math.max(0, width - 2 - visibleWidth(visible));
|
|
136
|
+
return painter(VBAR) + visible + ' '.repeat(pad) + painter(VBAR);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function truncate(text, n) {
|
|
140
|
+
const s = String(text || '');
|
|
141
|
+
if (s.length <= n) return s;
|
|
142
|
+
return s.slice(0, Math.max(0, n - 1)) + '…';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Inline (safe-default) prompt for shell-medium / network ────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Render the compact one-line prompt used for safe-default tiers.
|
|
149
|
+
* The bordered block is reserved for explicit-approval tiers.
|
|
150
|
+
*
|
|
151
|
+
* ? Run `npm install lodash`? Tier: SHELL-MEDIUM [Enter=yes n=no ?=why]
|
|
152
|
+
*/
|
|
153
|
+
export function renderInlinePrompt({ tool, args = {}, tier, why = '' } = {}) {
|
|
154
|
+
const summary = toolDisplaySummary(tool, args, {});
|
|
155
|
+
const label = toolDisplayLabel(tool);
|
|
156
|
+
const lines = [];
|
|
157
|
+
const head = `${paint.brand.data('?')} ${paint.text.primary(label)} ${paint.text.muted(summary ? `"${truncate(summary, 80)}"` : '')}`;
|
|
158
|
+
lines.push(' ' + head);
|
|
159
|
+
lines.push(' ' + paint.text.dim('Tier ') + paint.brand.data(tierLabel(tier)) +
|
|
160
|
+
(why ? ' ' + paint.text.dim('Why ') + paint.text.muted(truncate(why, 60)) : ''));
|
|
161
|
+
lines.push(' ' + paint.text.dim('[') + paint.brand.data('Enter') + paint.text.dim('=yes ') +
|
|
162
|
+
paint.brand.data('n') + paint.text.dim('=no ') +
|
|
163
|
+
paint.brand.data('?') + paint.text.dim('=why]'));
|
|
164
|
+
return '\n' + lines.join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { TIERS };
|
package/src/ui/banner.mjs
CHANGED
|
@@ -1,162 +1,173 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Banner & Branding — Kepler CLI startup display.
|
|
3
|
+
*
|
|
4
|
+
* Refreshed for Mission Control (PRD-055 §4.3). Uses the semantic palette
|
|
5
|
+
* (`paint.brand.*`) so the same banner renders correctly across truecolor,
|
|
6
|
+
* 256-color, ansi16, and monochrome terminals.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
9
|
import { execSync } from 'node:child_process';
|
|
6
10
|
import * as path from 'node:path';
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
12
|
+
import { icons } from './icons.mjs';
|
|
13
|
+
import { paint, strip } from './palette.mjs';
|
|
14
|
+
import { term } from './term.mjs';
|
|
15
|
+
|
|
16
|
+
const out = process.stderr;
|
|
17
|
+
const write = (s) => { try { out.write(s); } catch {} };
|
|
18
|
+
|
|
19
|
+
// ── Brand banner ─────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const KEPLER_LETTERS = ['K', 'E', 'P', 'L', 'E', 'R'];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render `KEPLER` letter-by-letter as a purple→magenta→cyan gradient.
|
|
25
|
+
* Each letter picks the appropriate brand token; the palette handles tier
|
|
26
|
+
* fallbacks transparently.
|
|
27
|
+
*
|
|
28
|
+
* Falls back to a single solid color in monochrome / ascii mode so we
|
|
29
|
+
* still see something distinctive on hostile terminals.
|
|
30
|
+
*/
|
|
31
|
+
function gradientKepler() {
|
|
32
|
+
if (!term().color) return KEPLER_LETTERS.join(' · ');
|
|
33
|
+
|
|
34
|
+
// Three-stop gradient mapped onto six letters. Stop selection:
|
|
35
|
+
// 0,1 → primary 2,3 → accent 4,5 → data
|
|
36
|
+
const painters = [
|
|
37
|
+
paint.brand.primary, paint.brand.primary,
|
|
38
|
+
paint.brand.accent, paint.brand.accent,
|
|
39
|
+
paint.brand.data, paint.brand.data,
|
|
40
|
+
];
|
|
41
|
+
return KEPLER_LETTERS.map((ch, i) => painters[i](ch)).join(paint.text.dim(' · '));
|
|
42
|
+
}
|
|
19
43
|
|
|
20
44
|
/**
|
|
21
45
|
* Print the branded orbital banner.
|
|
22
46
|
*/
|
|
23
47
|
export function printBanner() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
const dim = paint.text.dim;
|
|
49
|
+
const brandMark = paint.brand.primary(icons.brand);
|
|
50
|
+
const orbit = paint.brand.accent(icons.orbit);
|
|
51
|
+
|
|
52
|
+
write('\n');
|
|
53
|
+
write(` ${brandMark}\n`);
|
|
54
|
+
write(` ${dim('╭───────────────────────────╮')}\n`);
|
|
55
|
+
write(` ${dim('│')} ${gradientKepler()} ${dim('│')}\n`);
|
|
56
|
+
write(` ${dim('╰────── ')}${orbit}${dim(' ─────────────────╯')}\n`);
|
|
57
|
+
write(` ${dim('╱ ╲')}\n`);
|
|
58
|
+
write(` ${dim('the agentic os')}\n`);
|
|
59
|
+
write('\n');
|
|
32
60
|
}
|
|
33
61
|
|
|
62
|
+
// ── Project info bar ─────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const BAR_WIDTH = 60;
|
|
65
|
+
|
|
34
66
|
/**
|
|
35
67
|
* Print project info bar with version, project name, and git info.
|
|
36
|
-
* @param {string} version
|
|
37
68
|
*/
|
|
38
69
|
export function printProjectInfo(version) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
process.stderr.write(`${BLUE}│${RESET} ${info}${' '.repeat(Math.max(0, barWidth - stripAnsi(info).length - 1))}${BLUE}│${RESET}\n`);
|
|
56
|
-
process.stderr.write(`${bottomBorder}\n`);
|
|
70
|
+
const cwd = process.cwd();
|
|
71
|
+
const projectName = path.basename(cwd);
|
|
72
|
+
const gitInfo = getGitInfo(cwd);
|
|
73
|
+
|
|
74
|
+
const sep = paint.text.dim(' │ ');
|
|
75
|
+
let info = ` ${paint.text.dim('v' + version)}${sep}${paint.bold(projectName)}`;
|
|
76
|
+
if (gitInfo) {
|
|
77
|
+
info += `${sep}${paint.state.warn(gitInfo)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const padCount = Math.max(0, BAR_WIDTH - strip(info).length - 1);
|
|
81
|
+
const border = paint.brand.primary;
|
|
82
|
+
|
|
83
|
+
write(`${border('┌' + '─'.repeat(BAR_WIDTH) + '┐')}\n`);
|
|
84
|
+
write(`${border('│')} ${info}${' '.repeat(padCount)}${border('│')}\n`);
|
|
85
|
+
write(`${border('└' + '─'.repeat(BAR_WIDTH) + '┘')}\n`);
|
|
57
86
|
}
|
|
58
87
|
|
|
59
|
-
|
|
60
|
-
* Print keyboard hints and usage tips.
|
|
61
|
-
*/
|
|
62
|
-
export function printHints() {
|
|
63
|
-
const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
|
|
88
|
+
// ── Hints ────────────────────────────────────────────────────────────────
|
|
64
89
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
90
|
+
export function printHints() {
|
|
91
|
+
const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
|
|
92
|
+
const dim = paint.text.dim;
|
|
93
|
+
const accent = paint.brand.data;
|
|
94
|
+
|
|
95
|
+
write(`${paint.state.success('Type your instructions')}, or ${accent('/help')} for commands\n`);
|
|
96
|
+
write(`${dim('Ctrl+C')}${dim('=exit ')}${dim('/clear')}${dim('=reset ')}${dim('/config')}${dim('=settings ')}${dim('/login')}${dim('=auth')}\n`);
|
|
97
|
+
write(`${dim('env:' + env + ' models:configured via browser (/config)')}\n`);
|
|
98
|
+
write('\n');
|
|
69
99
|
}
|
|
70
100
|
|
|
71
|
-
|
|
72
|
-
* Print auth status indicators.
|
|
73
|
-
* @param {object} creds - { token, openRouterKey, anthropicKey, backendUrl, mode }
|
|
74
|
-
*/
|
|
75
|
-
export function printAuthStatus(creds) {
|
|
76
|
-
const check = `${GREEN}✓${RESET}`;
|
|
77
|
-
const cross = `${RED}✗${RESET}`;
|
|
101
|
+
// ── Auth + config ────────────────────────────────────────────────────────
|
|
78
102
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
export function printAuthStatus(creds) {
|
|
104
|
+
const check = paint.state.success(icons.pass);
|
|
105
|
+
const cross = paint.state.danger(icons.fail);
|
|
106
|
+
const dim = paint.text.dim;
|
|
107
|
+
|
|
108
|
+
const tokenOk = !!creds.token;
|
|
109
|
+
const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
|
|
110
|
+
|
|
111
|
+
write(` Auth: ${tokenOk
|
|
112
|
+
? `${check} logged in ${dim('(/whoami for details)')}`
|
|
113
|
+
: `${cross} not logged in ${dim('(/login)')}`}\n`);
|
|
114
|
+
write(` Env: ${dim(env)}\n`);
|
|
115
|
+
write(` Mode: ${dim(creds.mode || 'auto')}\n`);
|
|
116
|
+
write('\n');
|
|
86
117
|
}
|
|
87
118
|
|
|
88
|
-
/**
|
|
89
|
-
* Print config in a styled format.
|
|
90
|
-
* @param {object} creds
|
|
91
|
-
*/
|
|
92
119
|
export function printStyledConfig(creds) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
const check = paint.state.success(icons.pass);
|
|
121
|
+
const cross = paint.state.danger(icons.fail);
|
|
122
|
+
const dim = paint.text.dim;
|
|
123
|
+
|
|
124
|
+
const mask = (val) => {
|
|
125
|
+
if (!val) return `${cross} not set`;
|
|
126
|
+
if (val.length <= 8) return `${check} ****`;
|
|
127
|
+
return `${check} ${val.slice(0, 6)}...${val.slice(-4)}`;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
|
|
131
|
+
|
|
132
|
+
write(`\n${paint.bold('Kepler Configuration')} ${dim('(~/.kepler/config.json)')}\n`);
|
|
133
|
+
write(`${dim('─'.repeat(50))}\n`);
|
|
134
|
+
write(` Token: ${mask(creds.token)}\n`);
|
|
135
|
+
write(` OpenRouter: ${mask(creds.openRouterKey)}\n`);
|
|
136
|
+
write(` Anthropic: ${mask(creds.anthropicKey)}\n`);
|
|
137
|
+
write(` Environment: ${dim(env)}\n`);
|
|
138
|
+
write(` Backend URL: ${dim(creds.backendUrl)}\n`);
|
|
139
|
+
write(` Mode: ${dim(creds.mode || 'auto')}\n`);
|
|
140
|
+
write('\n');
|
|
111
141
|
}
|
|
112
142
|
|
|
113
|
-
/**
|
|
114
|
-
* Print a goodbye message.
|
|
115
|
-
*/
|
|
116
143
|
export function printGoodbye() {
|
|
117
|
-
|
|
144
|
+
write(`\n${paint.bold(paint.brand.primary('Goodbye!'))}\n\n`);
|
|
118
145
|
}
|
|
119
146
|
|
|
120
|
-
|
|
121
|
-
* Get git branch and change count for current directory.
|
|
122
|
-
* @param {string} cwd
|
|
123
|
-
* @returns {string|null}
|
|
124
|
-
*/
|
|
125
|
-
function getGitInfo(cwd) {
|
|
126
|
-
try {
|
|
127
|
-
const branch = execSync('git branch --show-current', {
|
|
128
|
-
cwd, encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
129
|
-
}).trim();
|
|
130
|
-
|
|
131
|
-
if (!branch) return null;
|
|
132
|
-
|
|
133
|
-
const status = execSync('git status --porcelain', {
|
|
134
|
-
cwd, encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
135
|
-
}).trim();
|
|
147
|
+
// ── Git probe ────────────────────────────────────────────────────────────
|
|
136
148
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
function getGitInfo(cwd) {
|
|
150
|
+
try {
|
|
151
|
+
const branch = execSync('git branch --show-current', {
|
|
152
|
+
cwd, encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
153
|
+
}).trim();
|
|
154
|
+
if (!branch) return null;
|
|
155
|
+
|
|
156
|
+
const status = execSync('git status --porcelain', {
|
|
157
|
+
cwd, encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
158
|
+
}).trim();
|
|
159
|
+
const changes = status ? status.split('\n').filter(Boolean).length : 0;
|
|
160
|
+
|
|
161
|
+
return changes > 0 ? `⎇ ${branch} (${changes} changed)` : `⎇ ${branch}`;
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
146
165
|
}
|
|
147
166
|
|
|
148
|
-
|
|
149
|
-
* Strip ANSI escape codes for length calculation.
|
|
150
|
-
*/
|
|
151
|
-
function stripAnsi(str) {
|
|
152
|
-
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
153
|
-
}
|
|
167
|
+
// ── OAuth success page (unchanged from prior version) ────────────────────
|
|
154
168
|
|
|
155
|
-
/**
|
|
156
|
-
* Branded HTML for OAuth success page.
|
|
157
|
-
*/
|
|
158
169
|
export function getLoginSuccessHTML() {
|
|
159
|
-
|
|
170
|
+
return `<!DOCTYPE html>
|
|
160
171
|
<html>
|
|
161
172
|
<head>
|
|
162
173
|
<meta charset="utf-8">
|
package/src/ui/dock.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard dock — Mission Control (PRD-055 §5.4).
|
|
3
|
+
*
|
|
4
|
+
* Maps each orbit to the dock hint shown on the second line of the status
|
|
5
|
+
* bar. Pure data so tests, the status bar, and `/help` stay in sync.
|
|
6
|
+
*
|
|
7
|
+
* import { dockForOrbit } from './dock.mjs';
|
|
8
|
+
* const hints = dockForOrbit(orbit); // → [{ key, label }, ...]
|
|
9
|
+
* const text = renderDock(hints, paint); // → ANSI-styled single line
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { ORBITS } from '../state/orbit.mjs';
|
|
13
|
+
import { paint, width } from './palette.mjs';
|
|
14
|
+
import { term } from './term.mjs';
|
|
15
|
+
|
|
16
|
+
const DEFAULT = [
|
|
17
|
+
{ key: 'Enter', label: 'send' },
|
|
18
|
+
{ key: '/', label: 'commands' },
|
|
19
|
+
{ key: '?', label: 'help' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const HINTS = {
|
|
23
|
+
[ORBITS.IDLE]: DEFAULT,
|
|
24
|
+
[ORBITS.DISCOVERY]: DEFAULT,
|
|
25
|
+
|
|
26
|
+
[ORBITS.PLANNING]: [
|
|
27
|
+
{ key: 'p', label: 'pause' },
|
|
28
|
+
{ key: 'Esc', label: 'interrupt' },
|
|
29
|
+
{ key: '?', label: 'why' },
|
|
30
|
+
],
|
|
31
|
+
|
|
32
|
+
[ORBITS.EXECUTION]: [
|
|
33
|
+
{ key: 'd', label: 'last diff' },
|
|
34
|
+
{ key: 'p', label: 'pause' },
|
|
35
|
+
{ key: 'Esc', label: 'interrupt' },
|
|
36
|
+
],
|
|
37
|
+
|
|
38
|
+
[ORBITS.ALIGNMENT]: [
|
|
39
|
+
{ key: 'd', label: 'last result' },
|
|
40
|
+
{ key: '?', label: 'explain' },
|
|
41
|
+
],
|
|
42
|
+
|
|
43
|
+
[ORBITS.AWAITING]: [
|
|
44
|
+
{ key: 'Enter', label: 'approve' },
|
|
45
|
+
{ key: 'e', label: 'edit' },
|
|
46
|
+
{ key: 'r', label: 're-plan' },
|
|
47
|
+
{ key: 'n', label: 'reject' },
|
|
48
|
+
{ key: '?', label: 'why' },
|
|
49
|
+
],
|
|
50
|
+
|
|
51
|
+
[ORBITS.PAUSED]: [
|
|
52
|
+
{ key: 'r', label: 'resume' },
|
|
53
|
+
{ key: 'Esc', label: 'cancel' },
|
|
54
|
+
{ key: 'q', label: 'quit' },
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Return the array of hints for the given orbit.
|
|
60
|
+
* Falls back to the default dock for unknown orbits so the bar is never empty.
|
|
61
|
+
*/
|
|
62
|
+
export function dockForOrbit(orbit) {
|
|
63
|
+
return HINTS[orbit] || DEFAULT;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Render a hint list to an ANSI-styled line that fits within `maxWidth`
|
|
68
|
+
* visible characters. Drops trailing entries when they don't fit, never
|
|
69
|
+
* mid-hint truncates.
|
|
70
|
+
*/
|
|
71
|
+
export function renderDock(hints, maxWidth = term().columns - 2) {
|
|
72
|
+
if (!Array.isArray(hints) || hints.length === 0) return '';
|
|
73
|
+
const parts = [];
|
|
74
|
+
let used = 0;
|
|
75
|
+
for (const h of hints) {
|
|
76
|
+
const piece = `[${h.key}] ${h.label}`;
|
|
77
|
+
const sep = parts.length ? ' ' : '';
|
|
78
|
+
const cost = width(sep) + width(piece);
|
|
79
|
+
if (used + cost > maxWidth) break;
|
|
80
|
+
parts.push(sep + paintHint(h));
|
|
81
|
+
used += cost;
|
|
82
|
+
}
|
|
83
|
+
return parts.join('');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function paintHint(h) {
|
|
87
|
+
return paint.text.dim('[') + paint.brand.data(h.key) + paint.text.dim('] ' + h.label);
|
|
88
|
+
}
|