('slash.exec', {
session_id: sessionId,
diff --git a/apps/desktop/src/components/assistant-ui/directive-text.tsx b/apps/desktop/src/components/assistant-ui/directive-text.tsx
index 2c5c40d7e5..04436b6de9 100644
--- a/apps/desktop/src/components/assistant-ui/directive-text.tsx
+++ b/apps/desktop/src/components/assistant-ui/directive-text.tsx
@@ -84,6 +84,31 @@ export function formatRefValue(value: string): string {
export const hermesDirectiveFormatter: Unstable_DirectiveFormatter = {
serialize(item: Unstable_TriggerItem): string {
+ const metadata = item.metadata as { rawText?: unknown; insertId?: unknown } | undefined
+ const rawText = typeof metadata?.rawText === 'string' ? metadata.rawText : null
+ const insertId = typeof metadata?.insertId === 'string' ? metadata.insertId : null
+
+ // Live-completion items carry the gateway's original `text` field via metadata.
+ if (rawText) {
+ // Palette starters (`@file:` with empty value) — insert verbatim so the
+ // user can keep typing the path inline.
+ if (rawText.endsWith(':') && !insertId) {
+ return rawText
+ }
+
+ // Simple references like `@diff` / `@staged`.
+ if (!insertId) {
+ return rawText
+ }
+
+ // Typed references with a value — quote when needed.
+ const kindMatch = rawText.match(/^@([^:]+):/)
+ const kind = kindMatch?.[1] ?? item.type
+
+ return `@${kind}:${formatRefValue(insertId)}`
+ }
+
+ // Fallback for legacy callers that pass raw `id` strings.
if (item.id === `${item.type}:`) {
return `@${item.id}`
}
diff --git a/apps/desktop/src/components/notifications.tsx b/apps/desktop/src/components/notifications.tsx
index e412881501..c0d462c2ca 100644
--- a/apps/desktop/src/components/notifications.tsx
+++ b/apps/desktop/src/components/notifications.tsx
@@ -64,7 +64,7 @@ export function NotificationStack() {
return (
diff --git a/apps/desktop/src/lib/voice-playback.ts b/apps/desktop/src/lib/voice-playback.ts
index 5afffe4ae6..02e5a7761f 100644
--- a/apps/desktop/src/lib/voice-playback.ts
+++ b/apps/desktop/src/lib/voice-playback.ts
@@ -11,8 +11,13 @@ import { sanitizeTextForSpeech } from './speech-text'
let currentAudio: HTMLAudioElement | null = null
let sequence = 0
-function currentState(status: VoicePlaybackState['status'], options?: VoicePlaybackOptions): VoicePlaybackState {
+function currentState(
+ status: VoicePlaybackState['status'],
+ options?: VoicePlaybackOptions,
+ audioElement: HTMLAudioElement | null = null
+): VoicePlaybackState {
return {
+ audioElement,
messageId: options?.messageId ?? null,
sequence,
source: options?.source ?? null,
@@ -35,6 +40,7 @@ export function stopVoicePlayback() {
}
setVoicePlaybackState({
+ audioElement: null,
messageId: null,
sequence,
source: null,
@@ -65,7 +71,7 @@ export async function playSpeechText(text: string, options: VoicePlaybackOptions
const audio = new Audio(response.data_url)
currentAudio = audio
- setVoicePlaybackState(currentState('speaking', options))
+ setVoicePlaybackState(currentState('speaking', options, audio))
await new Promise((resolve, reject) => {
audio.addEventListener('ended', () => resolve(), { once: true })
diff --git a/apps/desktop/src/store/voice-playback.ts b/apps/desktop/src/store/voice-playback.ts
index 475a8c0daf..257b1009f7 100644
--- a/apps/desktop/src/store/voice-playback.ts
+++ b/apps/desktop/src/store/voice-playback.ts
@@ -4,6 +4,7 @@ export type VoicePlaybackSource = 'read-aloud' | 'voice-conversation'
export type VoicePlaybackStatus = 'idle' | 'preparing' | 'speaking'
export interface VoicePlaybackState {
+ audioElement: HTMLAudioElement | null
messageId: string | null
sequence: number
source: VoicePlaybackSource | null
@@ -11,6 +12,7 @@ export interface VoicePlaybackState {
}
export const $voicePlayback = atom({
+ audioElement: null,
messageId: null,
sequence: 0,
source: null,
diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css
index 9d63d7c7b4..f26156e949 100644
--- a/apps/desktop/src/styles.css
+++ b/apps/desktop/src/styles.css
@@ -184,29 +184,6 @@ button {
-webkit-app-region: no-drag;
}
-@keyframes voice-wave {
- 0%,
- 100% {
- opacity: 0.45;
- transform: scaleY(0.28);
- }
-
- 35% {
- opacity: 0.95;
- transform: scaleY(1);
- }
-
- 62% {
- opacity: 0.7;
- transform: scaleY(0.52);
- }
-}
-
-.voice-wave-bar {
- animation: voice-wave 860ms ease-in-out infinite;
- transform-origin: center;
-}
-
.composer-liquid-shell-wrap {
pointer-events: none;
border-radius: var(--composer-glass-radius, 20px);
@@ -282,6 +259,32 @@ button {
box-shadow: none !important;
}
+[data-slot='composer-root']:has([data-slot='composer-completion-drawer'][data-state='open'])
+ [data-slot='composer-surface'] {
+ border-top-left-radius: 0 !important;
+ border-top-right-radius: 0 !important;
+ border-top-color: transparent;
+ box-shadow:
+ 0 0.0625rem 0 0.0625rem color-mix(in srgb, var(--dt-ring) 35%, transparent),
+ 0 0.5rem 1.5rem color-mix(in srgb, var(--shadow-ink) 6%, transparent);
+}
+
+[data-slot='composer-root']:has([data-slot='composer-completion-drawer'][data-state='open'])
+ [data-glass-frame='true'] {
+ border-top-left-radius: 0 !important;
+ border-top-right-radius: 0 !important;
+}
+
+[data-slot='composer-root']:has([data-slot='composer-completion-drawer'][data-state='open'])
+ [data-slot='composer-completion-drawer'] {
+ margin-bottom: -0.5rem;
+ border-bottom: 0;
+ box-shadow:
+ 0 -0.0625rem 0 0.0625rem color-mix(in srgb, var(--dt-ring) 35%, transparent),
+ 0 -1rem 2.25rem -1.75rem color-mix(in srgb, var(--dt-foreground) 34%, transparent),
+ 0 -0.3125rem 0.875rem -0.6875rem color-mix(in srgb, var(--dt-foreground) 22%, transparent);
+}
+
.composer-liquid-shell > .glass > .glass__warp {
border-radius: var(--composer-glass-radius, 20px) !important;
}