web/src/lib/slashExec.ts and web/src/components/SlashPopover.tsx had zero
importers since the React composer was replaced by the PTY-embedded TUI
(f49afd3122) — exactly what web/AGENTS.md forbids, now orphaned. Their
parseSlash still carried the `(.*)` newline bug and lacked the `prefill`
variant. Desktop and the TUI each hand-rolled the same slash split and the
same command.dispatch narrowing; the multi-line fix (#41323, #55510) had to
be applied to each copy separately.
Sites:
web/src/lib/slashExec.ts::executeSlash/parseSlash/parseCommandDispatch -> deleted
web/src/components/SlashPopover.tsx::SlashPopover -> deleted
apps/desktop/src/lib/chat-runtime.ts::parseSlashCommand -> apps/shared/src/slash.ts::parseSlashCommand
apps/desktop/src/lib/chat-runtime.ts::parseCommandDispatch -> apps/shared/src/slash.ts::parseCommandDispatch
apps/desktop/src/lib/chat-runtime.ts::SLASH_COMMAND_RE -> apps/shared/src/slash.ts::SLASH_COMMAND_RE
apps/desktop/src/app/types.ts::*CommandDispatchResponse (5 interfaces) -> apps/shared/src/slash.ts
ui-tui/src/domain/slash.ts::parseSlashCommand/looksLikeSlashCommand -> apps/shared/src/slash.ts
ui-tui/src/lib/rpc.ts::asCommandDispatch -> apps/shared/src/slash.ts::parseCommandDispatch
ui-tui/src/gatewayTypes.ts::CommandDispatchResponse -> apps/shared/src/slash.ts
9 desktop importers + 3 TUI importers repointed.
Behavior change: desktop `parseSlashCommand` now lower-cases the command
name like the TUI, backend `resolve_command` and `slash.exec` already do
(`/Help` resolved before via the case-insensitive backend; local desktop
action lookups were case-sensitive). TUI's parsed result no longer carries
the redundant `cmd` echo (no consumer read it).
Tests: apps/shared/src/slash.test.ts (parseSlashCommand multi-line /
newline-boundary / degenerate cases; parseCommandDispatch every variant +
malformed rejection). Sabotage: restoring `(.*)` in SLASH_PARTS_RE fails
2 tests; restored -> 7 pass. Desktop chat-runtime.test.ts and TUI
asCommandDispatch.test.ts cases moved here; slashParity.test.ts repointed.
45 lines
1.6 KiB
TypeScript
45 lines
1.6 KiB
TypeScript
import { SLASH_COMMAND_RE } from '@hermes/shared'
|
|
|
|
/** Matches `agent/title_generator.py`'s MAX_DERIVED_TITLE_CHARS, so a draft
|
|
* doesn't visibly reflow the moment the backend's derived title replaces it. */
|
|
const MAX_DRAFT_TITLE_CHARS = 48
|
|
|
|
/**
|
|
* Name a draft after what the user has typed into it.
|
|
*
|
|
* The client-side twin of the backend's `derive_title`: first meaningful line,
|
|
* whitespace collapsed, cut on a word boundary. It runs before any session
|
|
* exists, so it can't reach the real titler — a draft has no persisted row and
|
|
* no opening message yet, which is exactly what `apply_instant_title` needs.
|
|
*
|
|
* Empty when there's nothing worth naming, so the caller keeps "New session"
|
|
* rather than showing a title that says less than the placeholder.
|
|
*/
|
|
export function deriveDraftTitle(text: string): string {
|
|
const line =
|
|
text
|
|
.split('\n')
|
|
.find(candidate => candidate.trim())
|
|
?.trim() ?? ''
|
|
|
|
if (!line) {
|
|
return ''
|
|
}
|
|
|
|
// A bare `/skin` names the draft after the command rather than the work, the
|
|
// failure the backend titler summarizes away. Title from the argument instead;
|
|
// with no argument there is no intent yet, so the placeholder stands.
|
|
const body = (SLASH_COMMAND_RE.test(line) ? line.replace(/^\/\S+\s*/, '') : line).split(/\s+/).join(' ')
|
|
|
|
if (body.length <= MAX_DRAFT_TITLE_CHARS) {
|
|
return body
|
|
}
|
|
|
|
// Cut on a word boundary, unless that would throw away more than half of it.
|
|
const cut = body.slice(0, MAX_DRAFT_TITLE_CHARS)
|
|
const space = cut.lastIndexOf(' ')
|
|
const kept = space > MAX_DRAFT_TITLE_CHARS / 2 ? cut.slice(0, space) : cut
|
|
|
|
return `${kept.replace(/[\s,.;:—-]+$/, '')}…`
|
|
}
|