Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 94 additions & 4 deletions packages/app/src/components/prompt-input/submit.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"
import type { Prompt } from "@/context/prompt"
import type { ContextItem, Prompt } from "@/context/prompt"

let createPromptSubmit: typeof import("./submit").createPromptSubmit
type FollowupDraft = import("./submit").FollowupDraft

const createdClients: string[] = []
const createdSessions: string[] = []
Expand All @@ -20,10 +21,13 @@ const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {}
const promoted: Array<{ directory: string; sessionID: string }> = []
const sentShell: string[] = []
const syncedDirectories: string[] = []
const removedContextKeys: string[] = []

let params: { id?: string } = {}
let selected = "/repo/worktree-a"
let variant: string | undefined
let promptContextItems: Array<ContextItem & { key: string }> = []
let promptResetCount = 0

const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]

Expand Down Expand Up @@ -106,12 +110,17 @@ beforeAll(async () => {
mock.module("@/context/prompt", () => ({
usePrompt: () => ({
current: () => promptValue,
reset: () => undefined,
reset: () => {
promptResetCount += 1
},
set: () => undefined,
context: {
add: () => undefined,
remove: () => undefined,
items: () => [],
remove: (key: string) => {
removedContextKeys.push(key)
promptContextItems = promptContextItems.filter((item) => item.key !== key)
},
items: () => promptContextItems,
},
}),
}))
Expand Down Expand Up @@ -211,8 +220,11 @@ beforeEach(() => {
params = {}
sentShell.length = 0
syncedDirectories.length = 0
removedContextKeys.length = 0
selected = "/repo/worktree-a"
variant = undefined
promptContextItems = []
promptResetCount = 0
for (const key of Object.keys(storedSessions)) delete storedSessions[key]
})

Expand Down Expand Up @@ -343,3 +355,81 @@ describe("prompt submit worktree selection", () => {
expect(optimisticSeeded).toEqual([true])
})
})

describe("queued followups", () => {
test("queues followup drafts instead of sending immediately", async () => {
params = { id: "session-1" }
variant = "high"
promptContextItems = [
{
key: "context-1",
type: "file",
path: "src/index.ts",
selection: undefined,
comment: "Check this",
commentID: "comment-1",
commentOrigin: "file",
preview: "const value = 1",
},
]

const queued: FollowupDraft[] = []
const modes: Array<"normal" | "shell"> = []
const popovers: Array<"at" | "slash" | null> = []
let submitCalls = 0

const submit = createPromptSubmit({
info: () => ({ id: "session-1" }),
imageAttachments: () => [],
commentCount: () => 1,
autoAccept: () => false,
mode: () => "normal",
working: () => true,
editor: () => undefined,
queueScroll: () => undefined,
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
addToHistory: () => undefined,
resetHistoryNavigation: () => undefined,
setMode: (mode) => modes.push(mode),
setPopover: (popover) => popovers.push(popover),
shouldQueue: () => true,
onQueue: (draft) => queued.push(draft),
onSubmit: () => {
submitCalls += 1
},
})

const event = { preventDefault: () => undefined } as unknown as Event

await submit.handleSubmit(event)

expect(queued).toHaveLength(1)
expect(queued[0]).toMatchObject({
sessionID: "session-1",
sessionDirectory: "/repo/main",
prompt: promptValue,
context: [
{
key: "context-1",
type: "file",
path: "src/index.ts",
comment: "Check this",
commentID: "comment-1",
commentOrigin: "file",
preview: "const value = 1",
},
],
agent: "agent",
model: { providerID: "provider", modelID: "model" },
variant: "high",
})
expect(promptResetCount).toBe(1)
expect(promptContextItems).toEqual([])
expect(removedContextKeys).toEqual(["context-1"])
expect(modes).toEqual(["normal"])
expect(popovers).toEqual([null])
expect(submitCalls).toBe(0)
expect(optimistic).toEqual([])
expect(createdSessions).toEqual([])
})
})
23 changes: 23 additions & 0 deletions packages/app/src/components/settings-general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ export const SettingsGeneral: Component = () => {
{ value: "dark", label: language.t("theme.scheme.dark") },
])

