From 0e0df1fe9ee952aa8fda1915f43ade616d28277b Mon Sep 17 00:00:00 2001 From: henry701 Date: Fri, 5 Jun 2026 01:10:02 -0300 Subject: [PATCH 1/6] feat(tui): restore queued prompt workflows --- build-local.sh | 109 +++ opencode-local-update.sh | 42 + packages/app/src/components/prompt-input.tsx | 124 ++- .../components/prompt-input/submit.test.ts | 161 +++- .../app/src/components/prompt-input/submit.ts | 20 +- .../src/context/global-sync/bootstrap.test.ts | 1 + .../app/src/context/global-sync/bootstrap.ts | 13 +- .../src/context/global-sync/child-store.ts | 1 + .../src/context/global-sync/event-reducer.ts | 6 + .../context/global-sync/session-cache.test.ts | 4 + .../src/context/global-sync/session-cache.ts | 2 + packages/app/src/context/global-sync/types.ts | 3 + packages/app/src/context/server-sync.tsx | 16 +- packages/app/src/i18n/en.ts | 1 + .../app/src/pages/session.queue-save.test.ts | 35 + packages/app/src/pages/session.queue-save.ts | 16 + packages/app/src/pages/session.tsx | 256 +++--- .../composer/session-composer-region.tsx | 9 +- .../composer/session-followup-dock.tsx | 90 +- packages/app/src/utils/queue-parts.ts | 21 + .../opencode/scripts/adhoc-queue-qa-smoke.py | 769 +++++++++++++++++ packages/opencode/scripts/start-mock-llm.ts | 15 + .../src/cli/cmd/run/footer.command.tsx | 6 +- .../src/cli/cmd/run/footer.prompt.tsx | 537 +++++++----- .../src/cli/cmd/run/footer.queue-dock.tsx | 45 + packages/opencode/src/cli/cmd/run/footer.ts | 124 ++- .../opencode/src/cli/cmd/run/footer.view.tsx | 354 +++++--- .../opencode/src/cli/cmd/run/keymap.shared.ts | 154 ++++ .../opencode/src/cli/cmd/run/prompt.shared.ts | 206 ++++- .../opencode/src/cli/cmd/run/runtime.boot.ts | 101 ++- .../src/cli/cmd/run/runtime.lifecycle.ts | 15 +- .../src/cli/cmd/run/runtime.queue-remote.ts | 48 ++ .../opencode/src/cli/cmd/run/runtime.queue.ts | 395 ++++++--- packages/opencode/src/cli/cmd/run/runtime.ts | 152 +++- .../src/cli/cmd/run/stream.transport.ts | 85 +- packages/opencode/src/cli/cmd/run/types.ts | 51 +- .../cli/cmd/tui/component/prompt/index.tsx | 770 ++++++++++++++++-- .../cmd/tui/component/prompt/queue-actions.ts | 6 + .../cmd/tui/component/prompt/queue-dock.tsx | 48 ++ .../src/cli/cmd/tui/component/prompt/queue.ts | 30 + .../src/cli/cmd/tui/config/keybind.ts | 12 +- .../opencode/src/cli/cmd/tui/context/sync.tsx | 17 +- .../src/cli/cmd/tui/routes/session/index.tsx | 14 +- packages/opencode/src/effect/app-runtime.ts | 2 + packages/opencode/src/id/id.ts | 1 + packages/opencode/src/provider/schema.ts | 8 + packages/opencode/src/queue/edit.ts | 11 + packages/opencode/src/queue/preview.ts | 42 + packages/opencode/src/queue/prompt-queue.ts | 414 ++++++++++ packages/opencode/src/queue/queue-dock.tsx | 241 ++++++ .../routes/instance/httpapi/groups/session.ts | 140 +++- .../instance/httpapi/handlers/session.ts | 145 +++- .../server/routes/instance/httpapi/server.ts | 2 + packages/opencode/src/session/message-v2.ts | 145 +++- packages/opencode/src/session/prompt-queue.ts | 165 ++++ packages/opencode/src/session/prompt.ts | 557 +++++++++---- packages/opencode/src/session/revert.ts | 4 + packages/opencode/src/session/run-state.ts | 14 +- packages/opencode/src/session/schema.ts | 9 + packages/opencode/src/session/session.ts | 14 +- .../cli/cmd/tui/sync-live-hydration.test.tsx | 29 + .../test/cli/run/footer.view.test.tsx | 370 ++------- .../test/cli/run/prompt.shared.test.ts | 74 ++ .../test/cli/run/runtime.queue-remote.test.ts | 79 ++ .../test/cli/run/runtime.queue.test.ts | 514 +++++++----- packages/opencode/test/cli/run/stream.test.ts | 2 +- .../test/cli/run/stream.transport.test.ts | 76 +- .../test/cli/tui/prompt-queue-actions.test.ts | 33 + .../test/cli/tui/prompt-queue-dock.test.ts | 98 +++ .../test/cli/tui/prompt-queue-smoke.test.tsx | 143 ++++ .../tui/prompt-queue-tui-smoke.cli.test.ts | 122 +++ .../test/cli/tui/prompt-queue.test.ts | 110 +++ .../cli/tui/scripts/prompt-queue-tui-smoke.sh | 97 +++ .../opencode/test/queue/prompt-queue.test.ts | 75 ++ .../test/server/session-queue.test.ts | 188 +++++ .../opencode/test/session/message-v2.test.ts | 23 + .../test/session/prompt-queue-drain.test.ts | 280 +++++++ packages/opencode/test/session/prompt.test.ts | 2 + .../test/session/revert-compact.test.ts | 30 + .../test/session/snapshot-tool-race.test.ts | 2 + packages/plugin/src/tui.ts | 2 + packages/sdk/js/src/v2/gen/sdk.gen.ts | 433 ++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 416 ++++++++++ 83 files changed, 8511 insertions(+), 1485 deletions(-) create mode 100755 build-local.sh create mode 100755 opencode-local-update.sh create mode 100644 packages/app/src/pages/session.queue-save.test.ts create mode 100644 packages/app/src/pages/session.queue-save.ts create mode 100644 packages/app/src/utils/queue-parts.ts create mode 100755 packages/opencode/scripts/adhoc-queue-qa-smoke.py create mode 100644 packages/opencode/scripts/start-mock-llm.ts create mode 100644 packages/opencode/src/cli/cmd/run/footer.queue-dock.tsx create mode 100644 packages/opencode/src/cli/cmd/run/keymap.shared.ts create mode 100644 packages/opencode/src/cli/cmd/run/runtime.queue-remote.ts create mode 100644 packages/opencode/src/cli/cmd/tui/component/prompt/queue-actions.ts create mode 100644 packages/opencode/src/cli/cmd/tui/component/prompt/queue-dock.tsx create mode 100644 packages/opencode/src/cli/cmd/tui/component/prompt/queue.ts create mode 100644 packages/opencode/src/provider/schema.ts create mode 100644 packages/opencode/src/queue/edit.ts create mode 100644 packages/opencode/src/queue/preview.ts create mode 100644 packages/opencode/src/queue/prompt-queue.ts create mode 100644 packages/opencode/src/queue/queue-dock.tsx create mode 100644 packages/opencode/src/session/prompt-queue.ts create mode 100644 packages/opencode/test/cli/run/runtime.queue-remote.test.ts create mode 100644 packages/opencode/test/cli/tui/prompt-queue-actions.test.ts create mode 100644 packages/opencode/test/cli/tui/prompt-queue-dock.test.ts create mode 100644 packages/opencode/test/cli/tui/prompt-queue-smoke.test.tsx create mode 100644 packages/opencode/test/cli/tui/prompt-queue-tui-smoke.cli.test.ts create mode 100644 packages/opencode/test/cli/tui/prompt-queue.test.ts create mode 100755 packages/opencode/test/cli/tui/scripts/prompt-queue-tui-smoke.sh create mode 100644 packages/opencode/test/queue/prompt-queue.test.ts create mode 100644 packages/opencode/test/server/session-queue.test.ts create mode 100644 packages/opencode/test/session/prompt-queue-drain.test.ts diff --git a/build-local.sh b/build-local.sh new file mode 100755 index 000000000000..f4381fc33e9f --- /dev/null +++ b/build-local.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# build-local.sh - build opencode for current platform and install as opencode-local. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PKG_DIR="$REPO_ROOT/packages/opencode" +INSTALL_DIR="$HOME/.local/bin" +BINARY_NAME="opencode-local" +SERVICE_NAME="opencode-server" +RESTART_SERVICE=1 + +usage() { + cat <&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ "${OPENCODE_LOCAL_NO_RESTART:-}" == "1" || "${OPENCODE_LOCAL_SKIP_RESTART:-}" == "1" ]]; then + RESTART_SERVICE=0 +fi + +export PATH="$PATH:$REPO_ROOT/node_modules/.bin" + +echo "==> Installing dependencies..." +bun install --minimum-release-age 0 --frozen-lockfile + +echo "==> Building opencode (native linux-x64, --single)..." +cd "$PKG_DIR" +bun run script/build.ts --single --skip-install + +BUILT_BIN="$PKG_DIR/dist/opencode-linux-x64/bin/opencode" +if [[ ! -f "$BUILT_BIN" ]]; then + echo "ERROR: expected binary not found at $BUILT_BIN" >&2 + exit 1 +fi + +echo "==> Smoke test..." +"$BUILT_BIN" --version + +echo "==> Installing to $INSTALL_DIR/$BINARY_NAME..." +mkdir -p "$INSTALL_DIR" +cp -f "$BUILT_BIN" "$INSTALL_DIR/$BINARY_NAME" +chmod +x "$INSTALL_DIR/$BINARY_NAME" +echo " installed: $("$INSTALL_DIR/$BINARY_NAME" --version)" + +SERVICE_FILE="$HOME/.config/systemd/user/$SERVICE_NAME.service" +if [[ -f "$SERVICE_FILE" ]]; then + echo "==> Updating $SERVICE_FILE..." + sed -i "s|ExecStart=.*opencode |ExecStart=$INSTALL_DIR/$BINARY_NAME |" "$SERVICE_FILE" + echo " ExecStart line: $(grep ExecStart "$SERVICE_FILE")" +else + echo "==> Service file not found at $SERVICE_FILE; skipping service file update." +fi + +if [[ "$RESTART_SERVICE" == "0" ]]; then + echo "==> Skipping service reload/restart because --no-restart-service was set." + echo "==> Done." + exit 0 +fi + +echo "==> Reloading systemd user daemon and restarting $SERVICE_NAME..." +if XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user daemon-reload 2>/dev/null; then + XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user restart "$SERVICE_NAME" + sleep 1 + XDG_RUNTIME_DIR="/run/user/$(id -u)" systemctl --user status "$SERVICE_NAME" --no-pager +else + echo " DBUS unavailable - simulating start on a different port to verify binary works..." + TEST_PORT=14097 + OPENCODE_SERVER_PASSWORD=test OPENCODE_SERVER_USERNAME=test \ + "$INSTALL_DIR/$BINARY_NAME" serve --port "$TEST_PORT" --hostname 127.0.0.1 & + SIM_PID=$! + sleep 2 + if kill -0 "$SIM_PID" 2>/dev/null; then + echo " OK: binary started successfully on port $TEST_PORT (pid $SIM_PID)" + kill "$SIM_PID" + else + echo "ERROR: binary failed to stay alive on test port $TEST_PORT" >&2 + exit 1 + fi + echo "" + echo " NOTE: run 'systemctl --user restart $SERVICE_NAME' from your desktop session to apply." +fi + +echo "==> Done." diff --git a/opencode-local-update.sh b/opencode-local-update.sh new file mode 100755 index 000000000000..a5cd9ab21ae3 --- /dev/null +++ b/opencode-local-update.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# opencode-local-update.sh - build and install the currently checked-out production branch. +# Invoked manually or by opencode-local-build.service. This script pulls the current upstream with --ff-only. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BRANCH_NAME="production" + +export PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin:$REPO_ROOT/node_modules/.bin" +export SSH_AUTH_SOCK="/run/user/$UID/ssh-tpm-agent.sock" + +cd "$REPO_ROOT" + +CURRENT_BRANCH="$(git branch --show-current)" + +if [[ "$CURRENT_BRANCH" != "$BRANCH_NAME" ]]; then + echo "ERROR: current branch is '$CURRENT_BRANCH'; checkout '$BRANCH_NAME' before building." >&2 + exit 1 +fi + +if ! UPSTREAM_REF="$(git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)"; then + echo "ERROR: branch '$BRANCH_NAME' has no upstream configured." >&2 + exit 1 +fi + +UPSTREAM_REMOTE="${UPSTREAM_REF%%/*}" +UPSTREAM_BRANCH="${UPSTREAM_REF#*/}" + +if [[ "$UPSTREAM_BRANCH" != "$BRANCH_NAME" ]]; then + echo "ERROR: upstream branch is '$UPSTREAM_BRANCH'; expected '$BRANCH_NAME'." >&2 + exit 1 +fi + +echo "==> Pulling $UPSTREAM_REF..." +git pull --ff-only "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH" + +HEAD_HASH="$(git rev-parse HEAD)" +HEAD_SHORT="$(git rev-parse --short HEAD)" + +echo "==> Building current $BRANCH_NAME checkout at $HEAD_SHORT ($HEAD_HASH)..." +echo "==> Building and reinstalling from $BRANCH_NAME..." +"$REPO_ROOT/build-local.sh" "$@" diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 8e4913b1f5b0..ffb1b22b6605 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -86,7 +86,10 @@ interface PromptInputProps { newSessionWorktree?: string onNewSessionWorktreeReset?: () => void edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] } + editingQueueID?: string + onEditingQueueMessageID?: (id: string | undefined) => void onEditLoaded?: () => void + onCancelQueueEdit?: () => void shouldQueue?: () => boolean onQueue?: (draft: FollowupDraft) => void onAbort?: () => void @@ -268,6 +271,16 @@ export const PromptInput: Component = (props) => { const imageAttachments = createMemo(() => prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"), ) + // Per-message queue override toggled by the composer button or Alt+Enter. It + // makes the next submit go to the server prompt queue even + // when the followup setting is not "queue", and resets after each submit. + const [queueMode, setQueueMode] = createSignal(false) + const [localEditingQueueID, setLocalEditingQueueID] = createSignal() + const editingQueueID = () => props.editingQueueID ?? localEditingQueueID() + const setEditingQueueID = (id: string | undefined) => { + props.onEditingQueueMessageID?.(id) + if (props.onEditingQueueMessageID === undefined) setLocalEditingQueueID(id) + } const [store, setStore] = createStore<{ popover: "at" | "slash" | null @@ -277,7 +290,6 @@ export const PromptInput: Component = (props) => { draggingType: "image" | "@mention" | null mode: "normal" | "shell" applyingHistory: boolean - variantOpen: boolean }>({ popover: null, historyIndex: -1, @@ -286,7 +298,6 @@ export const PromptInput: Component = (props) => { draggingType: null, mode: "normal", applyingHistory: false, - variantOpen: false, }) const [picker, setPicker] = createStore({ projectOpen: false, @@ -334,6 +345,31 @@ export const PromptInput: Component = (props) => { ) } + // Per-message queue toggle. Only meaningful for existing sessions in normal + // mode (shell commands always run now, and new sessions have nothing to queue + // against). Clicking flips queueMode; the send icon and submit path follow. + const queueToggle = () => ( + + + setQueueMode((value) => !value)} + tabIndex={store.mode === "normal" ? undefined : -1} + aria-pressed={queueMode()} + aria-label={queueMode() ? language.t("prompt.action.sendDirect") : language.t("prompt.action.queue")} + /> + + + ) + const contextItems = createMemo(() => { const items = prompt.context.items() if (store.mode !== "shell") return items @@ -1027,6 +1063,8 @@ export const PromptInput: Component = (props) => { const edit = props.edit if (!id || !edit) return + setEditingQueueID(edit.id) + for (const item of prompt.context.items()) { prompt.context.remove(item.key) } @@ -1103,8 +1141,6 @@ export const PromptInput: Component = (props) => { ) const variants = createMemo(() => ["default", ...local.model.variant.list()]) - // Check provider variants directly: `variants` also includes the UI-only default option. - const showVariantControl = createMemo(() => local.model.variant.list().length > 0) const accepting = createMemo(() => { const id = params.id if (!id) return permission.isAutoAcceptingDirectory(sdk.directory) @@ -1130,6 +1166,10 @@ export const PromptInput: Component = (props) => { newSessionWorktree: () => props.newSessionWorktree, onNewSessionWorktreeReset: props.onNewSessionWorktreeReset, shouldQueue: props.shouldQueue, + queueMode, + resetQueueMode: () => setQueueMode(false), + editingQueueID: () => editingQueueID(), + resetEditingQueueID: () => setEditingQueueID(undefined), onQueue: props.onQueue, onAbort: props.onAbort, onSubmit: props.onSubmit, @@ -1179,6 +1219,13 @@ export const PromptInput: Component = (props) => { return } + if (editingQueueID()) { + props.onCancelQueueEdit?.() + event.preventDefault() + event.stopPropagation() + return + } + if (store.mode === "shell") { setStore("mode", "normal") event.preventDefault() @@ -1277,6 +1324,14 @@ export const PromptInput: Component = (props) => { return } + if (event.altKey && event.key === "Enter" && store.mode !== "shell") { + event.preventDefault() + if (event.repeat) return + setQueueMode(true) + void handleSubmit(event) + return + } + // Note: Shift+Enter is handled earlier, before IME check if (event.key === "Enter" && !event.shiftKey) { event.preventDefault() @@ -1367,13 +1422,13 @@ export const PromptInput: Component = (props) => { navigate(`/${base64Encode(worktree)}/session`) } const addProject = async () => { - const conn = server.current - if (!conn) return const select = (result: string | string[] | null) => { const directory = Array.isArray(result) ? result[0] : result if (!directory) return selectProject(directory) } + const conn = server.current + if (!conn) return if (platform.openDirectoryPickerDialog && server.isLocal()) { select(await platform.openDirectoryPickerDialog({ title: language.t("command.project.open") })) return @@ -1575,47 +1630,23 @@ export const PromptInput: Component = (props) => { - -
- -