Skip to content
Draft
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
1 change: 1 addition & 0 deletions apps/emdash-desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default defineConfig({
alias: {
'@': resolve('src'),
'@renderer': resolve('src/renderer'),
'@emdash/ui': resolve('../../packages/ui/src'),
'@shared': resolve('src/shared'),
'@root': resolve('.'),
// cli-agent-plugins metadata/icons chunks transitively reference node:buffer
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@emdash/ui/react/primitives';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Command } from 'cmdk';
import { Activity, FolderOpen, GitBranch, MessageSquare, type LucideIcon } from 'lucide-react';
import { useObserver } from 'mobx-react-lite';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
Expand Down Expand Up @@ -50,11 +57,7 @@ const KIND_ICON: Record<string, React.ReactNode> = {
conversation: <MessageSquare size={14} className="shrink-0 text-foreground/40" />,
};

const GROUP_CLASS = cn(
'[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5',
'[&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
'[&_[cmdk-group-heading]]:text-foreground/50'
);
const GROUP_CLASS = '[&_[cmdk-group-heading]]:text-foreground/50';

// Ordered allowlists for the "Suggested Actions" empty-state group. Defined at
// module scope so the arrays keep stable references across renders.
Expand Down Expand Up @@ -87,11 +90,11 @@ function PaletteItem({
KIND_ICON[item.kind]
);
return (
<Command.Item value={value} onSelect={onSelect} className={cn(PALETTE_ITEM_CLASS, 'group')}>
<CommandItem value={value} onSelect={onSelect} className={cn(PALETTE_ITEM_CLASS, 'group')}>
{iconNode}
<span className="flex-1 truncate">{item.title}</span>
{action?.shortcut && <Shortcut hotkey={action.shortcut} variant="keycaps" />}
</Command.Item>
</CommandItem>
);
}

Expand All @@ -105,13 +108,13 @@ function PaletteFileItem({
onSelect: () => void;
}) {
return (
<Command.Item value={value} onSelect={onSelect} className={PALETTE_ITEM_CLASS}>
<CommandItem value={value} onSelect={onSelect} className={PALETTE_ITEM_CLASS}>
<FileIcon filename={item.title} size={14} />
<span className="flex min-w-0 flex-1 items-baseline gap-2 overflow-hidden">
<span className="shrink-0">{item.title}</span>
<span className="truncate text-xs text-foreground/40">{item.subtitle}</span>
</span>
</Command.Item>
</CommandItem>
);
}

Expand Down Expand Up @@ -299,20 +302,20 @@ export function CommandPaletteModal({
return (
<Command className="flex flex-col overflow-hidden" shouldFilter={false} loop>
<div className="border-b border-foreground/10 px-1">
<Command.Input
<CommandInput
value={query}
onValueChange={setQuery}
placeholder="Search tasks, projects, actions…"
className="w-full bg-transparent px-3 py-3 text-sm outline-none placeholder:text-foreground/40"
className="px-3 py-3 placeholder:text-foreground/40"
autoFocus
/>
</div>
<Command.List className="h-96 overflow-y-auto p-1">
<CommandList className="h-96 p-1">
{query ? (
<>
<Command.Empty className="py-8 text-center text-sm text-foreground/40">
<CommandEmpty className="py-8 text-foreground/40">
No results for &ldquo;{query}&rdquo;
</Command.Empty>
</CommandEmpty>
{matchedResourceMonitor && (
<PaletteItem
value={matchedResourceMonitor.id}
Expand Down Expand Up @@ -409,14 +412,14 @@ export function CommandPaletteModal({
navigate={navigate}
/>
{actionResults.length > 0 && (
<Command.Group heading="Suggested Actions" className={GROUP_CLASS}>
<CommandGroup heading="Suggested Actions" className={GROUP_CLASS}>
{actionResults.map((item) => (
<PaletteItem key={item.id} value={item.id} item={item} onSelect={item.execute} />
))}
</Command.Group>
</CommandGroup>
)}
{taskResults.length > 0 && (
<Command.Group heading="Recent Tasks" className={GROUP_CLASS}>
<CommandGroup heading="Recent Tasks" className={GROUP_CLASS}>
{taskResults.slice(0, 5).map((item) => {
const store = item.projectId ? getTaskStore(item.projectId, item.id) : undefined;
return store ? (
Expand All @@ -435,7 +438,7 @@ export function CommandPaletteModal({
/>
);
})}
</Command.Group>
</CommandGroup>
)}
{!taskId && (
<PaletteProjectsGroup
Expand All @@ -446,7 +449,7 @@ export function CommandPaletteModal({
/>
)}
{taskId && conversationResults.length > 0 && (
<Command.Group heading="Recent Conversations" className={GROUP_CLASS}>
<CommandGroup heading="Recent Conversations" className={GROUP_CLASS}>
{conversationResults.slice(0, 5).map((item) => {
const convStore = item.taskId
? conversationRegistry.get(item.taskId)?.conversations.get(item.id)
Expand All @@ -467,11 +470,11 @@ export function CommandPaletteModal({
/>
);
})}
</Command.Group>
</CommandGroup>
)}
</>
)}
</Command.List>
</CommandList>

<div className="flex items-center gap-4 border-t border-foreground/10 px-3 py-2">
<span className="flex items-center gap-1 text-xs text-foreground/40">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Command } from 'cmdk';
import { CommandItem } from '@emdash/ui/react/primitives';
import { observer } from 'mobx-react-lite';
import { AgentStatusIndicator } from '@renderer/features/tasks/components/agent-status-indicator';
import type { ConversationStore } from '@renderer/features/tasks/conversations/conversation-manager';
Expand All @@ -18,10 +18,10 @@ export const PaletteConversationItem = observer(function PaletteConversationItem
const title = formatConversationTitleForDisplay(conv.data.providerId, conv.data.title ?? '');

return (
<Command.Item value={value} onSelect={onSelect} className={PALETTE_ITEM_CLASS}>
<CommandItem value={value} onSelect={onSelect} className={PALETTE_ITEM_CLASS}>
<AgentIcon id={conv.data.providerId} size={16} />
<span className="flex-1 truncate">{title}</span>
<AgentStatusIndicator status={conv.indicatorStatus} disableTooltip />
</Command.Item>
</CommandItem>
);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Command } from 'cmdk';
import { CommandGroup } from '@emdash/ui/react/primitives';
import { useObserver } from 'mobx-react-lite';
import {
asMounted,
Expand Down Expand Up @@ -72,7 +72,7 @@ export function PaletteNotificationsGroup({
if (items.length === 0) return null;

return (
<Command.Group heading="Notifications" className={GROUP_CLASS}>
<CommandGroup heading="Notifications" className={GROUP_CLASS}>
{items.map((item) => {
if (item.kind === 'conversation') {
return (
Expand Down Expand Up @@ -108,6 +108,6 @@ export function PaletteNotificationsGroup({
/>
);
})}
</Command.Group>
</CommandGroup>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Command } from 'cmdk';
import { CommandGroup, CommandItem } from '@emdash/ui/react/primitives';
import { FolderOpen } from 'lucide-react';
import { useObserver } from 'mobx-react-lite';
import {
Expand Down Expand Up @@ -45,9 +45,9 @@ export function PaletteProjectsGroup({
if (visible.length === 0) return null;

return (
<Command.Group heading="Projects" className={GROUP_CLASS}>
<CommandGroup heading="Projects" className={GROUP_CLASS}>
{visible.map((p) => (
<Command.Item
<CommandItem
key={p.id}
value={`project:${p.id}`}
onSelect={() => {
Expand All @@ -58,8 +58,8 @@ export function PaletteProjectsGroup({
>
<FolderOpen size={14} className="shrink-0 text-foreground/40" />
<span className="flex-1 truncate">{p.name}</span>
</Command.Item>
</CommandItem>
))}
</Command.Group>
</CommandGroup>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Command } from 'cmdk';
import { CommandItem } from '@emdash/ui/react/primitives';
import { GitBranch } from 'lucide-react';
import { observer } from 'mobx-react-lite';
import { AgentStatusIndicator } from '@renderer/features/tasks/components/agent-status-indicator';
Expand All @@ -18,10 +18,10 @@ export const PaletteTaskItem = observer(function PaletteTaskItem({
const status = taskAgentStatus(taskStore);

return (
<Command.Item value={value} onSelect={onSelect} className={PALETTE_ITEM_CLASS}>
<CommandItem value={value} onSelect={onSelect} className={PALETTE_ITEM_CLASS}>
<GitBranch size={14} className="shrink-0 text-foreground/40" />
<span className="flex-1 truncate">{taskStore.data.name}</span>
<AgentStatusIndicator status={status} disableTooltip />
</Command.Item>
</CommandItem>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const SidebarItemMiniButton = React.forwardRef<
SidebarItemMiniButton.displayName = 'SidebarItemMiniButton';

const sidebarMenuItemClass =
'flex w-full font-normal h-8 text-foreground-tertiary-muted rounded-lg items-center hover:bg-background-tertiary-1 hover:text-foreground-tertiary gap-2 px-3 py-2 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[active=true]:bg-background-tertiary-2 data-[active=true]:text-foreground-tertiary';
'group flex w-full font-normal h-8 text-foreground-tertiary-muted rounded-lg items-center hover:bg-background-tertiary-1 hover:text-foreground-tertiary gap-2 px-3 py-2 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[active=true]:bg-background-tertiary-2 data-[active=true]:text-foreground-tertiary';

interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
isActive?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { TabProvider } from './tab-provider';

// ── Type aliases ──────────────────────────────────────────────────────────────

// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any -- Registry boundary must preserve provider variance.
export type AnyTabProvider = TabProvider<any, any, any, any, any>;

/**
Expand Down Expand Up @@ -87,8 +87,7 @@ export function createTabRegistry<const P extends readonly AnyTabProvider[]>(
has(kind: string): boolean {
return map.has(kind);
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_providers: providers as any,
_providers: providers,
};
return registry;
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,18 @@ type TaskProviders = TaskRegistry['_providers'];
export type TaskTabKind = KindOf<TaskRegistry>;
export type TaskOpenArgsOf<K extends TaskTabKind> = OpenArgsOf<TaskRegistry, K>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ProviderFor<K extends TaskTabKind> = Extract<TaskProviders[number], { kind: K }>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ProviderEntry<P> = P extends AnyTabProvider & { serialize(entry: infer E): unknown }
? E
: never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ProviderResolved<P> = P extends AnyTabProvider & {
resolve(entry: any, ctx: any): (infer RD) | null;
resolve(entry: unknown, ctx: unknown): (infer RD) | null;
}
? RD
: never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ProviderData<P> = P extends AnyTabProvider & { deserialize(data: infer D, ctx: any): unknown }
type ProviderData<P> = P extends AnyTabProvider & {
deserialize(data: infer D, ctx: unknown): unknown;
}
? D
: never;

Expand Down
29 changes: 0 additions & 29 deletions apps/emdash-desktop/src/renderer/lib/ui/kbd.tsx

This file was deleted.

34 changes: 18 additions & 16 deletions apps/emdash-desktop/src/renderer/lib/ui/shortcut-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { parseHotkey } from '@tanstack/react-hotkeys';
import { describe, expect, it } from 'vitest';
import {
describeShortcut,
formatShortcutDisplay,
formatShortcutKey,
getShortcutKeyOpticalAlignClass,
getShortcutKeys,
Expand Down Expand Up @@ -48,21 +49,22 @@ describe('shortcut formatting', () => {
expect(formatShortcutKey('End', 'mac')).toBe('End');
});

it('optically raises punctuation and operators with low visual centers', () => {
expect(getShortcutKeyOpticalAlignClass('(')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass(')')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('+')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass(',')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('-')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('.')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass(':')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass(';')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('=')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('[')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass(']')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('{')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('}')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('/')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('\\')).toBe('-translate-y-px');
it('formats shortcut display keys without separators', () => {
expect(formatShortcutDisplay(['Meta', 'Shift', 'K'], 'mac')).toBe('⌘⇧K');
expect(formatShortcutDisplay(['Control', 'Alt', 'Delete'], 'windows')).toBe('CtrlAltDel');
});

it('optically aligns single uppercase letters and digits', () => {
expect(getShortcutKeyOpticalAlignClass('A')).toBe('translate-y-[0.5px]');
expect(getShortcutKeyOpticalAlignClass('7')).toBe('translate-y-[0.5px]');
expect(getShortcutKeyOpticalAlignClass('a')).toBeUndefined();
});

it('optically aligns modifier and navigation keycaps', () => {
expect(getShortcutKeyOpticalAlignClass('Alt')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('Control')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('Meta')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('Shift')).toBe('-translate-y-px');
expect(getShortcutKeyOpticalAlignClass('Enter')).toBe('translate-y-px');
});
});
Comment thread
janburzinski marked this conversation as resolved.
21 changes: 20 additions & 1 deletion apps/emdash-desktop/src/renderer/lib/ui/shortcut-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ const SPOKEN_KEY_LABELS: Record<string, string> = {
};

const OPTICAL_ALIGN_CLASS: Record<string, string> = {
Alt: '-translate-y-px',
ArrowDown: 'translate-y-px',
ArrowLeft: '-translate-x-px',
ArrowRight: 'translate-x-px',
ArrowUp: '-translate-y-px',
Backspace: 'translate-y-px',
Control: '-translate-y-px',
Enter: 'translate-y-px',
Meta: '-translate-y-px',
Shift: '-translate-y-px',
'(': '-translate-y-px',
')': '-translate-y-px',
'+': '-translate-y-px',
Expand Down Expand Up @@ -143,8 +153,17 @@ export function formatShortcutKey(key: string, platform: Platform = detectPlatfo
return VISIBLE_KEY_LABELS[key] ?? KEY_DISPLAY_SYMBOLS[key] ?? normalizeVisibleKeyLabel(key);
}

export function formatShortcutDisplay(
keys: readonly string[],
platform: Platform = detectPlatform()
): string {
return keys.map((key) => formatShortcutKey(key, platform)).join('');
}

export function getShortcutKeyOpticalAlignClass(key: string): string | undefined {
return OPTICAL_ALIGN_CLASS[key];
if (OPTICAL_ALIGN_CLASS[key]) return OPTICAL_ALIGN_CLASS[key];
if (/^[A-Z0-9]$/.test(key)) return 'translate-y-[0.5px]';
return undefined;
}

export function describeShortcut(
Expand Down
Loading
Loading