Fix plan command support in web bridge (#1018)

* fix: support plan command in web bridge

* fix: preserve queued bridge messages

* fix: avoid duplicate queued plan messages

* fix: preserve plan command semantics

---------

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
ekko
2026-05-25 15:48:17 +08:00
committed by GitHub
parent 6e2e502a75
commit 0eab6a1125
21 changed files with 622 additions and 49 deletions
@@ -28,6 +28,7 @@ const bridgeCommands = computed(() => [
{ name: 'status', args: '', description: t('chat.slashCommands.status') },
{ name: 'abort', args: '', description: t('chat.slashCommands.abort') },
{ name: 'queue', args: t('chat.slashCommandArgs.message'), description: t('chat.slashCommands.queue') },
{ name: 'plan', args: t('chat.slashCommandArgs.text'), description: t('chat.slashCommands.plan') },
{ name: 'clear', args: '', description: t('chat.slashCommands.clear') },
{ name: 'clear', args: '--history', insertText: 'clear --history', description: t('chat.slashCommands.clearHistory') },
{ name: 'title', args: t('chat.slashCommandArgs.title'), description: t('chat.slashCommands.title') },
@@ -1307,7 +1307,7 @@ async function handleSessionModelCustomSubmit() {
{{ t('chat.clarifyDismiss') }}
</NButton>
</div>
<div v-else class="clarify-actions">
<div v-else class="clarify-actions clarify-actions-open">
<div class="clarify-input-row">
<NInput
v-model:value="clarifyResponse"
@@ -2168,12 +2168,24 @@ async function handleSessionModelCustomSubmit() {
.clarify-input-row {
display: flex;
flex: 1;
width: 100%;
min-width: 0;
gap: 8px;
align-items: center;
.n-input {
flex: 1;
:deep(.n-input) {
flex: 1 1 auto;
min-width: 0;
}
:deep(.n-button) {
flex: 0 0 auto;
}
}
.clarify-actions-open {
display: flex;
width: 100%;
}
@media (max-width: 768px) {
.approval-bar {
@@ -2212,9 +2224,18 @@ async function handleSessionModelCustomSubmit() {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.clarify-actions-open {
display: flex;
grid-template-columns: none;
}
.clarify-actions :deep(.n-button) {
width: 100%;
}
.clarify-actions-open :deep(.n-button) {
width: auto;
}
}
@media (max-width: 420px) {
@@ -2233,6 +2254,15 @@ async function handleSessionModelCustomSubmit() {
.clarify-actions {
grid-template-columns: 1fr;
}
.clarify-input-row {
flex-direction: column;
align-items: stretch;
}
.clarify-actions-open :deep(.n-button) {
width: 100%;
}
}
@keyframes rainbow-glow {
+1
View File
@@ -218,6 +218,7 @@ export default {
status: 'Sitzungsstatus und Warteschlange anzeigen',
abort: 'Aktiven Bridge-Lauf stoppen',
queue: 'Nachricht hinter dem aktiven Lauf einreihen',
plan: 'Markdown-Implementierungsplan schreiben',
clear: 'Aktuelle Anzeige leeren',
clearHistory: 'Gespeicherten Nachrichtenverlauf dieser Sitzung löschen',
title: 'Diese Sitzung umbenennen',
+1
View File
@@ -219,6 +219,7 @@ export default {
status: 'Show session status and queue',
abort: 'Stop the active bridge run',
queue: 'Queue a message behind the active run',
plan: 'Write a markdown implementation plan',
clear: 'Clear the current display',
clearHistory: 'Delete this sessions stored message history',
title: 'Rename this session',
+1
View File
@@ -218,6 +218,7 @@ export default {
status: 'Mostrar estado de sesión y cola',
abort: 'Detener la ejecución activa de Bridge',
queue: 'Poner un mensaje en cola tras la ejecución activa',
plan: 'Escribir un plan de implementación en Markdown',
clear: 'Limpiar la vista actual',
clearHistory: 'Eliminar el historial de mensajes guardado de esta sesión',
title: 'Renombrar esta sesión',
+1
View File
@@ -218,6 +218,7 @@ export default {
status: 'Afficher l’état de la session et la file',
abort: 'Arrêter lexécution Bridge active',
queue: 'Mettre un message en file après lexécution active',
plan: 'Rédiger un plan dimplémentation Markdown',
clear: 'Effacer laffichage actuel',
clearHistory: 'Supprimer lhistorique des messages enregistrés de cette session',
title: 'Renommer cette session',
+1
View File
@@ -218,6 +218,7 @@ export default {
status: 'セッション状態とキューを表示',
abort: '実行中の Bridge を停止',
queue: '実行中の処理の後ろにメッセージをキュー追加',
plan: 'Markdown の実装計画を作成',
clear: '現在の表示をクリア',
clearHistory: 'このセッションの保存済みメッセージ履歴を削除',
title: 'このセッション名を変更',
+1
View File
@@ -218,6 +218,7 @@ export default {
status: '세션 상태와 대기열 표시',
abort: '활성 Bridge 실행 중지',
queue: '활성 실행 뒤에 메시지 대기열 추가',
plan: 'Markdown 구현 계획 작성',
clear: '현재 표시 내용 지우기',
clearHistory: '이 세션의 저장된 메시지 기록 삭제',
title: '이 세션 이름 변경',
+1
View File
@@ -218,6 +218,7 @@ export default {
status: 'Mostrar status da sessão e fila',
abort: 'Parar a execução ativa do Bridge',
queue: 'Enfileirar uma mensagem após a execução ativa',
plan: 'Escrever um plano de implementação em Markdown',
clear: 'Limpar a visualização atual',
clearHistory: 'Excluir o histórico de mensagens salvo desta sessão',
title: 'Renomear esta sessão',
@@ -218,6 +218,7 @@ export default {
status: '查看會話狀態和佇列',
abort: '停止目前 Bridge 執行',
queue: '將訊息加入目前執行後的佇列',
plan: '產生一份 Markdown 實作計畫',
clear: '清空目前顯示內容',
clearHistory: '刪除目前會話已儲存的訊息歷史',
title: '重新命名目前會話',
+1
View File
@@ -219,6 +219,7 @@ export default {
status: '查看会话状态和队列',
abort: '停止当前 Bridge 运行',
queue: '把消息加入当前运行后的队列',
plan: '生成一份 Markdown 实施计划',
clear: '清空当前显示内容',
clearHistory: '删除当前会话已入库的消息历史',
title: '重命名当前会话',
+29 -5
View File
@@ -967,12 +967,14 @@ export const useChatStore = defineStore('chat', () => {
const timestamp = typeof peer?.timestamp === 'number' && Number.isFinite(peer.timestamp)
? Math.round(peer.timestamp * 1000)
: Date.now()
const role = peer?.role === 'command' ? 'command' : 'user'
return [{
id: messageId,
role: 'user' as const,
role,
content,
timestamp,
queued: true,
systemType: role === 'command' ? 'command' as const : undefined,
}]
})
}
@@ -1028,11 +1030,12 @@ export const useChatStore = defineStore('chat', () => {
enqueueUserMessage(sessionId, {
...(existing || {}),
id: messageId,
role: 'user',
role: peer?.role === 'command' ? 'command' : 'user',
content,
timestamp: existing?.timestamp || timestamp,
attachments: existing?.attachments,
queued: true,
systemType: peer?.role === 'command' ? 'command' : existing?.systemType,
})
}
@@ -1093,6 +1096,22 @@ export const useChatStore = defineStore('chat', () => {
pendingClarifies.value = new Map(pendingClarifies.value)
}
function clearPendingInteractions(sessionId: string) {
let changed = false
if (pendingApprovals.value.has(sessionId)) {
pendingApprovals.value.delete(sessionId)
changed = true
}
if (pendingClarifies.value.has(sessionId)) {
pendingClarifies.value.delete(sessionId)
changed = true
}
if (changed) {
pendingApprovals.value = new Map(pendingApprovals.value)
pendingClarifies.value = new Map(pendingClarifies.value)
}
}
function respondToClarify(response: string) {
const pending = activePendingClarify.value
if (!pending) return
@@ -1169,8 +1188,9 @@ export const useChatStore = defineStore('chat', () => {
: false
const isBridgeSlashCommand = content.trim().startsWith('/')
const isBridgeCompressCommand = isBridgeSlashCommand && /^\/compress(?:\s|$)/i.test(content.trim())
const isBridgePlanCommand = isBridgeSlashCommand && /^\/plan(?:\s|$)/i.test(content.trim())
const wasLiveBeforeSend = isSessionLive(sid)
const shouldQueue = wasLiveBeforeSend && !isBridgeSlashCommand
const shouldQueue = wasLiveBeforeSend && (!isBridgeSlashCommand || isBridgePlanCommand)
const userMsg: Message = {
id: uid(),
@@ -1449,6 +1469,7 @@ export const useChatStore = defineStore('chat', () => {
case 'abort.completed': {
setAbortState({ aborting: false, synced: (evt as any).synced ?? false })
clearPendingInteractions(sid)
if ((evt as any).queue_length > 0) {
queueLengths.value.set(sid, (evt as any).queue_length)
setAbortState(null)
@@ -1798,7 +1819,7 @@ export const useChatStore = defineStore('chat', () => {
{ onReconnectResume: applyReconnectResume },
)
if (!isBridgeSlashCommand || isBridgeCompressCommand) {
if (!isBridgeSlashCommand || isBridgeCompressCommand || isBridgePlanCommand) {
streamStates.value.set(sid, ctrl)
}
} catch (err: any) {
@@ -1920,6 +1941,7 @@ export const useChatStore = defineStore('chat', () => {
case 'abort.completed': {
setAbortState({ aborting: false, synced: (evt as any).synced ?? false })
clearPendingInteractions(sid)
if ((evt as any).queue_length > 0) {
queueLengths.value.set(sid, (evt as any).queue_length)
setAbortState(null)
@@ -2286,10 +2308,11 @@ export const useChatStore = defineStore('chat', () => {
const message: Message = {
id: messageId || uid(),
role: 'user',
role: peer?.role === 'command' ? 'command' : 'user',
content,
timestamp,
queued: !!peer?.queued,
systemType: peer?.role === 'command' ? 'command' : undefined,
}
if (peer?.queued) {
enqueueUserMessage(sid, message)
@@ -2307,6 +2330,7 @@ export const useChatStore = defineStore('chat', () => {
const sid = activeSessionId.value
if (!sid) return
if (isAborting.value) return
clearPendingInteractions(sid)
const ctrl = streamStates.value.get(sid)
if (ctrl) {
setAbortState({ aborting: true, synced: null })