Skip to content
Open
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
30 changes: 0 additions & 30 deletions apps/whispering/src-tauri/capabilities/transformation-picker.json

This file was deleted.

6 changes: 3 additions & 3 deletions apps/whispering/src/lib/commands.browser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { SatisfiedCommand } from '$lib/commands';

/**
* Browser builds contribute no extra commands. The transformation picker is
* Browser builds contribute no extra commands. The recipe picker is
* desktop-only: it captures the selection in another app via a simulated system
* copy and opens a separate Tauri window, neither of which a browser tab can do.
* The cross-platform `Run transformation on clipboard` command covers the web
* transformation path. See `commands.tauri.ts` for the desktop addition.
* The cross-platform `Run recipe on clipboard` command covers the web recipe
* path. See `commands.tauri.ts` for the desktop addition.
*/
export const platformCommands = [] as const satisfies SatisfiedCommand[];
9 changes: 4 additions & 5 deletions apps/whispering/src/lib/commands.tauri.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SatisfiedCommand, ShortcutEventState } from '$lib/commands';
import { openTransformationPicker } from '$lib/operations/transformation-picker';
import { openRecipePicker } from '$lib/operations/recipe-picker';

/**
* Desktop-only commands, spread into the registry by the `#platform/commands`
Expand All @@ -9,8 +9,8 @@ import { openTransformationPicker } from '$lib/operations/transformation-picker'
*/
export const platformCommands = [
{
id: 'openTransformationPicker',
title: 'Open transformation picker',
id: 'openRecipePicker',
title: 'Open recipe picker',
// Fire on release, not press: the global accelerator carries a Cmd/Ctrl+Shift
// chord, and capturing on press synthesizes Cmd/Ctrl+C while that chord is
// still held, so the foreground app sees Cmd+Shift+C instead of a clean copy.
Expand All @@ -19,8 +19,7 @@ export const platformCommands = [
// in-app shortcut would never fire. The callback guard runs once, on release.
on: ['Pressed', 'Released'],
callback: (state?: ShortcutEventState) => {
if (state === 'Released' || state === undefined)
openTransformationPicker();
if (state === 'Released' || state === undefined) openRecipePicker();
},
},
] as const satisfies SatisfiedCommand[];
14 changes: 7 additions & 7 deletions apps/whispering/src/lib/commands.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { platformCommands } from '#platform/commands';
import { runRecipeOnClipboard } from '$lib/operations/recipe-clipboard';
import {
cancelRecording,
startManualRecording,
stopManualRecording,
toggleManualRecording,
toggleVadRecording,
} from '$lib/operations/recording';
import { runTransformationOnClipboard } from '$lib/operations/transformation-clipboard';

/**
* Registry of available commands in the application.
Expand All @@ -18,9 +18,9 @@ import { runTransformationOnClipboard } from '$lib/operations/transformation-cli
* command registry.
*
* Platform split: `sharedCommands` exist in every build. Desktop-only commands
* (the transformation picker, which captures a selection from another app and
* opens a Tauri window) come from the `#platform/commands` seam, so a browser
* build never imports their Tauri-only code and never offers them as shortcuts.
* (the recipe picker, which captures a selection from another app and opens a
* Tauri window) come from the `#platform/commands` seam, so a browser build
* never imports their Tauri-only code and never offers them as shortcuts.
*/

/**
Expand Down Expand Up @@ -84,10 +84,10 @@ const sharedCommands = [
callback: () => toggleVadRecording(),
},
{
id: 'runTransformationOnClipboard',
title: 'Run transformation on clipboard',
id: 'runRecipeOnClipboard',
title: 'Run recipe on clipboard',
on: ['Pressed'],
callback: () => runTransformationOnClipboard(),
callback: () => runRecipeOnClipboard(),
},
] as const satisfies SatisfiedCommand[];

Expand Down
132 changes: 0 additions & 132 deletions apps/whispering/src/lib/components/CandidateCards.svelte

This file was deleted.

16 changes: 8 additions & 8 deletions apps/whispering/src/lib/components/OutputDeliveryControls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
// One scope's full output delivery UI: copy to clipboard, paste at cursor (with
// its macOS Accessibility notice), and the dependent "press Enter" sub-toggle.
// These three always travel together because delivery.ts routes both the
// transcription and transformation scopes through the same path (clipboard,
// then a synthetic Cmd+V for cursor, then Enter), so the same trio and the same
// transcription and recipe scopes through the same path (clipboard, then a
// synthetic Cmd+V for cursor, then Enter), so the same trio and the same
// Accessibility caveat apply to both. Driving every surface (the two Settings
// output groups and the home capture popover) from this one component keeps the
// labels, the remediation copy, and the gating from ever drifting.
Expand All @@ -26,7 +26,7 @@
// in exactly one place. Paste-at-cursor stays interactive without the grant (it
// records intent); the capability recheck on window focus starts the paste the
// moment Accessibility lands, with no second visit to flip it back on.
type OutputScope = 'transcription' | 'transformation';
type OutputScope = 'transcription' | 'recipe';
let { scope }: { scope: OutputScope } = $props();

const SCOPES = {
Expand All @@ -36,11 +36,11 @@
cursor: 'output.transcription.cursor',
enter: 'output.transcription.enter',
},
transformation: {
noun: 'transformed text',
clipboard: 'output.transformation.clipboard',
cursor: 'output.transformation.cursor',
enter: 'output.transformation.enter',
recipe: {
noun: 'recipe output',
clipboard: 'output.recipe.clipboard',
cursor: 'output.recipe.cursor',
enter: 'output.recipe.enter',
},
} satisfies Record<
OutputScope,
Expand Down
77 changes: 77 additions & 0 deletions apps/whispering/src/lib/components/RecipePicker.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<script lang="ts">
import { Badge } from '@epicenter/ui/badge';
import * as Command from '@epicenter/ui/command';
import * as Modal from '@epicenter/ui/modal';
import { deliverRecipeResult } from '$lib/operations/delivery';
import { runRecipe } from '$lib/operations/run-recipe';
import { sound } from '$lib/operations/sound';
import { report } from '$lib/report';
import { isBuiltinRecipeId } from '$lib/state/builtin-recipes';
import { recipePicker } from '$lib/state/recipe-picker.svelte';
import { recipes } from '$lib/state/recipes.svelte';
import type { Recipe } from '$lib/workspace';

/**
* The in-app Recipe picker: a command palette the `openRecipePicker` /
* `runRecipeOnClipboard` shortcuts raise over the captured text. Mounted once
* in the app layout; visibility is driven entirely by the `recipePicker` rune.
* Picking a recipe runs it on the captured source and delivers the take. See
* ADR 0046.
*/

async function run(recipe: Recipe) {
const input = recipePicker.source;
recipePicker.close();
const loading = report.loading({
title: `Running ${recipe.name}...`,
description: 'Reshaping your text with AI.',
});
const { data, error } = await runRecipe({ input, recipe });
if (error) {
loading.reject({
title: `Couldn't run ${recipe.name}`,
description: error.message,
cause: error,
});
return;
}
await sound.playSoundIfEnabled('recipeComplete');
const { notice } = await deliverRecipeResult({
text: data,
recordingId: null,
});
loading.resolve(notice);
}
</script>

<Modal.Root
bind:open={
() => recipePicker.isOpen, (open) => { if (!open) recipePicker.close(); }
}
>
<Modal.Content class="overflow-hidden p-0">
<Modal.Title class="sr-only">Run a recipe</Modal.Title>
<Modal.Description class="sr-only">
Pick a recipe to run on your captured text.
</Modal.Description>
<Command.Root loop>
<Command.Input placeholder="Run a recipe..." />
<Command.List>
<Command.Empty>No recipes found.</Command.Empty>
<Command.Group>
{#each recipes.pickable as recipe (recipe.id)}
<Command.Item value={recipe.name} onSelect={() => run(recipe)}>
{#if recipe.icon}
<span aria-hidden="true">{recipe.icon}</span>
{/if}
<span class="flex-1 truncate">{recipe.name}</span>
{#if isBuiltinRecipeId(recipe.id)}
<Badge variant="secondary">Built-in</Badge>
{/if}
</Command.Item>
{/each}
</Command.Group>
</Command.List>
</Command.Root>
</Modal.Content>
</Modal.Root>
Loading
Loading