Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a1ea37e
improvement(SUPER-869): capture brief
justincrich May 22, 2026
a32be2d
improvement(SUPER-869): scope proposal v1
justincrich May 22, 2026
ce9a7fb
improvement(SUPER-869): scope challenged
justincrich May 22, 2026
f0834e2
improvement(SUPER-869): scope binding (moderate)
justincrich May 22, 2026
b0f27a5
feat(presets): add Factory Droid as a terminal agent preset (#SUPER-869)
justincrich May 22, 2026
8cf3340
Merge: add Factory Droid preset (SUPER-869)
justincrich May 22, 2026
c40da1b
perf(desktop): fix file navigator lag and empty-folder-on-expand bug
justincrich May 27, 2026
bf6d115
feat(desktop): add fs:events observability for file navigator debugging
justincrich May 27, 2026
eef6216
Merge remote-tracking branch 'origin/main'
justincrich May 28, 2026
41ab832
fix: remove duplicate droid preset-icon exports from merge
justincrich May 28, 2026
db27321
Merge remote-tracking branch 'fork/improvement/imp-file-navigator-lag…
justincrich May 28, 2026
074cad6
chore(desktop): add build-local-prod.sh for fork production installs
justincrich May 28, 2026
d759ac1
Add voice input settings search metadata
justincrich May 28, 2026
1eb8460
Add voice input shortcut registry entry
justincrich May 28, 2026
d3fb058
WISP-001: implement voice input settings API
justincrich May 28, 2026
c70c799
Merge WISP-003: Register voice input setting search metadata from wor…
justincrich May 28, 2026
aa13b56
Merge WISP-004: Add voice activation shortcut registry entry from wor…
justincrich May 28, 2026
f8f4094
fix(desktop): add voice input local-db migration
justincrich May 28, 2026
cb87b72
Merge WISP-001: Add persisted voice input settings API from worktree
justincrich May 28, 2026
d50d2ff
Add voice input behavior setting
justincrich May 28, 2026
0f1bb7e
Merge WISP-002: Add voice input behavior setting
justincrich May 28, 2026
9970882
Add voice activation enabled guard
justincrich May 28, 2026
3fed5f7
Wire voice shortcut editing
justincrich May 28, 2026
54a9fa4
Surface microphone readiness in behavior settings
justincrich May 28, 2026
14cb794
Fix disabled voice hotkey default handling
justincrich May 28, 2026
9144f9e
Merge WISP-008: Surface microphone readiness in behavior settings
justincrich May 28, 2026
fbbc956
Merge WISP-007: Add enabled-state voice activation guard
justincrich May 28, 2026
c6323e3
test(desktop): exercise voice shortcut settings flows
justincrich May 28, 2026
213e4c9
test(desktop): mount voice shortcut settings flow
justincrich May 28, 2026
393b956
Merge WISP-005: Wire voice shortcut editing
justincrich May 28, 2026
7d7d77c
Link behavior voice shortcut settings
justincrich May 28, 2026
801eddd
fix desktop voice shortcut hash routing
justincrich May 28, 2026
69e0800
Merge WISP-006: Link behavior settings to voice shortcut row
justincrich May 28, 2026
c88ad8e
test(desktop): cover voice preference shortcut flow
justincrich May 28, 2026
6679dc3
Merge WISP-009: Cover voice preference regression flow
justincrich May 28, 2026
b8f4585
feat(desktop): add voice control dictation
justincrich May 28, 2026
2f56a0d
Merge whisperflow voice control
justincrich May 28, 2026
499f1b3
Merge upstream main into fork main
justincrich May 28, 2026
fcfe4a0
chore: remove spec artifacts from PR
justincrich May 28, 2026
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
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@
"electron": "40.8.5",
"electron-builder": "26.8.1",
"electron-vite": "4.0.1",
"happy-dom": "20.9.0",
"material-icon-theme": "5.32.0",
"rimraf": "6.1.3",
"rollup-plugin-inject-process-env": "1.3.1",
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { createSettingsRouter } from "./settings";
import { createSystemRouter } from "./system";
import { createTerminalRouter } from "./terminal";
import { createUiStateRouter } from "./ui-state";
import { createVoiceInputRouter } from "./voice-input";
import { createWindowRouter } from "./window";
import { createWorkspacesRouter } from "./workspaces";

Expand Down Expand Up @@ -61,6 +62,7 @@ export const createAppRouter = (getWindow: () => BrowserWindow | null) => {
hostServiceCoordinator: createHostServiceCoordinatorRouter(),
keyboardLayout: createKeyboardLayoutRouter(),
migration: createMigrationRouter(),
voiceInput: createVoiceInputRouter(),
});
};

