mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
Merge pull request #55455 from NousResearch/bb/desktop-split-composer
refactor(desktop): extract composer pure helpers into composer-utils
This commit is contained in:
commit
116acf3821
3 changed files with 115 additions and 57 deletions
40
apps/desktop/src/app/chat/composer/composer-utils.test.ts
Normal file
40
apps/desktop/src/app/chat/composer/composer-utils.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { Unstable_TriggerItem } from '@assistant-ui/core'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { pickPlaceholder, slashArgStage, slashChipKindForItem, slashCommandToken } from './composer-utils'
|
||||
|
||||
const item = (group: string): Unstable_TriggerItem =>
|
||||
({ id: 'x', type: 'slash', label: 'x', metadata: { group } }) as unknown as Unstable_TriggerItem
|
||||
|
||||
describe('slashArgStage', () => {
|
||||
it('is true only once the query is past the command name', () => {
|
||||
expect(slashArgStage('personality')).toBe(false)
|
||||
expect(slashArgStage('personality alice')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('slashCommandToken', () => {
|
||||
it('extracts the lowercased /command token', () => {
|
||||
expect(slashCommandToken('Personality alice')).toBe('/personality')
|
||||
expect(slashCommandToken('model')).toBe('/model')
|
||||
})
|
||||
|
||||
it('handles an empty query', () => {
|
||||
expect(slashCommandToken('')).toBe('/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('slashChipKindForItem', () => {
|
||||
it('maps completion groups to chip kinds', () => {
|
||||
expect(slashChipKindForItem(item('Skills'))).toBe('skill')
|
||||
expect(slashChipKindForItem(item('Themes'))).toBe('theme')
|
||||
expect(slashChipKindForItem(item('Commands'))).toBe('command')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pickPlaceholder', () => {
|
||||
it('returns a member of the pool', () => {
|
||||
const pool = ['a', 'b', 'c'] as const
|
||||
expect(pool).toContain(pickPlaceholder(pool))
|
||||
})
|
||||
})
|
||||
60
apps/desktop/src/app/chat/composer/composer-utils.ts
Normal file
60
apps/desktop/src/app/chat/composer/composer-utils.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import type { Unstable_TriggerItem } from '@assistant-ui/core'
|
||||
|
||||
import type { SlashChipKind } from '@/components/assistant-ui/directive-text'
|
||||
import type { ComposerAttachment } from '@/store/composer'
|
||||
import { setSessionPickerOpen } from '@/store/session'
|
||||
|
||||
export const COMPOSER_STACK_BREAKPOINT_PX = 320
|
||||
|
||||
// A single editor line is ~28px (--composer-input-min-height 1.625rem + 0.5rem
|
||||
// vertical padding). Anything taller means the text wrapped to a second line,
|
||||
// which is when the composer should expand to the stacked layout.
|
||||
export const COMPOSER_SINGLE_LINE_MAX_PX = 36
|
||||
|
||||
export const COMPOSER_FADE_BACKGROUND =
|
||||
'linear-gradient(to bottom, transparent, color-mix(in srgb, var(--dt-background) 10%, transparent))'
|
||||
|
||||
// Quiet period after the last keystroke before persisting the draft;
|
||||
// unmount/pagehide flushes bypass it.
|
||||
export const DRAFT_PERSIST_DEBOUNCE_MS = 400
|
||||
|
||||
export const pickPlaceholder = (pool: readonly string[]) => pool[Math.floor(Math.random() * pool.length)]
|
||||
|
||||
/** Completion items can carry an `action` (set in use-slash-completions) that
|
||||
* runs a side effect on pick instead of inserting a chip — e.g. the session
|
||||
* picker's "Browse all…" entry opens the overlay. Table-driven so new action
|
||||
* items are a registry row, not a composer branch. */
|
||||
export const COMPLETION_ACTIONS: Record<string, () => void> = {
|
||||
'session-picker': () => setSessionPickerOpen(true)
|
||||
}
|
||||
|
||||
/** Map a picked `/` completion to its pill accent. Driven by the completion
|
||||
* group set in use-slash-completions (Skills / Themes / Commands|Options). */
|
||||
export function slashChipKindForItem(item: Unstable_TriggerItem): SlashChipKind {
|
||||
const group = (item.metadata as { group?: unknown } | undefined)?.group
|
||||
|
||||
if (group === 'Skills') {
|
||||
return 'skill'
|
||||
}
|
||||
|
||||
if (group === 'Themes') {
|
||||
return 'theme'
|
||||
}
|
||||
|
||||
return 'command'
|
||||
}
|
||||
|
||||
/** A `/` query is at its arg stage once it's past the command name. */
|
||||
export const slashArgStage = (query: string) => query.includes(' ')
|
||||
|
||||
/** The `/command` token of a slash query (`personality x` → `/personality`). */
|
||||
export const slashCommandToken = (query: string) => `/${query.split(/\s+/, 1)[0]?.toLowerCase() ?? ''}`
|
||||
|
||||
export interface QueueEditState {
|
||||
attachments: ComposerAttachment[]
|
||||
draft: string
|
||||
entryId: string
|
||||
sessionKey: string
|
||||
}
|
||||
|
||||
export const cloneAttachments = (attachments: ComposerAttachment[]) => attachments.map(a => ({ ...a }))
|
||||
|
|
@ -13,7 +13,7 @@ import {
|
|||
useState
|
||||
} from 'react'
|
||||
|
||||
import { hermesDirectiveFormatter, type SlashChipKind } from '@/components/assistant-ui/directive-text'
|
||||
import { hermesDirectiveFormatter } from '@/components/assistant-ui/directive-text'
|
||||
import { composerFill, composerSurfaceGlass } from '@/components/chat/composer-dock'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query'
|
||||
|
|
@ -65,7 +65,7 @@ import { $previewStatusBySession } from '@/store/preview-status'
|
|||
import { listRepoBranches, requestStartWorkSession, startWorkInRepo, switchBranchInRepo } from '@/store/projects'
|
||||
import { $activeSessionAwaitingInput } from '@/store/prompts'
|
||||
import { toggleReview } from '@/store/review'
|
||||
import { $gatewayState, $messages, setSessionPickerOpen } from '@/store/session'
|
||||
import { $gatewayState, $messages } from '@/store/session'
|
||||
import { $threadScrolledUp } from '@/store/thread-scroll'
|
||||
import { $autoSpeakReplies, setAutoSpeakReplies } from '@/store/voice-prefs'
|
||||
import { isSecondaryWindow } from '@/store/windows'
|
||||
|
|
@ -74,6 +74,19 @@ import { useTheme } from '@/themes'
|
|||
import { extractDroppedFiles, HERMES_PATHS_MIME, partitionDroppedFiles } from '../hooks/use-composer-actions'
|
||||
|
||||
import { AttachmentList } from './attachments'
|
||||
import {
|
||||
cloneAttachments,
|
||||
COMPLETION_ACTIONS,
|
||||
COMPOSER_FADE_BACKGROUND,
|
||||
COMPOSER_SINGLE_LINE_MAX_PX,
|
||||
COMPOSER_STACK_BREAKPOINT_PX,
|
||||
DRAFT_PERSIST_DEBOUNCE_MS,
|
||||
pickPlaceholder,
|
||||
type QueueEditState,
|
||||
slashArgStage,
|
||||
slashChipKindForItem,
|
||||
slashCommandToken
|
||||
} from './composer-utils'
|
||||
import { ContextMenu } from './context-menu'
|
||||
import { ComposerControls } from './controls'
|
||||
import { COMPOSER_DROP_ACTIVE_CLASS, COMPOSER_DROP_FADE_CLASS } from './drop-affordance'
|
||||
|
|
@ -121,61 +134,6 @@ import type { ChatBarProps } from './types'
|
|||
import { UrlDialog } from './url-dialog'
|
||||
import { VoiceActivity, VoicePlaybackActivity } from './voice-activity'
|
||||
|
||||
const COMPOSER_STACK_BREAKPOINT_PX = 320
|
||||
|
||||
// A single editor line is ~28px (--composer-input-min-height 1.625rem + 0.5rem
|
||||
// vertical padding). Anything taller means the text wrapped to a second line,
|
||||
// which is when the composer should expand to the stacked layout.
|
||||
const COMPOSER_SINGLE_LINE_MAX_PX = 36
|
||||
|
||||
const COMPOSER_FADE_BACKGROUND =
|
||||
'linear-gradient(to bottom, transparent, color-mix(in srgb, var(--dt-background) 10%, transparent))'
|
||||
|
||||
const pickPlaceholder = (pool: readonly string[]) => pool[Math.floor(Math.random() * pool.length)]
|
||||
|
||||
/** Completion items can carry an `action` (set in use-slash-completions) that
|
||||
* runs a side effect on pick instead of inserting a chip — e.g. the session
|
||||
* picker's "Browse all…" entry opens the overlay. Table-driven so new action
|
||||
* items are a registry row, not a composer branch. */
|
||||
const COMPLETION_ACTIONS: Record<string, () => void> = {
|
||||
'session-picker': () => setSessionPickerOpen(true)
|
||||
}
|
||||
|
||||
/** Map a picked `/` completion to its pill accent. Driven by the completion
|
||||
* group set in use-slash-completions (Skills / Themes / Commands|Options). */
|
||||
function slashChipKindForItem(item: Unstable_TriggerItem): SlashChipKind {
|
||||
const group = (item.metadata as { group?: unknown } | undefined)?.group
|
||||
|
||||
if (group === 'Skills') {
|
||||
return 'skill'
|
||||
}
|
||||
|
||||
if (group === 'Themes') {
|
||||
return 'theme'
|
||||
}
|
||||
|
||||
return 'command'
|
||||
}
|
||||
|
||||
/** A `/` query is at its arg stage once it's past the command name. */
|
||||
const slashArgStage = (query: string) => query.includes(' ')
|
||||
|
||||
/** The `/command` token of a slash query (`personality x` → `/personality`). */
|
||||
const slashCommandToken = (query: string) => `/${query.split(/\s+/, 1)[0]?.toLowerCase() ?? ''}`
|
||||
|
||||
interface QueueEditState {
|
||||
attachments: ComposerAttachment[]
|
||||
draft: string
|
||||
entryId: string
|
||||
sessionKey: string
|
||||
}
|
||||
|
||||
const cloneAttachments = (attachments: ComposerAttachment[]) => attachments.map(a => ({ ...a }))
|
||||
|
||||
// Quiet period after the last keystroke before persisting the draft;
|
||||
// unmount/pagehide flushes bypass it.
|
||||
const DRAFT_PERSIST_DEBOUNCE_MS = 400
|
||||
|
||||
export function ChatBar({
|
||||
busy,
|
||||
cwd,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue