Merge pull request #57267 from NousResearch/bb/desktop-journey-memory-graph

feat(desktop): /journey opens the memory graph overlay instead of printing text
This commit is contained in:
brooklyn! 2026-07-02 12:30:28 -05:00 committed by GitHub
commit eb506e656a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 62 additions and 2 deletions

View file

@ -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,

View file

@ -54,6 +54,7 @@ function Harness({
busyRef,
onReady,
onSeedState,
openMemoryGraph,
refreshSessions,
requestGateway,
resumeStoredSession,
@ -63,6 +64,7 @@ function Harness({
busyRef?: MutableRefObject<boolean>
onReady: (handle: HarnessHandle) => void
onSeedState?: (state: Record<string, unknown>) => void
openMemoryGraph?: () => void
refreshSessions: () => Promise<void>
requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
resumeStoredSession?: (storedSessionId: string) => Promise<void> | 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(
<Harness
onReady={h => (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<string, unknown> }[] = []

View file

@ -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<boolean>
createBackendSessionForSend: (preview?: string | null) => Promise<string | null>
handleSkinCommand: (arg: string) => string
openMemoryGraph: () => void
refreshSessions: () => Promise<void>
requestGateway: <T>(method: string, params?: Record<string, unknown>, timeoutMs?: number) => Promise<T>
resumeStoredSession: (storedSessionId: string) => Promise<void> | 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,

View file

@ -54,6 +54,7 @@ interface SlashCommandDeps {
platform: string,
options?: { onProgress?: (state: string) => void; sessionId?: string }
) => Promise<{ ok: boolean; error?: string }>
openMemoryGraph: () => void
refreshSessions: () => Promise<void>
requestGateway: GatewayRequest
resumeStoredSession: (storedSessionId: string) => Promise<void> | 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,

View file

@ -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'

View file

@ -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)

View file

@ -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 },