mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
635a966c3e
This folds together the desktop-side fixes needed before broader rollout. Session resume no longer deadlocks waiting on init, Mermaid and inline image output render inside chat, task and sub-agent state stay visible during execution, local build/release paths are safer, and Feishu/Telegram now expose lightweight mobile commands (/help, /status, /clear) without adding a new adapter-specific protocol. Constraint: Desktop releases must publish updater artifacts from non-draft GitHub releases Constraint: IM commands need short, phone-friendly responses and low operational complexity Rejected: Add a dedicated IM command API surface | re-used existing slash commands and session/task REST endpoints to keep adapters thin Rejected: Wait for task_update push events in WebUI | added low-risk polling because the current frontend ignores that event path Confidence: medium Scope-risk: broad Reversibility: clean Directive: Keep IM command replies terse and mobile-first, and merge local fallback slash commands when server-provided lists are partial Tested: cd desktop && bun x vitest run src/components/chat/MermaidRenderer.test.tsx src/components/markdown/MarkdownRenderer.test.tsx Tested: cd desktop && bun x vitest run src/components/chat/composerUtils.test.ts src/pages/ActiveSession.test.tsx src/stores/chatStore.test.ts Tested: cd desktop && bun run lint Tested: bun test src/server/__tests__/conversations.test.ts --test-name-pattern "SDK init arrives only after the first user turn" --timeout 60000 Tested: cd adapters && bun test common/ feishu/ telegram/ Tested: cd adapters && bunx tsc --noEmit Not-tested: Full GitHub Actions release run on all three desktop platforms Not-tested: Local DMG packaging end-to-end on Apple Silicon Not-tested: Real Feishu/Telegram device sessions against a live adapter process
150 lines
4.6 KiB
TypeScript
150 lines
4.6 KiB
TypeScript
export const FALLBACK_SLASH_COMMANDS = [
|
|
{ name: 'compact', description: 'Compact conversation context' },
|
|
{ name: 'clear', description: 'Clear conversation history' },
|
|
{ name: 'help', description: 'Show available commands' },
|
|
{ name: 'review', description: 'Review code changes' },
|
|
{ name: 'commit', description: 'Create a git commit' },
|
|
{ name: 'pr', description: 'Create a pull request' },
|
|
{ name: 'init', description: 'Initialize project CLAUDE.md' },
|
|
{ name: 'bug', description: 'Report a bug' },
|
|
{ name: 'config', description: 'Open configuration' },
|
|
{ name: 'cost', description: 'Show token usage and costs' },
|
|
{ name: 'doctor', description: 'Diagnose installation issues' },
|
|
{ name: 'login', description: 'Switch Anthropic accounts' },
|
|
{ name: 'logout', description: 'Sign out of current account' },
|
|
{ name: 'memory', description: 'Edit CLAUDE.md memory files' },
|
|
{ name: 'model', description: 'Switch AI model' },
|
|
{ name: 'permissions', description: 'View or manage tool permissions' },
|
|
{ name: 'status', description: 'Show project and session status' },
|
|
{ name: 'terminal-setup', description: 'Set up terminal integration' },
|
|
{ name: 'vim', description: 'Toggle vim editing mode' },
|
|
]
|
|
|
|
export type SlashCommandOption = {
|
|
name: string
|
|
description: string
|
|
}
|
|
|
|
export function mergeSlashCommands(
|
|
preferred: ReadonlyArray<SlashCommandOption>,
|
|
fallback: ReadonlyArray<SlashCommandOption> = FALLBACK_SLASH_COMMANDS,
|
|
): SlashCommandOption[] {
|
|
const merged = new Map<string, SlashCommandOption>()
|
|
|
|
for (const command of preferred) {
|
|
if (!command?.name) continue
|
|
merged.set(command.name, {
|
|
name: command.name,
|
|
description: command.description?.trim() || '',
|
|
})
|
|
}
|
|
|
|
for (const command of fallback) {
|
|
if (!command?.name) continue
|
|
const existing = merged.get(command.name)
|
|
if (existing) {
|
|
if (!existing.description && command.description) {
|
|
merged.set(command.name, {
|
|
...existing,
|
|
description: command.description,
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
merged.set(command.name, command)
|
|
}
|
|
|
|
return [...merged.values()]
|
|
}
|
|
|
|
export type SlashTrigger = {
|
|
slashPos: number
|
|
filter: string
|
|
}
|
|
|
|
export function findSlashTrigger(value: string, cursorPos: number): SlashTrigger | null {
|
|
const textBeforeCursor = value.slice(0, cursorPos)
|
|
let slashPos = -1
|
|
|
|
for (let i = textBeforeCursor.length - 1; i >= 0; i--) {
|
|
const ch = textBeforeCursor[i]!
|
|
if (ch === '/') {
|
|
if (i === 0 || /\s/.test(textBeforeCursor[i - 1]!)) {
|
|
slashPos = i
|
|
break
|
|
}
|
|
break
|
|
}
|
|
if (/\s/.test(ch)) {
|
|
break
|
|
}
|
|
}
|
|
|
|
if (slashPos < 0) return null
|
|
|
|
const filter = textBeforeCursor.slice(slashPos + 1)
|
|
if (/\s/.test(filter)) return null
|
|
|
|
return { slashPos, filter }
|
|
}
|
|
|
|
export function replaceSlashToken(
|
|
input: string,
|
|
cursorPos: number,
|
|
command: string,
|
|
options?: { trailingSpace?: boolean },
|
|
): { value: string; cursorPos: number } {
|
|
const trigger = findSlashTrigger(input, cursorPos)
|
|
if (!trigger) {
|
|
const prefix = input && !/\s$/.test(input) ? `${input} ` : input
|
|
const token = `/${command}`
|
|
const suffix = options?.trailingSpace !== false ? ' ' : ''
|
|
const value = `${prefix}${token}${suffix}`
|
|
return { value, cursorPos: value.length }
|
|
}
|
|
|
|
const before = input.slice(0, trigger.slashPos)
|
|
const after = input.slice(cursorPos)
|
|
const token = `/${command}`
|
|
const suffix = options?.trailingSpace !== false ? ' ' : ''
|
|
const value = `${before}${token}${suffix}${after}`
|
|
const nextCursorPos = before.length + token.length + suffix.length
|
|
return { value, cursorPos: nextCursorPos }
|
|
}
|
|
|
|
export type SlashToken = {
|
|
start: number
|
|
filter: string
|
|
}
|
|
|
|
export function findSlashToken(value: string, cursorPos: number): SlashToken | null {
|
|
const trigger = findSlashTrigger(value, cursorPos)
|
|
if (!trigger) return null
|
|
return { start: trigger.slashPos, filter: trigger.filter }
|
|
}
|
|
|
|
export function replaceSlashCommand(
|
|
value: string,
|
|
cursorPos: number,
|
|
command: string,
|
|
): { value: string; cursorPos: number } | null {
|
|
const trigger = findSlashTrigger(value, cursorPos)
|
|
if (!trigger) return null
|
|
|
|
return replaceSlashToken(value, cursorPos, command, { trailingSpace: true })
|
|
}
|
|
|
|
export function insertSlashTrigger(
|
|
value: string,
|
|
cursorPos: number,
|
|
): { value: string; cursorPos: number } {
|
|
const before = value.slice(0, cursorPos)
|
|
const after = value.slice(cursorPos)
|
|
const needsLeadingSpace = before.length > 0 && !/\s$/.test(before)
|
|
const token = `${needsLeadingSpace ? ' ' : ''}/`
|
|
return {
|
|
value: `${before}${token}${after}`,
|
|
cursorPos: before.length + token.length,
|
|
}
|
|
}
|