diff --git a/MIGRATION_V2.md b/MIGRATION_V2.md index c8bb3ecb..0a2aec5d 100644 --- a/MIGRATION_V2.md +++ b/MIGRATION_V2.md @@ -143,7 +143,7 @@ During stabilization, CodeNomad implements the public native V2 contract and rem | Delete-to-boundary / undo | `session.revert.stage` and `session.revert.clear` use V2 staged-revert semantics instead of arbitrary deletion. | Undo uses `revert.stage`. Redo/clear remains a CodeNomad UI gap, not a reason to restore V1 deletion. | Expose `revert.clear` through the existing native contract. | | Compaction | Native checkpoint compaction summarizes the older head and retains a server-selected recent tail controlled by `compaction.keep.tokens`. | Uses `session.compact`; there is no message-level selective compaction. CodeNomad displays the terminal summary without rendering every streamed delta. | Add scoped controls only if V2 defines scoped compaction semantics. | | Full-session search | Native message cursors exist, but there is no server search endpoint that returns message identity and position. | Retained through bounded cursor traversal while keeping only the 200-message resident window and collected matches. Large searches still fetch every page. | V2 exposes server search plus a rank/cursor navigation target. | -| Queued prompt management | Native inbox list, cancel, steer, and queue operations are available. | Implemented with authoritative queue/steer switching, cancellation, safe edit replacement, draft preservation, order-preserving suffix rewrites, and queue admission for follow-up prompts. | Replace suffix rewrites if V2 adds atomic inbox update and reorder operations. | +| Queued prompt management | Native inbox list, cancel, steer, and queue operations are available. | Implemented with authoritative steering, cancellation, safe edit replacement, draft preservation, and queue admission for follow-up prompts. | Add in-place editing and reordering when V2 exposes atomic inbox mutations; replacement currently moves an edited prompt to the queue tail. | | Background execution | Native Shell resources support listing, bounded output, and removal, but do not reproduce every custom V1 process-manager control. | Replaced the custom manager with native Shells and removed unsupported controls such as rename. | Add controls only when native Shell APIs support them. | | Service lifecycle | V2 uses one shared externally owned service rather than one runtime per workspace. | CodeNomad discovers or starts the service but never exposes workspace stop or stops the daemon on shutdown. | No parity work planned unless V2 changes service ownership semantics. | diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index ff35da08..c0bda8de 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -50,7 +50,6 @@ import { syncLoadedSessionInboxes, syncPendingRequests, } from "./stores/instances" -import { shellStore } from "./stores/shells" import { getSessions, getSessionRoot, @@ -333,7 +332,6 @@ const App: Component = () => { syncPendingRequests(id, (invalidate) => { invalidatePendingRequests = invalidate }), refreshVolatileInstanceState(id), syncLoadedSessionInboxes(id), - shellStore.refreshForEvent(id, { type: "server.connected" }), ]) if (sessionError) throw sessionError })(), diff --git a/packages/ui/src/components/message-section.tsx b/packages/ui/src/components/message-section.tsx index 38604d7b..ab181760 100644 --- a/packages/ui/src/components/message-section.tsx +++ b/packages/ui/src/components/message-section.tsx @@ -792,7 +792,8 @@ 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 }), + visit: () => buildSessionSearchMatches({ store: store(), sessionId, query, includeThinking }) + .filter((match) => !props.queuedMessageIds?.has(match.messageId)), }).then((matches) => { if (!matches) { if (isCurrentSearch()) setIsSearchPending(false) @@ -828,6 +829,7 @@ 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 diff --git a/packages/ui/src/components/prompt-input.tsx b/packages/ui/src/components/prompt-input.tsx index 62f63bd5..824e368c 100644 --- a/packages/ui/src/components/prompt-input.tsx +++ b/packages/ui/src/components/prompt-input.tsx @@ -95,6 +95,7 @@ export default function PromptInput(props: PromptInputProps) { let wrapperRef: HTMLDivElement | undefined let fieldContainerRef: HTMLDivElement | undefined let resizeDragState: ResizeDragState | undefined + let submissionsInFlight = 0 const getPlaceholder = () => { if (mode() === "shell") { @@ -519,6 +520,7 @@ export default function PromptInput(props: PromptInputProps) { focusConversationStream(wrapperRef?.closest(".session-view")) } + submissionsInFlight += 1 try { if (isShellMode) { if (props.onRunShell) { @@ -546,6 +548,8 @@ export default function PromptInput(props: PromptInputProps) { textareaRef?.focus() } return + } finally { + submissionsInFlight -= 1 } } @@ -737,7 +741,7 @@ export default function PromptInput(props: PromptInputProps) { getAttachments: attachments, removeAttachment: (attachmentId) => removeAttachment(props.instanceId, props.sessionId, attachmentId), submitOnEnter, - onSend: (alternate) => void handleSend(alternate && props.isSessionBusy ? "queue" : "steer"), + onSend: (alternate) => void handleSend(alternate && (props.isSessionBusy || submissionsInFlight > 0) ? "queue" : "steer"), selectPreviousHistory: (force) => selectPreviousHistory({ force, isPickerOpen: showPicker(), getTextarea: () => textareaRef ?? null }), selectNextHistory: (force) => diff --git a/packages/ui/src/components/prompt-queue.tsx b/packages/ui/src/components/prompt-queue.tsx index 1e41ca3d..2e3908c4 100644 --- a/packages/ui/src/components/prompt-queue.tsx +++ b/packages/ui/src/components/prompt-queue.tsx @@ -1,6 +1,6 @@ import type { SessionInboxUser } from "@opencode-ai/client" import { For, Show } from "solid-js" -import { ArrowDown, ArrowUp, Pencil, Play, Trash2, X } from "lucide-solid" +import { Pencil, Play, Trash2, X } from "lucide-solid" import { useI18n } from "../lib/i18n" interface PromptQueueProps { @@ -11,7 +11,6 @@ interface PromptQueueProps { onEdit: (item: SessionInboxUser) => void onCancelEdit: () => void onRemove: (item: SessionInboxUser) => void - onMove: (item: SessionInboxUser, direction: -1 | 1) => void } export default function PromptQueue(props: PromptQueueProps) { @@ -26,8 +25,8 @@ export default function PromptQueue(props: PromptQueueProps) {
{t("promptQueue.title", { count: props.items.length })}
- {(item, index) => { - const busy = () => props.busyId === item.id + {(item) => { + const busy = () => Boolean(props.busyId) const editing = () => props.editingId === item.id const attachmentCount = () => item.payload.files?.length ?? 0 return ( @@ -51,12 +50,6 @@ export default function PromptQueue(props: PromptQueueProps) { >