fix(office-ui): native progress parity, ui_state lock hardening, approval-card idempotency

Native agent progress panel (company mode):
- ws_handler: filter runtime bookkeeping noise (turn/status/member_inbox_updated),
  keep tool_completed as tool_call, thinking summary previews content,
  preserve raw thinking_delta fragments (no strip; skip whitespace-only)
- frontend progressLog: summarize thinking by content preview; merge thinking
  by detail only so the 'Thinking' label never splices into text
- AgentProgressBlock: add bottom "Show more (N earlier steps)" toggle

ui_state.db "database is locked" hardening:
- ws_handler: isolate engine progress/kanban/runtime-event callbacks so UI
  persistence failures never crash work items
- chat_store: busy_timeout, _retry_locked backoff, idempotent insert_message
  (INSERT OR REPLACE), create_channel read-before-write to stop poll writes
- server: flock single-instance guard for `opc ui` per OPC home

Approval card duplicate-click bug:
- EscalationPanel: disable buttons on click with Submitting state and 30s
  reconnect fallback
- ws_handler: stale-escalation branch checks real card status (new
  chat_store.get_checkpoint_message); already-resolved cards get an accurate
  "already handled (decision: X)" reply without being re-marked stale;
  dedup identical helper messages within 120s to stop reply spam

Company mode prompt:
- add soft guidance that the runtime monitors state and re-activates roles,
  so leaders need not poll work items after delegation/review

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
LZH-YS1998
2026-07-07 16:18:01 +08:00
parent ee79331d48
commit 12817a4e60
12 changed files with 516 additions and 157 deletions
@@ -1,7 +1,11 @@
import React, { useCallback, useMemo } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { ChatMessageMeta, HumanEscalationOption } from '../types/chat'
import { MarkdownBody } from './MarkdownBody'
// If the server never confirms (click lost on a dropped connection), re-enable
// the buttons after this long so the user can retry.
const SUBMIT_CONFIRM_TIMEOUT_MS = 30000
interface EscalationPanelProps {
meta: ChatMessageMeta
onReply: (text: string) => void
@@ -57,10 +61,20 @@ export const EscalationPanel = React.memo(function EscalationPanel({
const worktreePath = String(meta.worktree_path ?? '').trim()
const hasRuntimeState = activeSubagents.length > 0 || permissionRequests.length > 0 || !!worktreePath
const [submittedOptionId, setSubmittedOptionId] = useState('')
const isSubmitting = !!submittedOptionId && !isResponded
useEffect(() => {
if (!isSubmitting) return
const timer = window.setTimeout(() => setSubmittedOptionId(''), SUBMIT_CONFIRM_TIMEOUT_MS)
return () => window.clearTimeout(timer)
}, [isSubmitting, submittedOptionId])
const handleReply = useCallback((option: HumanEscalationOption) => {
if (isResponded) return
if (isResponded || isSubmitting) return
setSubmittedOptionId(option.id)
onReply(option.label || option.id)
}, [isResponded, onReply])
}, [isResponded, isSubmitting, onReply])
return (
<div className="ckpt-panel ckpt-escalation">
@@ -109,15 +123,22 @@ export const EscalationPanel = React.memo(function EscalationPanel({
{options.map((option) => (
<button
key={option.id}
className={`ckpt-btn ${option.id.includes('deny') ? 'ckpt-btn-deny' : 'ckpt-btn-approve'}`}
className={`ckpt-btn ${option.id.includes('deny') ? 'ckpt-btn-deny' : 'ckpt-btn-approve'}${isSubmitting ? ' ckpt-btn-submitting' : ''}`}
onClick={() => handleReply(option)}
disabled={isSubmitting}
>
{option.label || option.id}
{isSubmitting && submittedOptionId === option.id ? 'Submitting…' : (option.label || option.id)}
</button>
))}
</div>
)}
{isSubmitting && (
<div className="ckpt-escalation-hint">
Decision sent waiting for server confirmation
</div>
)}
{!isResponded && meta.default_action && (
<div className="ckpt-escalation-hint">
Default on timeout: <code>{meta.default_action}</code>