feat(hub,web): support scheduling messages for future delivery (#590)

This commit is contained in:
Junmo Kim
2026-05-18 09:09:17 +08:00
committed by GitHub
parent 2e96992dd4
commit b2a30c2e39
33 changed files with 3082 additions and 153 deletions
+16 -3
View File
@@ -1,6 +1,9 @@
import { useCallback, useMemo } from 'react'
import type React from 'react'
import type { AppendMessage, AttachmentAdapter, ThreadMessageLike } from '@assistant-ui/react'
import { useExternalMessageConverter, useExternalStoreRuntime } from '@assistant-ui/react'
import type { PendingSchedule } from '@/components/AssistantChat/ScheduleTimePicker'
import { resolvePendingSchedule } from '@/components/AssistantChat/ScheduleTimePicker'
import { safeStringify } from '@hapi/protocol'
import { renderEventLabel } from '@/chat/presentation'
import type { ChatBlock, CliOutputBlock, CodexReview, UsageData } from '@/chat/types'
@@ -298,10 +301,11 @@ export function useHappyRuntime(props: {
blocks: readonly VisibleChatBlock[]
isSending: boolean
isRunning?: boolean
onSendMessage: (text: string, attachments?: AttachmentMetadata[]) => void
onSendMessage: (text: string, attachments?: AttachmentMetadata[], scheduledAt?: number | null) => void
onAbort: () => Promise<void>
attachmentAdapter?: AttachmentAdapter
allowSendWhenInactive?: boolean
pendingScheduleRef?: React.RefObject<PendingSchedule | null>
}) {
const isRunning = props.isRunning ?? props.session.thinking
@@ -316,8 +320,13 @@ export function useHappyRuntime(props: {
const onNew = useCallback(async (message: AppendMessage) => {
const { text, attachments } = extractMessageContent(message)
if (!text && attachments.length === 0) return
props.onSendMessage(text, attachments.length > 0 ? attachments : undefined)
}, [props.onSendMessage])
// Resolve pendingSchedule at send time (Date.now()) so preset-type schedules
// ("5 minutes from now") are relative to the actual send action, not the
// moment the user clicked the preset button.
const sendNow = Date.now()
const scheduledAt = resolvePendingSchedule(props.pendingScheduleRef?.current ?? null, sendNow)
props.onSendMessage(text, attachments.length > 0 ? attachments : undefined, scheduledAt)
}, [props.onSendMessage, props.pendingScheduleRef])
const onCancel = useCallback(async () => {
await props.onAbort()
@@ -344,5 +353,9 @@ export function useHappyRuntime(props: {
props.attachmentAdapter
])
// Note: pendingScheduleRef is intentionally not in the deps above.
// The ref is read at send time inside onNew (not at render time), so changes
// to pendingSchedule do not need to invalidate the adapter or re-run onNew.
return useExternalStoreRuntime(adapter)
}
+8
View File
@@ -319,6 +319,14 @@ export default {
'composer.send': 'Send',
'composer.stop': 'Stop',
'composer.voice': 'Voice assistant',
'composer.scheduleSend': 'Schedule send',
'composer.scheduleRelativeTab': 'Relative',
'composer.scheduleSpecificTab': 'Specific',
'composer.scheduleSpecificHint': 'Max 7 days. Requires the hub running; the CLI catches up the next time it connects after that time.',
'composer.scheduleErrorPast': 'Scheduled time must be in the future.',
'composer.scheduleErrorTooFar': 'Maximum schedule time is 7 days.',
'queuedMessages.scheduledFor': 'Scheduled for {time}',
'queuedMessages.editAlreadyInvoked': "Message already sent — it can't be edited",
'composer.codexSlashUnsupported.title': 'Codex command unavailable',
'composer.codexSlashUnsupported.body': 'HAPI remote mode does not yet run built-in Codex slash commands like {command}. Use natural language instead, or run it in the local Codex TUI.',
+8
View File
@@ -321,6 +321,14 @@ export default {
'composer.send': '发送',
'composer.stop': '停止',
'composer.voice': '语音助手',
'composer.scheduleSend': '定时发送',
'composer.scheduleRelativeTab': '相对时间',
'composer.scheduleSpecificTab': '指定时间',
'composer.scheduleSpecificHint': '最多 7 天。需 Hub 运行;CLI 下次连接时会接收消息。',
'composer.scheduleErrorPast': '发送时间必须在未来。',
'composer.scheduleErrorTooFar': '最多只能定时 7 天。',
'queuedMessages.scheduledFor': '定时发送: {time}',
'queuedMessages.editAlreadyInvoked': '消息已发送,无法编辑',
'composer.codexSlashUnsupported.title': '无法执行 Codex 命令',
'composer.codexSlashUnsupported.body': 'HAPI 远程模式暂不支持 {command} 这类 Codex 内建 slash command,请改用自然语言,或在本地 Codex TUI 中执行。',