Expand Down
59 changes: 59 additions & 0 deletions apps/desktop/src/lib/trpc/routers/permissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { beforeEach, describe, expect, it, mock } from "bun:test";

const askForMediaAccessMock = mock(async () => false);
const getMediaAccessStatusMock = mock(() => "not-determined");
const isTrustedAccessibilityClientMock = mock(() => false);
const openExternalMock = mock(async () => undefined);

mock.module("electron", () => ({
app: {
relaunch: mock(() => {}),
},
shell: {
openExternal: openExternalMock,
},
systemPreferences: {
askForMediaAccess: askForMediaAccessMock,
getMediaAccessStatus: getMediaAccessStatusMock,
isTrustedAccessibilityClient: isTrustedAccessibilityClientMock,
},
}));

const { createPermissionsRouter } = await import("./permissions");

function createCaller() {
return createPermissionsRouter().createCaller({});
}

describe("permissions router", () => {
beforeEach(() => {
askForMediaAccessMock.mockClear();
getMediaAccessStatusMock.mockClear();
isTrustedAccessibilityClientMock.mockClear();
openExternalMock.mockClear();
});

it("doesNotRequestMicrophoneOnStatusRead", async () => {
const caller = createCaller();

const status = await caller.getStatus();

expect(status.microphone).toBe(false);
expect(status.microphoneStatus).toBe("promptable");
expect(getMediaAccessStatusMock).toHaveBeenCalledWith("microphone");
expect(askForMediaAccessMock).not.toHaveBeenCalled();
expect(openExternalMock).not.toHaveBeenCalled();
});

it("requestsMicrophoneOnlyThroughExplicitMutation", async () => {
const caller = createCaller();

await caller.requestMicrophone();

if (process.platform === "darwin") {
expect(askForMediaAccessMock).toHaveBeenCalledWith("microphone");
} else {
expect(openExternalMock).toHaveBeenCalledTimes(1);
}
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { describe, expect, it, mock } from "bun:test";

mock.module("electron", () => ({
app: {
relaunch: mock(() => {}),
},
shell: {
openExternal: mock(async () => {}),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ type SystemPreferencesApi = Pick<
typeof electronSystemPreferences,
"askForMediaAccess" | "getMediaAccessStatus" | "isTrustedAccessibilityClient"
>;
export type MicrophonePermissionStatus =
| "granted"
| "denied"
| "promptable"
| "unknown";

function getElectronShell(): ShellApi {
return (require("electron") as Partial<typeof import("electron")>)
Expand Down Expand Up @@ -49,20 +54,41 @@ export function checkMicrophone({
}: {
systemPreferencesApi?: Pick<SystemPreferencesApi, "getMediaAccessStatus">;
} = {}): boolean {
return getMicrophonePermissionStatus({ systemPreferencesApi }) === "granted";
}

export function getMicrophonePermissionStatus({
systemPreferencesApi = getElectronSystemPreferences(),
}: {
systemPreferencesApi?: Pick<SystemPreferencesApi, "getMediaAccessStatus">;
} = {}): MicrophonePermissionStatus {
try {
return (
systemPreferencesApi?.getMediaAccessStatus("microphone") === "granted"
);
const status = systemPreferencesApi?.getMediaAccessStatus("microphone");

if (status === "granted") {
return "granted";
}
if (status === "denied" || status === "restricted") {
return "denied";
}
if (status === "not-determined") {
return "promptable";
}

return "unknown";
} catch {
return false;
return "unknown";
}
}

export function getPermissionStatus() {
const microphoneStatus = getMicrophonePermissionStatus();

return {
fullDiskAccess: checkFullDiskAccess(),
accessibility: checkAccessibility(),
microphone: checkMicrophone(),
microphone: microphoneStatus === "granted",
microphoneStatus,
};
}

Expand Down
37 changes: 37 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
DEFAULT_SHOW_RESOURCE_MONITOR,
DEFAULT_TERMINAL_LINK_BEHAVIOR,
DEFAULT_USE_COMPACT_TERMINAL_ADD_BUTTON,
DEFAULT_VOICE_INPUT_ENABLED,
} from "shared/constants";
import { normalizePresetProjectIds } from "shared/preset-project-targeting";
import {
Expand Down Expand Up @@ -260,8 +261,25 @@ export function getPresetsForTrigger(
);
}

function getSettingsPayload() {
const row = getSettings();
return {
confirmOnQuit: row.confirmOnQuit ?? DEFAULT_CONFIRM_ON_QUIT,
fileOpenMode: row.fileOpenMode ?? DEFAULT_FILE_OPEN_MODE,
openLinksInApp: row.openLinksInApp ?? DEFAULT_OPEN_LINKS_IN_APP,
showResourceMonitor:
row.showResourceMonitor ?? DEFAULT_SHOW_RESOURCE_MONITOR,
terminalLinkBehavior:
row.terminalLinkBehavior ?? DEFAULT_TERMINAL_LINK_BEHAVIOR,
voiceInputEnabled: row.voiceInputEnabled ?? DEFAULT_VOICE_INPUT_ENABLED,
};
}

export const createSettingsRouter = () => {
return router({
getSettings: publicProcedure
.input(z.void())
.query(() => getSettingsPayload()),
getTerminalPresets: publicProcedure.query(() => {
const row = getSettings();
if (!row.terminalPresetsInitialized) {
Expand Down Expand Up @@ -925,6 +943,25 @@ export const createSettingsRouter = () => {
return row.showResourceMonitor ?? DEFAULT_SHOW_RESOURCE_MONITOR;
}),

getVoiceInputEnabled: publicProcedure.input(z.void()).query(() => {
return getSettingsPayload().voiceInputEnabled;
}),

setVoiceInputEnabled: publicProcedure
.input(z.object({ enabled: z.boolean() }))
.mutation(({ input }) => {
localDb
.insert(settings)
.values({ id: 1, voiceInputEnabled: input.enabled })
.onConflictDoUpdate({
target: settings.id,
set: { voiceInputEnabled: input.enabled },
})
.run();

return { success: true };
}),

setShowResourceMonitor: publicProcedure
.input(z.object({ enabled: z.boolean() }))
.mutation(({ input }) => {
Expand Down
178 changes: 178 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/voice-input.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Database } from "bun:sqlite";
import { beforeEach, describe, expect, it, mock } from "bun:test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { drizzle } from "drizzle-orm/bun-sqlite";
import * as schema from "../../../../../../../packages/local-db/src";

const sqlite = new Database(":memory:");

function createPreVoiceInputSettingsTable() {
sqlite.exec(`
DROP TABLE IF EXISTS settings;
CREATE TABLE settings (
id integer PRIMARY KEY DEFAULT 1,
last_active_workspace_id text,
terminal_presets text,
terminal_presets_initialized integer,
agent_preset_overrides text,
agent_custom_definitions text,
agent_preset_permissions_migrated_at integer,
selected_ringtone_id text,
active_organization_id text,
confirm_on_quit integer,
terminal_link_behavior text,
persist_terminal integer DEFAULT true,
auto_apply_default_preset integer,
branch_prefix_mode text,
branch_prefix_custom text,
notification_sounds_muted integer,
notification_volume integer,
delete_local_branch integer,
file_open_mode text,
show_presets_bar integer,
use_compact_terminal_add_button integer,
terminal_font_family text,
terminal_font_size integer,
editor_font_family text,
editor_font_size integer,
show_resource_monitor integer,
worktree_base_dir text,
open_links_in_app integer,
default_editor text,
expose_host_service_via_relay integer
);
`);
}

function applyVoiceInputMigration() {
const migrationSql = readFileSync(
resolve(
process.cwd(),
"packages/local-db/drizzle/0042_add_voice_input_enabled.sql",
),
"utf8",
);

for (const statement of migrationSql.split("--> statement-breakpoint")) {
const trimmedStatement = statement.trim();
if (trimmedStatement.length > 0) {
sqlite.exec(trimmedStatement);
}
}
}

const testLocalDb = drizzle(sqlite, { schema });

mock.module("@superset/local-db", () => schema);

const getHostServiceCoordinatorMock = mock(() => ({
getActiveOrganizationIds: () => [],
restartAll: async () => {},
}));
const loadTokenMock = mock(async () => ({ token: null }));

mock.module("electron", () => ({
app: {
relaunch: mock(() => {}),
},
shell: {
openExternal: mock(async () => undefined),
},
systemPreferences: {
askForMediaAccess: mock(async () => false),
getMediaAccessStatus: mock(() => "not-determined"),
isTrustedAccessibilityClient: mock(() => false),
},
}));

mock.module("main/env.main", () => ({
env: {
NODE_ENV: "test",
NEXT_PUBLIC_API_URL: "https://api.superset.test",
},
}));

mock.module("main/index", () => ({
exitImmediately: mock(() => {}),
}));

mock.module("main/lib/custom-ringtones", () => ({
hasCustomRingtone: mock(() => false),
}));

mock.module("main/lib/host-service-coordinator", () => ({
getHostServiceCoordinator: getHostServiceCoordinatorMock,
}));

mock.module("main/lib/local-db", () => ({
localDb: testLocalDb,
}));

mock.module("../auth/utils/auth-functions", () => ({
loadToken: loadTokenMock,
}));

mock.module("../workspaces/utils/git", () => ({
NotGitRepoError: class NotGitRepoError extends Error {},
getGitAuthorName: mock(async () => null),
getGitHubUsername: mock(async () => null),
}));

const { createSettingsRouter } = await import("./index");

function createCaller() {
return createSettingsRouter().createCaller({});
}

describe("voice input settings", () => {
beforeEach(() => {
createPreVoiceInputSettingsTable();
applyVoiceInputMigration();
getHostServiceCoordinatorMock.mockClear();
loadTokenMock.mockClear();
});

it("returnsDefaultDisabledVoiceInputSetting", async () => {
const caller = createCaller();

expect(await caller.getVoiceInputEnabled()).toBe(false);
});

it("persistsVoiceInputEnabledSetting", async () => {
const caller = createCaller();

await caller.setVoiceInputEnabled({ enabled: true });
expect(await caller.getVoiceInputEnabled()).toBe(true);

await caller.setVoiceInputEnabled({ enabled: false });
expect(await caller.getVoiceInputEnabled()).toBe(false);
});

it("exposesVoiceInputInGetSettings", async () => {
const caller = createCaller();

await caller.setVoiceInputEnabled({ enabled: true });

expect(await caller.getSettings()).toMatchObject({
confirmOnQuit: true,
fileOpenMode: "split-pane",
openLinksInApp: false,
showResourceMonitor: true,
terminalLinkBehavior: "file-viewer",
voiceInputEnabled: true,
});
});

it("keepsVoiceSettingsLocalOnly", async () => {
const caller = createCaller();

await caller.setVoiceInputEnabled({ enabled: true });
expect(await caller.getVoiceInputEnabled()).toBe(true);

const payload = await caller.getSettings();
expect(payload).not.toHaveProperty("activeOrganizationId");
expect(getHostServiceCoordinatorMock).not.toHaveBeenCalled();
expect(loadTokenMock).not.toHaveBeenCalled();
});
});
Loading
Loading