const followupOptions = createMemo((): { value: "queue" | "steer"; label: string }[] => [
{ value: "queue", label: language.t("settings.general.row.followup.option.queue") },
{ value: "steer", label: language.t("settings.general.row.followup.option.steer") },
])

const languageOptions = createMemo(() =>
language.locales.map((locale) => ({
value: locale,
Expand Down Expand Up @@ -388,6 +393,24 @@ export const SettingsGeneral: Component = () => {
</div>
</SettingsRow>

<SettingsRow
title={language.t("settings.general.row.followup.title")}
description={language.t("settings.general.row.followup.description")}
>
<Select
data-action="settings-followup"
options={followupOptions()}
current={followupOptions().find((o) => o.value === settings.general.followup())}
value={(o) => o.value}
label={(o) => o.label}
onSelect={(option) => option && settings.general.setFollowup(option.value)}
variant="secondary"
size="small"
triggerVariant="settings"
triggerStyle={{ "min-width": "180px" }}
/>
</SettingsRow>

<SettingsRow
title={language.t("settings.general.row.showSessionProgressBar.title")}
description={language.t("settings.general.row.showSessionProgressBar.description")}
Expand Down
12 changes: 2 additions & 10 deletions packages/app/src/context/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
root.style.setProperty("--font-family-sans", sansFontFamily(store.appearance?.sans))
})

createEffect(() => {
if (store.general?.followup !== "queue") return
setStore("general", "followup", "steer")
})

return {
ready,
get current() {
Expand All @@ -181,12 +176,9 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setReleaseNotes(value: boolean) {
setStore("general", "releaseNotes", value)
},
followup: withFallback(
() => (store.general?.followup === "queue" ? "steer" : store.general?.followup),
defaultSettings.general.followup,
),
followup: withFallback(() => store.general?.followup, defaultSettings.general.followup),
setFollowup(value: "queue" | "steer") {
setStore("general", "followup", value === "queue" ? "steer" : value)
setStore("general", "followup", value)
},
showFileTree: withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree),
setShowFileTree(value: boolean) {
Expand Down
30 changes: 21 additions & 9 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { syncSessionModel } from "@/pages/session/session-model-helpers"
import { SessionSidePanel } from "@/pages/session/session-side-panel"
import { TerminalPanel } from "@/pages/session/terminal-panel"
import { useSessionCommands } from "@/pages/session/use-session-commands"
import { shouldAutoSendFollowup, shouldQueueFollowup } from "@/pages/session/session-followup-state"
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
import { shouldUseV2NewSessionPage } from "@/pages/session/new-session-layout"
import { Identifier } from "@/utils/id"
Expand Down Expand Up @@ -1414,7 +1415,12 @@ export default function Page() {
const queueEnabled = createMemo(() => {
const id = params.id
if (!id) return false
return settings.general.followup() === "queue" && busy(id) && !composer.blocked() && !isChildSession()
return shouldQueueFollowup({
followup: settings.general.followup(),
busy: busy(id),
blocked: composer.blocked(),
child: isChildSession(),
})
})

const followupText = (item: FollowupDraft) => {
Expand Down Expand Up @@ -1573,15 +1579,21 @@ export default function Page() {
if (!sessionID) return

const item = queuedFollowups()[0]
if (!item) return
if (followupBusy(sessionID)) return
if (followup.failed[sessionID] === item.id) return
if (followup.paused[sessionID]) return
if (isChildSession()) return
if (composer.blocked()) return
if (busy(sessionID)) return
if (
!shouldAutoSendFollowup({
hasItem: !!item,
sending: followupBusy(sessionID),
failed: followup.failed[sessionID] === item?.id,
paused: !!followup.paused[sessionID],
blocked: composer.blocked(),
busy: busy(sessionID),
child: isChildSession(),
})
) {
return
}

void sendFollowup(sessionID, item.id)
void sendFollowup(sessionID, item!.id)
})

createResizeObserver(
Expand Down
Loading
Loading