From 03c638e4e00928abf29341f710fc35c9a7d1768e Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:09:05 +0800 Subject: [PATCH] Unify keyboard intent in original Agent conversation replies Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../attached-agent-conversations.css | 8 +- .../attached-agent-conversations.tsx | 35 ++++++-- .../codex-attached.mjs | 84 +++++++++++++++++- .../attached-composer/after-desktop.png | Bin 0 -> 80401 bytes .../attached-composer/after-narrow.png | Bin 0 -> 32503 bytes .../attached-composer/before-desktop.png | Bin 0 -> 79879 bytes .../attached-composer/before-narrow.png | Bin 0 -> 31931 bytes .../busy-recovery-narrow.png | Bin 0 -> 45300 bytes .../attached-composer/help-desktop.png | Bin 0 -> 106221 bytes .../attached-composer/help-english-narrow.png | Bin 0 -> 48508 bytes .../attached-composer/help-narrow.png | Bin 0 -> 46768 bytes .../attached-composer/long-mixed-desktop.png | Bin 0 -> 152129 bytes .../attached-composer/long-mixed-narrow.png | Bin 0 -> 37788 bytes 13 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 examples/personal-workspace-browser/evidence/attached-composer/after-desktop.png create mode 100644 examples/personal-workspace-browser/evidence/attached-composer/after-narrow.png create mode 100644 examples/personal-workspace-browser/evidence/attached-composer/before-desktop.png create mode 100644 examples/personal-workspace-browser/evidence/attached-composer/before-narrow.png create mode 100644 examples/personal-workspace-browser/evidence/attached-composer/busy-recovery-narrow.png create mode 100644 examples/personal-workspace-browser/evidence/attached-composer/help-desktop.png create mode 100644 examples/personal-workspace-browser/evidence/attached-composer/help-english-narrow.png create mode 100644 examples/personal-workspace-browser/evidence/attached-composer/help-narrow.png create mode 100644 examples/personal-workspace-browser/evidence/attached-composer/long-mixed-desktop.png create mode 100644 examples/personal-workspace-browser/evidence/attached-composer/long-mixed-narrow.png diff --git a/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.css b/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.css index d94165c6bd..69f81040e2 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.css @@ -48,11 +48,13 @@ .agent-work-unavailable {padding-block: 24px; color: var(--pw-muted); line-height: var(--leading-prose);} .goal-agent-work-page .attached-conversation-content {flex: 1; display: flex; flex-direction: column; padding: 0 0 16px; overflow: auto;} .goal-agent-work-page .attached-conversation-content > p {margin-block: 8px;} -.goal-agent-work-page .attached-conversation-source {font-size: var(--text-caption); color: var(--pw-muted);} +.goal-agent-work-page .attached-conversation-source {display: flex; align-items: center; gap: 8px; font-size: var(--text-caption); color: var(--pw-muted);} .goal-agent-work-page .attached-conversation-messages {flex: 1; min-height: 160px; max-height: none; margin-block: 12px;} .goal-agent-work-page .attached-conversation-content > button {align-self: flex-start; min-height: 44px;} .goal-agent-work-page form {flex-shrink: 0; display: flex; gap: 12px; align-items: flex-end; border-top: 1px solid var(--pw-line); padding-top: 8px;} -.goal-agent-work-page form label {margin: 0; flex: 1; min-width: 0;} +.goal-agent-work-page form .attached-composer-input {display: grid; gap: 8px; flex: 1; min-width: 0;} +.attached-composer-label {display: flex; align-items: center; gap: 8px;} +.goal-agent-work-page form label {display: inline; margin: 0;} .goal-agent-work-page form textarea {min-height: 64px; max-height: 160px;} .goal-agent-work-page form button[type="submit"] {background: var(--pw-text); color: var(--pw-card); min-height: 44px;} @media (max-width: 640px) {.agent-work-navigation {width: 100%;} .agent-work-navigation select {flex: 1; min-width: 0;} .goal-agent-work-page .attached-conversation-messages {min-height: 120px;}} @@ -74,7 +76,7 @@ @media (prefers-reduced-motion: reduce) {.attached-activity-mark[data-active]::after {animation: none;}} @media (max-width: 640px) { .goal-agent-work-page form {flex-wrap: wrap;} - .goal-agent-work-page form label {flex-basis: 100%;} + .goal-agent-work-page form .attached-composer-input {flex-basis: 100%;} .goal-agent-work-page form button[type="submit"] {margin-left: auto;} } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.tsx b/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.tsx index 811e075b07..21b3c0d726 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.tsx @@ -1,5 +1,8 @@ -import {useEffect, useLayoutEffect, useRef, useState} from "react"; +import {useEffect, useId, useLayoutEffect, useRef, useState} from "react"; +import {Keyboard} from "lucide-react"; import {AttachedConversationTimeline, type ConversationRecord, type ContentPage} from "./attached-conversation-record"; +import {composerKeyAction} from "./composer-keyboard"; +import {ContextHelp} from "./context-help"; import "./attached-agent-conversations.css"; export type Binding = {agent_id: string; thread_id: string; binding_ref: string}; @@ -92,6 +95,9 @@ export function AttachedConversation({goalId, binding, zh, reconnect}: { const [draft, setDraft] = useState(""); const [submission, setSubmission] = useState(null); const [sending, setSending] = useState(false); + const sendingInput = useRef(false); + const composing = useRef(false); + const inputId = useId(); const [loadingOlder, setLoadingOlder] = useState(false); const [opening, setOpening] = useState(false); const [openRequested, setOpenRequested] = useState(false); @@ -341,11 +347,12 @@ export function AttachedConversation({goalId, binding, zh, reconnect}: { } async function send() { - if (sending || error || recoveryError || !snapshot?.can_reply || !draft.trim() || awaitingReceipt) return; + if (sendingInput.current || sending || error || recoveryError || !snapshot?.can_reply || !draft.trim() || awaitingReceipt) return; + sendingInput.current = true; const current = generation.current; const pending = {operation_id: crypto.randomUUID(), text: draft, status: "pending"}; try {sessionStorage.setItem(cacheKey, JSON.stringify(pending));} - catch {setError(zh ? "无法保存回复回执,未发送。" : "Cannot save reply recovery; nothing was sent."); return;} + catch {sendingInput.current = false; setError(zh ? "无法保存回复回执,未发送。" : "Cannot save reply recovery; nothing was sent."); return;} receiptGeneration.current++; setSubmission(pending); setSending(true); setSendNotice(""); try { @@ -372,7 +379,7 @@ export function AttachedConversation({goalId, binding, zh, reconnect}: { } else {setError(String(failure)); void readReceipt(pending, current);} } } - finally {if (current === generation.current) setSending(false);} + finally {sendingInput.current = false; if (current === generation.current) setSending(false);} } async function readReceipt(pending = submission, current = generation.current) { // Recover from existing evidence only. Never retry the native effect. @@ -403,7 +410,7 @@ export function AttachedConversation({goalId, binding, zh, reconnect}: { {recoveryError ?

{recoveryError}

: null} {sendNotice ?

{sendNotice}

: null} -

Codex App

{!snapshot ? (zh ? "正在连接或连接不可用" : "Connecting or unavailable") : snapshot.can_reply ? (zh ? "原会话可回复" : "Original conversation accepts replies") : snapshot.activity.state === "turn_open" ? (zh ? "原会话有未结束执行;可在原 App 中追加输入" : "The original conversation has an unfinished turn; add input in the original App") : (zh ? "历史可读;回复连接或执行状态尚未确认" : "History available; reply connection or execution state unconfirmed")}

+
Codex App

{zh ? "已保存消息与工具记录自动更新;实时内容来自原宿主。" : "Saved messages and tools update automatically; live content comes from the original host."}

{!snapshot ? (zh ? "正在连接或连接不可用" : "Connecting or unavailable") : snapshot.can_reply ? (zh ? "原会话可回复" : "Original conversation accepts replies") : snapshot.activity.state === "turn_open" ? (zh ? "原会话有未结束执行;可在原 App 中追加输入" : "The original conversation has an unfinished turn; add input in the original App") : (zh ? "历史可读;回复连接或执行状态尚未确认" : "History available; reply connection or execution state unconfirmed")}

{openRequested ?

{zh ? "已请求打开原会话,请在 Codex App 中查看。" : "Requested opening the original conversation; check Codex App."}

: null} {openError ?

{openError}

: null} @@ -423,7 +430,23 @@ export function AttachedConversation({goalId, binding, zh, reconnect}: { {connectionError} {liveUnavailable ?

{zh ? "实时观察暂不可用,已保存的消息与工具记录仍会自动更新。" : "Live observation is unavailable; saved messages and tools still update automatically."}

: null} {submission?.status ?

{submission.status === "delivered" ? (zh ? "已由原 App 接纳;等待 Agent 结果。" : "Accepted by the original App; awaiting the Agent result.") : (zh ? "投递尚未确认,请先在原 App 核对,不会自动重发。" : "Delivery unconfirmed; check the original App. No automatic resend.")} {submission.status === "pending" ? : null}{awaitingReceipt ? : null}

: null} - {!awaitingReceipt ?
{event.preventDefault(); void send();}}>