Skip to content

Commit 982747d

Browse files
committed
feat: improve onboarding agent selection and local typecheck reliability
1 parent 9a25785 commit 982747d

File tree

4 files changed

+116
-41
lines changed

4 files changed

+116
-41
lines changed

packages/core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ async function main(): Promise<void> {
241241
} catch {
242242
defaultCwd = null;
243243
}
244-
log.info("Config loaded", { defaultCwd, mode: "local" });
244+
log.debug("Config loaded", { defaultCwd, mode: "local" });
245245

246246
loadOdeConfig();
247247
await runOnboardingIfNeeded();

packages/core/onboarding.ts

Lines changed: 88 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createInterface, type Interface } from "node:readline/promises";
2+
import { emitKeypressEvents } from "node:readline";
23
import process from "node:process";
34
import {
45
getWebHost,
@@ -55,32 +56,94 @@ async function askRequired(rl: Interface, prompt: string): Promise<string> {
5556
}
5657
}
5758

58-
function parseSelection(input: string, max: number): number[] | null {
59-
const trimmed = input.trim();
60-
if (!trimmed) return null;
61-
62-
const values = trimmed.split(",").map((part) => part.trim()).filter(Boolean);
63-
if (values.length === 0) return null;
64-
65-
const numbers = new Set<number>();
66-
for (const value of values) {
67-
const parsed = Number.parseInt(value, 10);
68-
if (!Number.isFinite(parsed) || parsed < 1 || parsed > max) {
69-
return [];
70-
}
71-
numbers.add(parsed);
72-
}
73-
74-
return Array.from(numbers).sort((a, b) => a - b);
75-
}
76-
7759
function detectAgents(): AgentOption[] {
7860
return agentOptions.map((agent) => ({
7961
...agent,
8062
installed: Boolean(Bun.which(agent.command)),
8163
}));
8264
}
8365

66+
async function selectAgentsWithKeyboard(agents: AgentOption[], defaultSelected: number[]): Promise<number[]> {
67+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
68+
return defaultSelected;
69+
}
70+
71+
return new Promise((resolve) => {
72+
const selected = new Set(defaultSelected.map((index) => index - 1));
73+
let cursor = 0;
74+
const lineCount = agents.length + 2;
75+
76+
const render = (initial = false): void => {
77+
if (!initial) {
78+
process.stdout.write(`\x1b[${lineCount}F`);
79+
}
80+
process.stdout.write("\x1b[J");
81+
console.log("Step 2/2: Select coding agents to enable.");
82+
console.log("Use Up/Down to move, Space to toggle, Enter to confirm.");
83+
for (const [index, agent] of agents.entries()) {
84+
const pointer = index === cursor ? ">" : " ";
85+
const checked = selected.has(index) ? "x" : " ";
86+
const status = agent.installed ? "installed" : "not found";
87+
console.log(` ${pointer} [${checked}] ${agent.label} (${agent.command}) - ${status}`);
88+
}
89+
};
90+
91+
const cleanup = (): void => {
92+
process.stdin.off("keypress", onKeypress);
93+
if (process.stdin.isTTY) {
94+
process.stdin.setRawMode(false);
95+
}
96+
process.stdin.pause();
97+
};
98+
99+
const finalize = (): void => {
100+
cleanup();
101+
process.stdout.write("\n");
102+
resolve(Array.from(selected).sort((a, b) => a - b).map((index) => index + 1));
103+
};
104+
105+
const onKeypress = (_input: string, key: { name?: string; ctrl?: boolean }): void => {
106+
if (key.ctrl && key.name === "c") {
107+
cleanup();
108+
process.kill(process.pid, "SIGINT");
109+
return;
110+
}
111+
112+
if (key.name === "up") {
113+
cursor = (cursor - 1 + agents.length) % agents.length;
114+
render();
115+
return;
116+
}
117+
118+
if (key.name === "down") {
119+
cursor = (cursor + 1) % agents.length;
120+
render();
121+
return;
122+
}
123+
124+
if (key.name === "space") {
125+
if (selected.has(cursor)) {
126+
selected.delete(cursor);
127+
} else {
128+
selected.add(cursor);
129+
}
130+
render();
131+
return;
132+
}
133+
134+
if (key.name === "return" || key.name === "enter") {
135+
finalize();
136+
}
137+
};
138+
139+
emitKeypressEvents(process.stdin);
140+
process.stdin.setRawMode(true);
141+
process.stdin.resume();
142+
process.stdin.on("keypress", onKeypress);
143+
render(true);
144+
});
145+
}
146+
84147
async function setupSlackWorkspaces(rl: Interface, config: OdeConfig): Promise<OdeConfig> {
85148
const wantsSetup = await askYesNo(
86149
rl,
@@ -141,28 +204,13 @@ async function setupCodingAgents(rl: Interface, config: OdeConfig): Promise<OdeC
141204
.filter((entry) => entry.installed)
142205
.map((entry) => entry.index + 1);
143206

144-
console.log("Step 2/2: Select coding agents to enable.");
145-
for (const [index, agent] of agents.entries()) {
146-
const selected = defaultSelected.includes(index + 1) ? "x" : " ";
147-
const status = agent.installed ? "installed" : "not found";
148-
console.log(` ${index + 1}. [${selected}] ${agent.label} (${agent.command}) - ${status}`);
149-
}
150-
151-
let selectedIndices: number[] | null = null;
152-
while (selectedIndices === null) {
153-
const input = await ask(
154-
rl,
155-
"Choose agents by number (comma-separated). Press Enter to keep detected defaults: "
156-
);
157-
const parsed = parseSelection(input, agents.length);
158-
if (parsed !== null && parsed.length === 0) {
159-
console.log("Please enter valid numbers from the list, like 1,3.");
160-
continue;
161-
}
162-
selectedIndices = parsed;
207+
rl.pause();
208+
let finalIndices: number[];
209+
try {
210+
finalIndices = await selectAgentsWithKeyboard(agents, defaultSelected);
211+
} finally {
212+
rl.resume();
163213
}
164-
165-
const finalIndices = selectedIndices ?? defaultSelected;
166214
const selectedIds = new Set<AgentId>(
167215
finalIndices.map((index) => agents[index - 1]!.id)
168216
);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const EMBEDDED_ASSETS: Record<string, string>;
2+
export const HAS_EMBEDDED_ASSETS: boolean;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
declare module "svelte/store" {
2+
export type Subscriber<T> = (value: T) => void;
3+
export type Unsubscriber = () => void;
4+
5+
export interface Readable<T> {
6+
subscribe(run: Subscriber<T>): Unsubscriber;
7+
}
8+
9+
export interface Writable<T> extends Readable<T> {
10+
set(value: T): void;
11+
update(updater: (value: T) => T): void;
12+
}
13+
14+
export function writable<T>(value: T): Writable<T>;
15+
export function readable<T>(value: T): Readable<T>;
16+
export function get<T>(store: Readable<T>): T;
17+
}
18+
19+
declare module "@sveltejs/kit/vite" {
20+
export function sveltekit(): unknown;
21+
}
22+
23+
declare module "vite" {
24+
export function defineConfig<T>(config: T): T;
25+
}

0 commit comments

Comments
 (0)