mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-08-26 00:31:54 +00:00
fix(v2): keep queued prompts and forms stable
Render pending inbox prompts in their existing message bubbles with queue, steer, edit, and remove actions instead of maintaining a separate queue panel. Keep duplicate form sync results from replacing the active form object, preventing local field state from resetting and avoiding the resulting virtual timeline height correction. Validated with UI and server typechecks, focused form, projection, shortcut, bottom-pin, and virtual-follow tests, plus the production UI build.
This commit is contained in:
parent
36be70ac38
commit
dde642bc1b
10 changed files with 136 additions and 197 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { For, Index, Match, Show, Suspense, Switch, createEffect, createMemo, createSignal, lazy, onCleanup, untrack, type Accessor } from "solid-js"
|
||||
import { Copy, ExternalLink, FoldVertical, Loader2, Volume2, XCircle } from "lucide-solid"
|
||||
import MessageItem from "./message-item"
|
||||
import type { SessionInboxUser } from "@opencode-ai/client"
|
||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||
import type { ClientPart, Message, MessageInfo, TextPart } from "../types/message"
|
||||
import { isHiddenSyntheticTextPart, partHasRenderableText } from "../types/message"
|
||||
|
|
@ -279,6 +280,11 @@ interface MessageContentItemProps {
|
|||
messageIndex: number
|
||||
onRevert?: (messageId: string) => void
|
||||
onFork?: (messageId?: string) => void
|
||||
pendingPrompt?: SessionInboxUser
|
||||
pendingPromptBusy?: boolean
|
||||
onPendingPromptDeliveryChange?: (item: SessionInboxUser) => void
|
||||
onPendingPromptEdit?: (item: SessionInboxUser) => void
|
||||
onPendingPromptRemove?: (item: SessionInboxUser) => void
|
||||
onContentRendered?: () => void
|
||||
}
|
||||
|
||||
|
|
@ -367,6 +373,11 @@ function MessageContentItem(props: MessageContentItemProps) {
|
|||
showAgentMeta={showAgentMeta()}
|
||||
onRevert={props.onRevert}
|
||||
onFork={props.onFork}
|
||||
pendingPrompt={props.pendingPrompt}
|
||||
pendingPromptBusy={props.pendingPromptBusy}
|
||||
onPendingPromptDeliveryChange={props.onPendingPromptDeliveryChange}
|
||||
onPendingPromptEdit={props.onPendingPromptEdit}
|
||||
onPendingPromptRemove={props.onPendingPromptRemove}
|
||||
onContentRendered={props.onContentRendered}
|
||||
/>
|
||||
</Show>
|
||||
|
|
@ -506,6 +517,11 @@ interface MessageBlockProps {
|
|||
toolVisibility: (toolName: string) => VisibilityPreference
|
||||
onRevert?: (messageId: string) => void
|
||||
onFork?: (messageId?: string) => void
|
||||
pendingPrompt?: SessionInboxUser
|
||||
pendingPromptBusy?: boolean
|
||||
onPendingPromptDeliveryChange?: (item: SessionInboxUser) => void
|
||||
onPendingPromptEdit?: (item: SessionInboxUser) => void
|
||||
onPendingPromptRemove?: (item: SessionInboxUser) => void
|
||||
onContentRendered?: () => void
|
||||
searchQuery?: Accessor<string>
|
||||
searchResultMessageIds?: Accessor<Set<string>>
|
||||
|
|
@ -768,6 +784,11 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||
messageIndex={props.messageIndex}
|
||||
onRevert={props.onRevert}
|
||||
onFork={props.onFork}
|
||||
pendingPrompt={props.pendingPrompt}
|
||||
pendingPromptBusy={props.pendingPromptBusy}
|
||||
onPendingPromptDeliveryChange={props.onPendingPromptDeliveryChange}
|
||||
onPendingPromptEdit={props.onPendingPromptEdit}
|
||||
onPendingPromptRemove={props.onPendingPromptRemove}
|
||||
onContentRendered={props.onContentRendered}
|
||||
/>
|
||||
</Match>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { For, Show, createEffect, createSignal, onCleanup } from "solid-js"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { Copy, Split, Undo, Volume2 } from "lucide-solid"
|
||||
import { Copy, Pencil, Play, Split, Trash2, Undo, Volume2 } from "lucide-solid"
|
||||
import type { SessionInboxUser } from "@opencode-ai/client"
|
||||
import type { MessageInfo, ClientPart } from "../types/message"
|
||||
import { isHiddenSyntheticTextPart, partHasRenderableText } from "../types/message"
|
||||
import type { MessageRecord } from "../stores/message-v2/types"
|
||||
|
|
@ -22,6 +23,11 @@ interface MessageItemProps {
|
|||
parts: ClientPart[]
|
||||
onRevert?: (messageId: string) => void
|
||||
onFork?: (messageId?: string) => void
|
||||
pendingPrompt?: SessionInboxUser
|
||||
pendingPromptBusy?: boolean
|
||||
onPendingPromptDeliveryChange?: (item: SessionInboxUser) => void
|
||||
onPendingPromptEdit?: (item: SessionInboxUser) => void
|
||||
onPendingPromptRemove?: (item: SessionInboxUser) => void
|
||||
showAgentMeta?: boolean
|
||||
contentStartPartId?: string
|
||||
onContentRendered?: () => void
|
||||
|
|
@ -388,9 +394,35 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
onSelect: () => void speech.toggle(),
|
||||
})
|
||||
}
|
||||
|
||||
const pending = props.pendingPrompt
|
||||
if (pending) {
|
||||
items.push(
|
||||
{
|
||||
key: "pending-delivery",
|
||||
label: t(`promptQueue.actions.${pending.delivery === "queue" ? "steer" : "queue"}`),
|
||||
icon: pending.delivery === "queue"
|
||||
? <Play class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
: <Undo class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => props.onPendingPromptDeliveryChange?.(pending),
|
||||
},
|
||||
{
|
||||
key: "pending-edit",
|
||||
label: t("promptQueue.actions.edit"),
|
||||
icon: <Pencil class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => props.onPendingPromptEdit?.(pending),
|
||||
},
|
||||
{
|
||||
key: "pending-remove",
|
||||
label: t("promptQueue.actions.remove"),
|
||||
icon: <Trash2 class="w-3.5 h-3.5" aria-hidden="true" />,
|
||||
onSelect: () => props.onPendingPromptRemove?.(pending),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isUser() && props.onFork) {
|
||||
if (isUser() && !props.pendingPrompt && props.onFork) {
|
||||
items.push({
|
||||
key: "fork",
|
||||
label: t("messageItem.actions.fork"),
|
||||
|
|
@ -399,7 +431,7 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
})
|
||||
}
|
||||
|
||||
if (isUser() && props.onRevert) {
|
||||
if (isUser() && !props.pendingPrompt && props.onRevert) {
|
||||
items.push({
|
||||
key: "revert",
|
||||
label: t("messageItem.actions.revertTitle"),
|
||||
|
|
@ -474,6 +506,42 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={props.pendingPrompt} keyed>
|
||||
{(pending) => (
|
||||
<>
|
||||
<button
|
||||
class="message-action-button"
|
||||
disabled={props.pendingPromptBusy}
|
||||
onClick={() => props.onPendingPromptDeliveryChange?.(pending)}
|
||||
title={t(`promptQueue.actions.${pending.delivery === "queue" ? "steer" : "queue"}`)}
|
||||
aria-label={t(`promptQueue.actions.${pending.delivery === "queue" ? "steer" : "queue"}`)}
|
||||
>
|
||||
<Show when={pending.delivery === "queue"} fallback={<Undo class="w-3.5 h-3.5" aria-hidden="true" />}>
|
||||
<Play class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</Show>
|
||||
</button>
|
||||
<button
|
||||
class="message-action-button"
|
||||
disabled={props.pendingPromptBusy}
|
||||
onClick={() => props.onPendingPromptEdit?.(pending)}
|
||||
title={t("promptQueue.actions.edit")}
|
||||
aria-label={t("promptQueue.actions.edit")}
|
||||
>
|
||||
<Pencil class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="message-action-button"
|
||||
disabled={props.pendingPromptBusy}
|
||||
onClick={() => props.onPendingPromptRemove?.(pending)}
|
||||
title={t("promptQueue.actions.remove")}
|
||||
aria-label={t("promptQueue.actions.remove")}
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
<ActionOverflowMenu
|
||||
items={actionMenuItems()}
|
||||
|
|
@ -536,6 +604,11 @@ export default function MessageItem(props: MessageItemProps) {
|
|||
|
||||
<div class="pt-0 whitespace-pre-wrap break-words leading-[1.1]" dir="auto">
|
||||
|
||||
<Show when={props.pendingPrompt} keyed>
|
||||
{(pending) => (
|
||||
<div class="message-queued-badge">{t(`promptQueue.delivery.${pending.delivery}`)}</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={errorMessage()}>
|
||||
<div class="message-error-block" dir="auto">⚠️ {errorMessage()}</div>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { isLatestWindow, toWindowSnapshot } from "../stores/message-v2/message-w
|
|||
import { getLogger } from "../lib/logger"
|
||||
import { beginMessageHistoryTraversal, invalidateMessageHistoryTraversal } from "../stores/session-api"
|
||||
import { getOpenCodeInstanceGeneration, getOpenCodeMutationRevision } from "../stores/opencode-data"
|
||||
import type { SessionInboxUser } from "@opencode-ai/client"
|
||||
|
||||
const MESSAGE_SCROLL_CACHE_SCOPE = "message-stream"
|
||||
const QUOTE_SELECTION_MAX_LENGTH = 2000
|
||||
|
|
@ -59,7 +60,11 @@ export interface MessageSectionProps {
|
|||
sessionStreamingActive?: boolean
|
||||
explicitBottomPinIntent?: VirtualExplicitBottomPinIntent | null
|
||||
onExplicitBottomPinCancelled?: () => void
|
||||
queuedMessageIds?: ReadonlySet<string>
|
||||
pendingPrompts?: ReadonlyMap<string, SessionInboxUser>
|
||||
pendingPromptBusy?: boolean
|
||||
onPendingPromptDeliveryChange?: (item: SessionInboxUser) => void
|
||||
onPendingPromptEdit?: (item: SessionInboxUser) => void
|
||||
onPendingPromptRemove?: (item: SessionInboxUser) => void
|
||||
}
|
||||
|
||||
export default function MessageSection(props: MessageSectionProps) {
|
||||
|
|
@ -76,7 +81,6 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
const visibleMessageIds = createMemo(() => {
|
||||
const resolvedStore = store()
|
||||
return messageIds().filter((messageId) => {
|
||||
if (props.queuedMessageIds?.has(messageId)) return false
|
||||
const record = resolvedStore.getMessage(messageId)
|
||||
if (!record) return false
|
||||
|
||||
|
|
@ -792,8 +796,7 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
isLatest: () => isLatestWindow(store().getMessageWindow(sessionId)),
|
||||
loadOldest: props.onLoadOldestMessages ?? (() => Promise.resolve()),
|
||||
loadNewer: props.onLoadNewerMessages ?? (() => Promise.resolve()),
|
||||
visit: () => buildSessionSearchMatches({ store: store(), sessionId, query, includeThinking })
|
||||
.filter((match) => !props.queuedMessageIds?.has(match.messageId)),
|
||||
visit: () => buildSessionSearchMatches({ store: store(), sessionId, query, includeThinking }),
|
||||
}).then((matches) => {
|
||||
if (!matches) {
|
||||
if (isCurrentSearch()) setIsSearchPending(false)
|
||||
|
|
@ -829,7 +832,6 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
const includeThinking = Boolean(preferences().showThinkingBlocks)
|
||||
const currentResidentIds = messageIds()
|
||||
const currentMatches = buildSessionSearchMatches({ store: store(), sessionId: props.sessionId, query, includeThinking })
|
||||
.filter((match) => !props.queuedMessageIds?.has(match.messageId))
|
||||
const frame = requestAnimationFrame(() => {
|
||||
if (isSearchPending() || !hasMessageSearchAuthority(searchQuery(), query)) return
|
||||
const activeId = activeSearchMatch()?.id
|
||||
|
|
@ -1186,6 +1188,11 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
toolVisibility={(toolName) => resolveToolVisibility(preferences(), toolName)}
|
||||
onRevert={props.onRevert}
|
||||
onFork={props.onFork}
|
||||
pendingPrompt={props.pendingPrompts?.get(messageId)}
|
||||
pendingPromptBusy={props.pendingPromptBusy}
|
||||
onPendingPromptDeliveryChange={props.onPendingPromptDeliveryChange}
|
||||
onPendingPromptEdit={props.onPendingPromptEdit}
|
||||
onPendingPromptRemove={props.onPendingPromptRemove}
|
||||
onContentRendered={handleContentRendered}
|
||||
searchQuery={authoritativeSearchQuery}
|
||||
searchResultMessageIds={searchResultMessageIds}
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
import type { SessionInboxUser } from "@opencode-ai/client"
|
||||
import { For, Show } from "solid-js"
|
||||
import { Pencil, Play, Trash2, X } from "lucide-solid"
|
||||
import { useI18n } from "../lib/i18n"
|
||||
|
||||
interface PromptQueueProps {
|
||||
items: SessionInboxUser[]
|
||||
busyId?: string
|
||||
editingId?: string
|
||||
onDeliveryChange: (item: SessionInboxUser) => void
|
||||
onEdit: (item: SessionInboxUser) => void
|
||||
onCancelEdit: () => void
|
||||
onRemove: (item: SessionInboxUser) => void
|
||||
}
|
||||
|
||||
export default function PromptQueue(props: PromptQueueProps) {
|
||||
const { t } = useI18n()
|
||||
const text = (item: SessionInboxUser) => {
|
||||
const display = item.payload.metadata?.displayText
|
||||
return typeof display === "string" && display ? display : item.payload.text
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={props.items.length > 0}>
|
||||
<section class="prompt-queue" aria-label={t("promptQueue.title", { count: props.items.length })}>
|
||||
<header class="prompt-queue-header">{t("promptQueue.title", { count: props.items.length })}</header>
|
||||
<div class="prompt-queue-list">
|
||||
<For each={props.items}>{(item) => {
|
||||
const busy = () => Boolean(props.busyId)
|
||||
const editing = () => props.editingId === item.id
|
||||
const attachmentCount = () => item.payload.files?.length ?? 0
|
||||
return (
|
||||
<div class="prompt-queue-row" data-delivery={item.delivery} data-editing={editing() ? "true" : undefined}>
|
||||
<div class="prompt-queue-content">
|
||||
<div class="prompt-queue-meta">
|
||||
<span>{t("promptQueue.delivery.queue")}</span>
|
||||
<Show when={attachmentCount() > 0}>
|
||||
<span>{t(`promptQueue.attachments.${attachmentCount() === 1 ? "one" : "other"}`, { count: attachmentCount() })}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="prompt-queue-text" title={text(item)}>{text(item)}</div>
|
||||
</div>
|
||||
<div class="prompt-queue-actions">
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy()}
|
||||
title={t("promptQueue.actions.steer")}
|
||||
aria-label={t("promptQueue.actions.steer")}
|
||||
onClick={() => props.onDeliveryChange(item)}
|
||||
>
|
||||
<Play aria-hidden="true" />
|
||||
</button>
|
||||
<Show
|
||||
when={!editing()}
|
||||
fallback={
|
||||
<button type="button" disabled={busy()} title={t("promptQueue.actions.cancelEdit")} aria-label={t("promptQueue.actions.cancelEdit")} onClick={props.onCancelEdit}>
|
||||
<X aria-hidden="true" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<button type="button" disabled={busy()} title={t("promptQueue.actions.edit")} aria-label={t("promptQueue.actions.edit")} onClick={() => props.onEdit(item)}>
|
||||
<Pencil aria-hidden="true" />
|
||||
</button>
|
||||
</Show>
|
||||
<button type="button" disabled={busy()} title={t("promptQueue.actions.remove")} aria-label={t("promptQueue.actions.remove")} onClick={() => props.onRemove(item)}>
|
||||
<Trash2 aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}</For>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import MessageSection from "../message-section"
|
|||
import { messageStoreBus } from "../../stores/message-v2/bus"
|
||||
import PromptInput from "../prompt-input"
|
||||
import PromptAttachmentsBar from "../prompt-input/PromptAttachmentsBar"
|
||||
import PromptQueue from "../prompt-queue"
|
||||
import { getAttachments, hydrateSessionAttachments, removeAttachment } from "../../stores/attachments"
|
||||
import { instances, waitForInstanceWorkspaceMetadataHydration } from "../../stores/instances"
|
||||
import { getMessageNextCursor, hasMoreMessages, isLatestMessageWindow, loadLatestMessageWindow, loadMessages, loadMoreMessages, loadNewerMessageWindow, loadOldestMessageWindow, sendMessage, forkSession, renameSession, isSessionMessagesLoading, getSessionMessagesLoadError, markSessionIdleSeen, ensureSessionAncestorsExpanded, setActiveSessionFromList, runShellCommand, abortSession } from "../../stores/sessions"
|
||||
|
|
@ -94,8 +93,7 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
const attachments = createMemo(() => getAttachments(props.instanceId, props.sessionId))
|
||||
const pendingUserPrompts = createMemo(() => getOpenCodeSessionInbox(props.instanceId, props.sessionId, props.instanceFolder)
|
||||
.filter((item): item is SessionInboxUser => item.type === "user"))
|
||||
const queuedPrompts = createMemo(() => pendingUserPrompts().filter((item) => item.delivery === "queue"))
|
||||
const queuedMessageIds = createMemo(() => new Set(queuedPrompts().map((item) => item.id)))
|
||||
const pendingPromptById = createMemo(() => new Map(pendingUserPrompts().map((item) => [item.id, item])))
|
||||
const preview = createMemo(() => getSessionPreview(props.sessionId))
|
||||
|
||||
const MESSAGE_SCROLL_CACHE_SCOPE = "message-stream"
|
||||
|
|
@ -117,7 +115,7 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
createEffect(() => {
|
||||
const editing = editingQueuedPrompt()
|
||||
if (!editing) return
|
||||
const current = queuedPrompts().find((item) => item.id === editing.id)
|
||||
const current = pendingPromptById().get(editing.id)
|
||||
if (!current) cancelQueuedPromptEdit()
|
||||
else if (current !== editing) setEditingQueuedPrompt(current)
|
||||
})
|
||||
|
|
@ -129,8 +127,7 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
let submitBottomPinIntentSequence = 0
|
||||
|
||||
function visibleMessageCount() {
|
||||
const hidden = queuedMessageIds()
|
||||
return messageStore().getSessionMessageIds(props.sessionId).filter((id) => !hidden.has(id)).length
|
||||
return messageStore().getSessionMessageIds(props.sessionId).length
|
||||
}
|
||||
|
||||
function shouldScrollToBottomOnActivate() {
|
||||
|
|
@ -633,7 +630,11 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
showSidebarToggle={props.showSidebarToggle}
|
||||
onSidebarToggle={props.onSidebarToggle}
|
||||
forceCompactStatusLayout={props.forceCompactStatusLayout}
|
||||
queuedMessageIds={queuedMessageIds()}
|
||||
pendingPrompts={pendingPromptById()}
|
||||
pendingPromptBusy={Boolean(queueBusyId())}
|
||||
onPendingPromptDeliveryChange={(item) => void manageQueuedPrompt(item, "delivery")}
|
||||
onPendingPromptEdit={handleEditQueuedPrompt}
|
||||
onPendingPromptRemove={(item) => void manageQueuedPrompt(item, "remove")}
|
||||
onQuoteSelection={handleQuoteSelection}
|
||||
/>
|
||||
}
|
||||
|
|
@ -646,16 +647,6 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||
/>
|
||||
</Show>
|
||||
|
||||
<PromptQueue
|
||||
items={queuedPrompts()}
|
||||
busyId={queueBusyId()}
|
||||
editingId={editingQueuedPrompt()?.id}
|
||||
onDeliveryChange={(item) => void manageQueuedPrompt(item, "delivery")}
|
||||
onEdit={handleEditQueuedPrompt}
|
||||
onCancelEdit={cancelQueuedPromptEdit}
|
||||
onRemove={(item) => void manageQueuedPrompt(item, "remove")}
|
||||
/>
|
||||
|
||||
<Show when={attachments().length > 0}>
|
||||
<PromptAttachmentsBar
|
||||
attachments={attachments()}
|
||||
|
|
|
|||
|
|
@ -1068,12 +1068,12 @@ export default function ToolCall(props: ToolCallProps) {
|
|||
)}
|
||||
</Show>
|
||||
|
||||
<Show keyed when={pendingForm()}>
|
||||
<Show when={pendingForm()}>
|
||||
{(form) => (
|
||||
<FormRequest
|
||||
form={form}
|
||||
onReply={(answer) => sendFormReply(props.instanceId, form.id, answer)}
|
||||
onCancel={() => sendFormCancel(props.instanceId, form.id)}
|
||||
form={form()}
|
||||
onReply={(answer) => sendFormReply(props.instanceId, form().id, answer)}
|
||||
onCancel={() => sendFormCancel(props.instanceId, form().id)}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
sendFormReply,
|
||||
syncPendingRequests,
|
||||
} from "./instances.ts"
|
||||
import { formRequestOptions, getFormQueue } from "./forms.ts"
|
||||
import { addFormToQueue, clearFormQueue, formRequestOptions, getFormQueue } from "./forms.ts"
|
||||
import { getRootClient } from "./opencode-client.ts"
|
||||
import { sdkManager } from "../lib/sdk-manager.ts"
|
||||
import { sessions, setSessions } from "./session-state.ts"
|
||||
|
|
@ -24,6 +24,18 @@ const form = {
|
|||
} as any
|
||||
|
||||
describe("form interruption lifecycle", () => {
|
||||
it("keeps an active form object stable across duplicate sync results", () => {
|
||||
const instanceId = "stable-form-sync"
|
||||
try {
|
||||
addFormToQueue(instanceId, form)
|
||||
const active = getFormQueue(instanceId)[0]
|
||||
addFormToQueue(instanceId, { ...form })
|
||||
assert.strictEqual(getFormQueue(instanceId)[0], active)
|
||||
} finally {
|
||||
clearFormQueue(instanceId)
|
||||
}
|
||||
})
|
||||
|
||||
it("marks created or restored forms as pending until reply or cancellation", () => {
|
||||
const instanceId = "form-lifecycle"
|
||||
const sessionId = "session-1"
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ export function addFormToQueue(instanceId: string, form: FormWithLocation): void
|
|||
const next = new Map(previous)
|
||||
const queue = next.get(instanceId) ?? []
|
||||
const index = queue.findIndex((item) => item.id === form.id)
|
||||
if (index !== -1) return previous
|
||||
const updated = queue.slice()
|
||||
if (index === -1) updated.push(form)
|
||||
else updated[index] = form
|
||||
updated.push(form)
|
||||
next.set(instanceId, updated)
|
||||
return next
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
@import "./messaging/message-base.css";
|
||||
@import "./messaging/prompt-input.css";
|
||||
@import "./messaging/prompt-queue.css";
|
||||
@import "./messaging/message-section.css";
|
||||
@import "./messaging/virtual-follow-list.css";
|
||||
@import "./messaging/message-selection.css";
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
.prompt-queue {
|
||||
max-height: min(15rem, 34vh);
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid var(--border-base);
|
||||
background: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.prompt-queue-header {
|
||||
padding: 0.35rem 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.prompt-queue-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.prompt-queue-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
gap: 0.5rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
border-top: 1px solid var(--border-subtle, var(--border-base));
|
||||
}
|
||||
|
||||
.prompt-queue-row[data-editing="true"] {
|
||||
box-shadow: inset 3px 0 0 var(--accent-primary);
|
||||
background: var(--surface-base);
|
||||
}
|
||||
|
||||
.prompt-queue-content {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.prompt-queue-meta {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
.prompt-queue-text {
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.prompt-queue-actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.prompt-queue-actions button {
|
||||
display: inline-flex;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.prompt-queue-actions button:hover:not(:disabled) {
|
||||
border-color: var(--border-base);
|
||||
background: var(--surface-base);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.prompt-queue-actions button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.prompt-queue-actions svg {
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue