-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat(onboard): Onboard presets #1463
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3164,6 +3164,102 @@ function arePolicyPresetsApplied(sandboxName, selectedPresets = []) { | |
| return selectedPresets.every((preset) => applied.has(preset)); | ||
| } | ||
|
|
||
| /** | ||
| * Raw-mode TUI preset selector. | ||
| * Keys: ↑/↓ or k/j to move, Space to toggle, a to select/unselect all, Enter to confirm. | ||
| * Falls back to a simple line-based prompt when stdin is not a TTY. | ||
| */ | ||
| async function presetsCheckboxSelector(allPresets, initialSelected) { | ||
| const selected = new Set(initialSelected); | ||
| const n = allPresets.length; | ||
|
|
||
| // ── Fallback: non-TTY (piped input) ────────────────────────────── | ||
| if (!process.stdin.isTTY) { | ||
| console.log(""); | ||
| console.log(" Available policy presets:"); | ||
| allPresets.forEach((p) => { | ||
| const marker = selected.has(p.name) ? "[\x1b[32m✓\x1b[0m]" : "[ ]"; | ||
| console.log(` ${marker} ${p.name.padEnd(14)} — ${p.description}`); | ||
| }); | ||
|
Comment on lines
+3177
to
+3183
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only use the redraw UI when both terminal streams support it. This path writes cursor-control and color escapes directly to Suggested fix- if (!process.stdin.isTTY) {
+ const canUseTui =
+ process.stdin.isTTY &&
+ process.stdout.isTTY &&
+ typeof process.stdin.setRawMode === "function";
+ if (!canUseTui) {
console.log("");
console.log(" Available policy presets:");
allPresets.forEach((p) => {
- const marker = selected.has(p.name) ? "[\x1b[32m✓\x1b[0m]" : "[ ]";
+ const marker = selected.has(p.name)
+ ? USE_COLOR
+ ? "[\x1b[32m✓\x1b[0m]"
+ : "[✓]"
+ : "[ ]";
console.log(` ${marker} ${p.name.padEnd(14)} — ${p.description}`);
});
@@
const renderLines = () => {
const lines = [" Available policy presets:"];
allPresets.forEach((p, i) => {
- const check = selected.has(p.name) ? "[\x1b[32m✓\x1b[0m]" : "[ ]";
+ const check = selected.has(p.name)
+ ? USE_COLOR
+ ? "[\x1b[32m✓\x1b[0m]"
+ : "[✓]"
+ : "[ ]";
const arrow = i === cursor ? ">" : " ";
lines.push(` ${arrow} ${check} ${p.name.padEnd(14)} — ${p.description}`);
});Also applies to: 3193-3225 🤖 Prompt for AI Agents |
||
| console.log(""); | ||
| const raw = await prompt(" Select presets (comma-separated names, Enter to skip): "); | ||
| if (!raw.trim()) return []; | ||
| return raw | ||
| .split(",") | ||
| .map((s) => s.trim()) | ||
| .filter((name) => allPresets.some((p) => p.name === name)); | ||
| } | ||
|
|
||
| // ── Raw-mode TUI ───────────────────────────────────────────────── | ||
| let cursor = 0; | ||
|
|
||
| const HINT = " ↑/↓ j/k move Space toggle a all/none Enter confirm"; | ||
|
|
||
| const renderLines = () => { | ||
| const lines = [" Available policy presets:"]; | ||
| allPresets.forEach((p, i) => { | ||
| const check = selected.has(p.name) ? "[\x1b[32m✓\x1b[0m]" : "[ ]"; | ||
| const arrow = i === cursor ? ">" : " "; | ||
| lines.push(` ${arrow} ${check} ${p.name.padEnd(14)} — ${p.description}`); | ||
| }); | ||
| lines.push(""); | ||
| lines.push(HINT); | ||
| return lines; | ||
| }; | ||
|
|
||
| // Initial paint | ||
| process.stdout.write("\n"); | ||
| const initial = renderLines(); | ||
| for (const line of initial) process.stdout.write(`${line}\n`); | ||
| let lineCount = initial.length; | ||
|
|
||
| const redraw = () => { | ||
| process.stdout.write(`\x1b[${lineCount}A`); | ||
| const lines = renderLines(); | ||
| for (const line of lines) process.stdout.write(`\r\x1b[2K${line}\n`); | ||
| lineCount = lines.length; | ||
| }; | ||
|
|
||
| process.stdin.setRawMode(true); | ||
| process.stdin.resume(); | ||
| process.stdin.setEncoding("utf8"); | ||
|
|
||
| return new Promise((resolve) => { | ||
| const cleanup = () => { | ||
| process.stdin.setRawMode(false); | ||
| process.stdin.pause(); | ||
| process.stdin.removeAllListeners("data"); | ||
| }; | ||
|
|
||
| process.stdin.on("data", (key) => { | ||
| if (key === "\r" || key === "\n") { | ||
| cleanup(); | ||
| process.stdout.write("\n"); | ||
| resolve([...selected]); | ||
| } else if (key === "\x03") { | ||
| // Ctrl+C | ||
| cleanup(); | ||
| process.exit(1); | ||
| } else if (key === "\x1b[A" || key === "k") { | ||
| cursor = (cursor - 1 + n) % n; | ||
| redraw(); | ||
| } else if (key === "\x1b[B" || key === "j") { | ||
| cursor = (cursor + 1) % n; | ||
| redraw(); | ||
| } else if (key === " ") { | ||
| const name = allPresets[cursor].name; | ||
| if (selected.has(name)) selected.delete(name); | ||
| else selected.add(name); | ||
| redraw(); | ||
| } else if (key === "a") { | ||
| if (selected.size === n) selected.clear(); | ||
| else for (const p of allPresets) selected.add(p.name); | ||
| redraw(); | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| // eslint-disable-next-line complexity | ||
| async function setupPoliciesWithSelection(sandboxName, options = {}) { | ||
| const selectedPresets = Array.isArray(options.selectedPresets) ? options.selectedPresets : null; | ||
|
|
@@ -3212,9 +3308,7 @@ async function setupPoliciesWithSelection(sandboxName, options = {}) { | |
| } | ||
| } else if (policyMode === "suggested" || policyMode === "default" || policyMode === "auto") { | ||
| const envPresets = parsePolicyPresetEnv(process.env.NEMOCLAW_POLICY_PRESETS); | ||
| if (envPresets.length > 0) { | ||
| chosen = envPresets; | ||
| } | ||
| if (envPresets.length > 0) chosen = envPresets; | ||
| } else { | ||
| console.error(` Unsupported NEMOCLAW_POLICY_MODE: ${policyMode}`); | ||
| console.error(" Valid values: suggested, custom, skip"); | ||
|
|
@@ -3256,46 +3350,14 @@ async function setupPoliciesWithSelection(sandboxName, options = {}) { | |
| return chosen; | ||
| } | ||
|
|
||
| console.log(""); | ||
| console.log(" Available policy presets:"); | ||
| allPresets.forEach((p) => { | ||
| const marker = applied.includes(p.name) ? "●" : "○"; | ||
| const suggested = suggestions.includes(p.name) ? " (suggested)" : ""; | ||
| console.log(` ${marker} ${p.name} — ${p.description}${suggested}`); | ||
| }); | ||
| console.log(""); | ||
|
|
||
| const answer = await prompt( | ||
| ` Apply suggested presets (${suggestions.join(", ")})? [Y/n/list]: `, | ||
| ); | ||
|
|
||
| if (answer.toLowerCase() === "n") { | ||
| console.log(" Skipping policy presets."); | ||
| return []; | ||
| } | ||
|
|
||
| let interactiveChoice = suggestions; | ||
| if (answer.toLowerCase() === "list") { | ||
| const custom = await prompt(" Enter preset names (comma-separated): "); | ||
| interactiveChoice = parsePolicyPresetEnv(custom); | ||
| } | ||
|
|
||
| const knownPresets = new Set(allPresets.map((p) => p.name)); | ||
| let invalidPresets = interactiveChoice.filter((name) => !knownPresets.has(name)); | ||
| while (invalidPresets.length > 0) { | ||
| console.error(` Unknown policy preset(s): ${invalidPresets.join(", ")}`); | ||
| console.log(" Available presets:"); | ||
| for (const p of allPresets) { | ||
| console.log(` - ${p.name}`); | ||
| } | ||
| const retry = await prompt(" Enter preset names (comma-separated), or leave empty to skip: "); | ||
| if (!retry.trim()) { | ||
| console.log(" Skipping policy presets."); | ||
| return []; | ||
| } | ||
| interactiveChoice = parsePolicyPresetEnv(retry); | ||
| invalidPresets = interactiveChoice.filter((name) => !knownPresets.has(name)); | ||
| } | ||
| // Interactive: raw-mode TUI checkbox selector | ||
| // Seed selection with already-applied presets and credential-based suggestions | ||
| const knownNames = new Set(allPresets.map((p) => p.name)); | ||
| const initialSelected = [ | ||
| ...applied.filter((name) => knownNames.has(name)), | ||
| ...suggestions.filter((name) => knownNames.has(name) && !applied.includes(name)), | ||
| ]; | ||
| const interactiveChoice = await presetsCheckboxSelector(allPresets, initialSelected); | ||
|
|
||
| if (onSelection) onSelection(interactiveChoice); | ||
| if (!waitForSandboxReady(sandboxName)) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard the zero-preset case before entering raw mode.
policies.listPresets()can legitimately return an empty array, and withn === 0the navigation handlers do modulo-by-zero while Space dereferencesallPresets[cursor].name. That can throw after raw mode is enabled and leave the terminal in a bad state.Suggested fix
async function presetsCheckboxSelector(allPresets, initialSelected) { const selected = new Set(initialSelected); const n = allPresets.length; + if (n === 0) { + console.log(" No policy presets are available."); + return []; + } // ── Fallback: non-TTY (piped input) ────────────────────────────── if (!process.stdin.isTTY) {Also applies to: 3243-3256
🤖 Prompt for AI Agents