diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index 29f34b958..b4a1c863e 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -191,6 +191,7 @@ export function DesktopController() { currentView, openAgents, openCommandCenterSection, + openStarmap, profilesOpen, settingsOpen, starmapOpen, @@ -739,6 +740,7 @@ export function DesktopController() { busyRef, createBackendSessionForSend, handleSkinCommand, + openMemoryGraph: openStarmap, refreshSessions, requestGateway, resumeStoredSession: resumeSession, diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx index c824e6e3c..fd50574ea 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx @@ -54,6 +54,7 @@ function Harness({ busyRef, onReady, onSeedState, + openMemoryGraph, refreshSessions, requestGateway, resumeStoredSession, @@ -63,6 +64,7 @@ function Harness({ busyRef?: MutableRefObject onReady: (handle: HarnessHandle) => void onSeedState?: (state: Record) => void + openMemoryGraph?: () => void refreshSessions: () => Promise requestGateway: (method: string, params?: Record) => Promise resumeStoredSession?: (storedSessionId: string) => Promise | void @@ -91,6 +93,7 @@ function Harness({ busyRef: localBusyRef, createBackendSessionForSend: async () => RUNTIME_SESSION_ID, handleSkinCommand: () => '', + openMemoryGraph: openMemoryGraph ?? (() => undefined), refreshSessions, requestGateway, resumeStoredSession: resumeStoredSession ?? (() => undefined), @@ -369,6 +372,29 @@ describe('usePromptActions desktop slash pickers', () => { expect(requestGateway).not.toHaveBeenCalledWith('slash.exec', expect.anything()) }) + it('opens the memory graph overlay for /journey and its aliases instead of hitting the backend', async () => { + const openMemoryGraph = vi.fn() + const requestGateway = vi.fn(async () => ({}) as never) + + let handle: HarnessHandle | null = null + render( + (handle = h)} + openMemoryGraph={openMemoryGraph} + refreshSessions={async () => undefined} + requestGateway={requestGateway} + /> + ) + + await handle!.submitText('/journey') + await handle!.submitText('/memory-graph') + await handle!.submitText('/learning') + + expect(openMemoryGraph).toHaveBeenCalledTimes(3) + expect(requestGateway).not.toHaveBeenCalledWith('slash.exec', expect.anything()) + expect(requestGateway).not.toHaveBeenCalledWith('command.dispatch', expect.anything()) + }) + it('marks a timed-out handoff as failed so the next attempt can retry', async () => { vi.useFakeTimers() const calls: { method: string; params?: Record }[] = [] diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts index 01d25c95d..66b4667b2 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts @@ -2,7 +2,7 @@ import type { AppendMessage, ThreadMessage } from '@assistant-ui/react' import { useStore } from '@nanostores/react' import { type MutableRefObject, useCallback, useEffect, useRef } from 'react' -import { transcribeAudio, PROMPT_SUBMIT_REQUEST_TIMEOUT_MS } from '@/hermes' +import { PROMPT_SUBMIT_REQUEST_TIMEOUT_MS, transcribeAudio } from '@/hermes' import { useI18n } from '@/i18n' import { stripAnsi } from '@/lib/ansi' import { branchGroupForUser, type ChatMessage, chatMessageText, textPart } from '@/lib/chat-messages' @@ -158,6 +158,7 @@ interface PromptActionsOptions { branchCurrentSession: () => Promise createBackendSessionForSend: (preview?: string | null) => Promise handleSkinCommand: (arg: string) => string + openMemoryGraph: () => void refreshSessions: () => Promise requestGateway: (method: string, params?: Record, timeoutMs?: number) => Promise resumeStoredSession: (storedSessionId: string) => Promise | void @@ -185,6 +186,7 @@ export function usePromptActions({ branchCurrentSession, createBackendSessionForSend, handleSkinCommand, + openMemoryGraph, refreshSessions, requestGateway, resumeStoredSession, @@ -447,6 +449,7 @@ export function usePromptActions({ createBackendSessionForSend, handleSkinCommand, handoffSession, + openMemoryGraph, refreshSessions, requestGateway, resumeStoredSession, diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts index 3c918c7ed..def08fe6e 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts @@ -54,6 +54,7 @@ interface SlashCommandDeps { platform: string, options?: { onProgress?: (state: string) => void; sessionId?: string } ) => Promise<{ ok: boolean; error?: string }> + openMemoryGraph: () => void refreshSessions: () => Promise requestGateway: GatewayRequest resumeStoredSession: (storedSessionId: string) => Promise | void @@ -75,6 +76,7 @@ export function useSlashCommand(deps: SlashCommandDeps) { createBackendSessionForSend, handleSkinCommand, handoffSession, + openMemoryGraph, refreshSessions, requestGateway, resumeStoredSession, @@ -388,6 +390,13 @@ export function useSlashCommand(deps: SlashCommandDeps) { renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`) } }, + // /journey (aliases /learning, /memory-graph) opens the memory graph + // overlay — the desktop's visual counterpart of the TUI journey + // timeline — instead of printing a text rendering into the transcript. + // Args are ignored, matching the TUI overlay behavior. + journey: async () => { + openMemoryGraph() + }, // /hatch opens the pet generator overlay (the desktop's rich, multi-step // generate→pick→hatch→adopt flow). A typed description seeds the prompt // so `/hatch a cyber fox` lands on the composer step prefilled. @@ -612,6 +621,7 @@ export function useSlashCommand(deps: SlashCommandDeps) { createBackendSessionForSend, handleSkinCommand, handoffSession, + openMemoryGraph, refreshSessions, requestGateway, resumeStoredSession, diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/submit.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions/submit.ts index fba7eac82..5127b534f 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions/submit.ts +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/submit.ts @@ -1,7 +1,7 @@ import { type MutableRefObject, useCallback } from 'react' -import type { Translations } from '@/i18n' import { PROMPT_SUBMIT_REQUEST_TIMEOUT_MS } from '@/hermes' +import type { Translations } from '@/i18n' import { type ChatMessage, textPart } from '@/lib/chat-messages' import { optimisticAttachmentRef } from '@/lib/chat-runtime' import { setMutableRef } from '@/lib/mutable-ref' diff --git a/apps/desktop/src/lib/desktop-slash-commands.test.ts b/apps/desktop/src/lib/desktop-slash-commands.test.ts index 8e30e5bfc..0a108e77b 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.test.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.test.ts @@ -73,6 +73,18 @@ describe('desktop slash command curation', () => { expect(resolveDesktopCommand('/browser')?.args).toBe(true) }) + it('routes /journey (and aliases) to the memory graph overlay action', () => { + expect(resolveDesktopCommand('/journey')?.surface).toEqual({ kind: 'action', action: 'journey' }) + expect(resolveDesktopCommand('/memory-graph')?.surface).toEqual({ kind: 'action', action: 'journey' }) + expect(resolveDesktopCommand('/learning')?.surface).toEqual({ kind: 'action', action: 'journey' }) + expect(isDesktopSlashCommand('/journey')).toBe(true) + expect(isDesktopSlashCommand('/memory-graph')).toBe(true) + expect(isDesktopSlashSuggestion('/journey')).toBe(true) + // Aliases execute but stay out of the popover. + expect(isDesktopSlashSuggestion('/memory-graph')).toBe(false) + expect(desktopSlashUnavailableMessage('/journey')).toBeNull() + }) + it('allows aliases to execute without cluttering the popover', () => { expect(isDesktopSlashSuggestion('/reset')).toBe(false) expect(isDesktopSlashCommand('/reset')).toBe(true) diff --git a/apps/desktop/src/lib/desktop-slash-commands.ts b/apps/desktop/src/lib/desktop-slash-commands.ts index c5e288195..20d5416f8 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.ts @@ -34,6 +34,7 @@ export type DesktopActionId = | 'handoff' | 'hatch' | 'help' + | 'journey' | 'new' | 'pet' | 'profile' @@ -122,6 +123,12 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [ surface: action('browser'), args: true }, + { + name: '/journey', + description: 'Open the memory graph — skills + memories over time', + aliases: ['/learning', '/memory-graph'], + surface: action('journey') + }, // Overlay pickers { name: '/model', description: 'Switch the model for this session', surface: picker('model'), hidden: true },