Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions apps/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ export interface SessionContext {
credsStore?: CredentialsStore;
/** User settings.json path (REPL-injected, honors --home) — backs /config set. */
userSettingsPath?: string;
/** Home dir override (REPL-injected from --home) — backs default-path lookups
* like /voice's `~/.deepcode/models/...` model probe. Defaults to os.homedir(). */
home?: string;
sessionId: string;
sessions: SessionManager;
usage: {
Expand Down Expand Up @@ -1169,6 +1172,57 @@ export const TasksCommand: SlashCommand = {
},
};

export const VoiceCommand: SlashCommand = {
name: '/voice',
description: 'Check local voice-input (whisper.cpp) setup; `/voice setup` shows install steps.',
async run(args, ctx) {
const { detectVoice } = await import('@deepcode/core');
const status = await detectVoice(ctx.settings.voice, { home: ctx.home });
const forceSetup = (args[0] ?? '').toLowerCase() === 'setup';

if (status.ready && !forceSetup) {
return [
'🎙 Voice input is ready — whisper.cpp, fully local (no audio leaves your machine).',
` binary: ${status.binPath}`,
` model: ${status.modelPath}`,
'',
'Dictate from the REPL with the voice key (default Ctrl+V; remap in keybindings.json).',
'Note: live mic capture lands in a follow-up — this step ships setup + detection.',
];
}

const lines: string[] = [
status.ready
? '🎙 Voice input is ready. Setup reference below.'
: '🎙 Voice input is not set up yet. Enable local dictation (whisper.cpp — no cloud):',
'',
'Detected:',
` ${status.binPath ? '✓' : '✗'} whisper binary ${status.binPath ?? '(not found)'}`,
` ${status.modelPath ? '✓' : '✗'} model ${status.modelPath ?? '(not found)'}`,
];
if (status.problems.length) {
lines.push('', 'Issues:');
for (const p of status.problems) lines.push(` • ${p}`);
}
lines.push(
'',
'Setup:',
' 1. Install whisper.cpp',
' macOS: brew install whisper-cpp',
' Linux: build https://github.com/ggerganov/whisper.cpp, put `whisper` on PATH',
' 2. Download a model (base.en ≈ 140 MB is a good default) and save it:',
' mkdir -p ~/.deepcode/models',
' cp ggml-base.en.bin ~/.deepcode/models/whisper-base.en.bin',
' 3. (optional) Point DeepCode at custom paths in ~/.deepcode/settings.json:',
' { "voice": { "binPath": "/opt/homebrew/bin/whisper-cli",',
' "modelPath": "~/.deepcode/models/whisper-base.en.bin" } }',
'',
'Full guide: docs/VOICE_INPUT.md',
);
return lines;
},
};

export const BackgroundCommand: SlashCommand = {
name: '/background',
aliases: ['/bg'],
Expand Down Expand Up @@ -1229,6 +1283,7 @@ export const BUILTIN_COMMANDS: SlashCommand[] = [
BtwCommand,
TasksCommand,
BackgroundCommand,
VoiceCommand,
];

// ──────────────────────────────────────────────────────────────────────────
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
creds,
credsStore,
userSettingsPath: settingsPaths({ cwd, home: opts.home }).userPath,
home: opts.home,
sessionId: session.id,
sessions,
usage: { inputTokens: 0, outputTokens: 0, reasoningTokens: 0, cacheReadTokens: 0 },
Expand Down
85 changes: 85 additions & 0 deletions apps/cli/src/voice-cmd.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Tests for the /voice slash command messaging. Detection logic itself is
// unit-tested in core (voice/detect.test.ts); here we drive the command end to
// end with real temp files so the "ready" path is deterministic, and bogus
// configured paths so the "not set up" path never depends on the host's PATH.

import { afterEach, describe, expect, it } from 'vitest';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { SessionManager } from '@deepcode/core';
import { CommandRegistry, type SessionContext } from './commands.js';

const reg = new CommandRegistry();
const tmps: string[] = [];
async function tmpDir(): Promise<string> {
const d = await mkdtemp(join(tmpdir(), 'dc-voice-'));
tmps.push(d);
return d;
}
afterEach(async () => {
await Promise.all(tmps.splice(0).map((d) => rm(d, { recursive: true, force: true })));
});

function ctx(overrides: Partial<SessionContext> = {}): SessionContext {
return {
cwd: '/tmp/x',
model: 'deepseek-chat',
mode: 'default',
effort: 'medium',
settings: {},
creds: { apiKey: 'sk-test' },
sessionId: 's1',
sessions: new SessionManager({ root: '/tmp/x' }),
usage: { inputTokens: 0, outputTokens: 0, reasoningTokens: 0, cacheReadTokens: 0 },
...overrides,
};
}

const run = (args: string[], c: SessionContext) => reg.match('/voice')!.cmd.run(args, c);

describe('/voice', () => {
it('reports ready when configured binary + model both exist', async () => {
const dir = await tmpDir();
const binPath = join(dir, 'whisper-cli');
const modelPath = join(dir, 'model.bin');
await writeFile(binPath, '#!/bin/sh\n');
await writeFile(modelPath, 'GGML');
const out = (await run([], ctx({ settings: { voice: { binPath, modelPath } } }))).join('\n');
expect(out).toMatch(/ready/i);
expect(out).toContain(binPath);
expect(out).toContain(modelPath);
expect(out).toMatch(/Ctrl\+V/);
});

it('prints setup steps + issues when configured paths are missing', async () => {
const out = (
await run(
[],
ctx({ settings: { voice: { binPath: '/no/such/whisper', modelPath: '/no/such/m.bin' } } }),
)
).join('\n');
expect(out).toMatch(/not set up yet/i);
expect(out).toMatch(/brew install whisper-cpp/);
expect(out).toMatch(/docs\/VOICE_INPUT\.md/);
// The specific configured-but-missing problems surface under "Issues:".
expect(out).toMatch(/Issues:/);
expect(out).toContain('Configured voice.binPath not found: /no/such/whisper');
expect(out).toContain('Configured voice.modelPath not found: /no/such/m.bin');
});

it('`/voice setup` always shows install steps, even when ready', async () => {
const dir = await tmpDir();
const binPath = join(dir, 'whisper-cli');
const modelPath = join(dir, 'model.bin');
await writeFile(binPath, '');
await writeFile(modelPath, '');
const out = (await run(['setup'], ctx({ settings: { voice: { binPath, modelPath } } }))).join(
'\n',
);
expect(out).toMatch(/Setup:/);
expect(out).toMatch(/brew install whisper-cpp/);
// Still acknowledges it's already ready.
expect(out).toMatch(/ready/i);
});
});
Loading
Loading