| author | |
| committer | |
| log | 7afa184a99b266d466cc9517c147a75c3d839ad7 |
| tree | 8b9eaee79a2e997777bb73c885c4001e63316eea |
| parent | a7c5ad5db167b3a172ccb26408b0638c99b2a459 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: AMohamedAakhil <hello@takaitech.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>4 files changed, 287 insertions(+), 224 deletions(-)
apps/web/src/components/chat/ChatComposer.tsx+3| ... | ... | @@ -407,6 +407,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( |
| 407 | 407 | isEnvironmentUnavailable: boolean; |
| 408 | 408 | hasSendableContent: boolean; |
| 409 | 409 | preserveComposerFocusOnPointerDown?: boolean; |
| 410 | showSendWhileRunning?: boolean; | |
| 410 | 411 | onPreviousPendingQuestion: () => void; |
| 411 | 412 | onInterrupt: () => void; |
| 412 | 413 | onImplementPlanInNewThread: () => void; |
| ... | ... | @@ -435,6 +436,7 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( |
| 435 | 436 | isPreparingWorktree={props.isPreparingWorktree} |
| 436 | 437 | hasSendableContent={props.hasSendableContent} |
| 437 | 438 | preserveComposerFocusOnPointerDown={props.preserveComposerFocusOnPointerDown ?? false} |
| 439 | showSendWhileRunning={props.showSendWhileRunning ?? false} | |
| 438 | 440 | onPreviousPendingQuestion={props.onPreviousPendingQuestion} |
| 439 | 441 | onInterrupt={props.onInterrupt} |
| 440 | 442 | onImplementPlanInNewThread={props.onImplementPlanInNewThread} |
| ... | ... | @@ -3166,6 +3168,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 3166 | 3168 | isPreparingWorktree={isPreparingWorktree} |
| 3167 | 3169 | hasSendableContent={composerSendState.hasSendableContent} |
| 3168 | 3170 | preserveComposerFocusOnPointerDown={isMobileViewport} |
| 3171 | showSendWhileRunning={isMobileViewport} | |
| 3169 | 3172 | onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion} |
| 3170 | 3173 | onInterrupt={handleInterruptPrimaryAction} |
| 3171 | 3174 | onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction} |
apps/web/src/components/chat/ComposerPrimaryActions.test.ts deleted-218| ... | ... | @@ -1,218 +0,0 @@ |
| 1 | import { createElement } from "react"; | |
| 2 | import { renderToStaticMarkup } from "react-dom/server"; | |
| 3 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; | |
| 4 | ||
| 5 | const stageArtworkState = vi.hoisted(() => ({ | |
| 6 | mode: "none" as "artwork" | "none", | |
| 7 | variant: null as "nightly" | "dev" | null, | |
| 8 | })); | |
| 9 | ||
| 10 | vi.mock("~/hooks/useSettings", () => ({ | |
| 11 | useEnvironmentIdentificationMode: () => stageArtworkState.mode, | |
| 12 | })); | |
| 13 | vi.mock("../SidebarStageBackdrop", () => ({ | |
| 14 | StageBackdropButtonArt: ({ variant }: { variant: string }) => `stage-${variant}`, | |
| 15 | useSidebarStageBackdropVariant: (enabled = true) => (enabled ? stageArtworkState.variant : null), | |
| 16 | })); | |
| 17 | ||
| 18 | import { ComposerPrimaryActions, formatPendingPrimaryActionLabel } from "./ComposerPrimaryActions"; | |
| 19 | ||
| 20 | function renderPendingActions(isRunning: boolean) { | |
| 21 | return renderToStaticMarkup( | |
| 22 | createElement(ComposerPrimaryActions, { | |
| 23 | compact: true, | |
| 24 | pendingAction: { | |
| 25 | questionIndex: 0, | |
| 26 | isLastQuestion: true, | |
| 27 | canAdvance: true, | |
| 28 | isResponding: false, | |
| 29 | isComplete: true, | |
| 30 | }, | |
| 31 | isRunning, | |
| 32 | showPlanFollowUpPrompt: false, | |
| 33 | promptHasText: false, | |
| 34 | isSendBusy: false, | |
| 35 | sendDisabledReason: null, | |
| 36 | isConnecting: false, | |
| 37 | isEnvironmentUnavailable: false, | |
| 38 | isPreparingWorktree: false, | |
| 39 | hasSendableContent: false, | |
| 40 | onPreviousPendingQuestion: () => {}, | |
| 41 | onInterrupt: () => {}, | |
| 42 | onImplementPlanInNewThread: () => {}, | |
| 43 | }), | |
| 44 | ); | |
| 45 | } | |
| 46 | ||
| 47 | function renderStandaloneStop() { | |
| 48 | return renderToStaticMarkup( | |
| 49 | createElement(ComposerPrimaryActions, { | |
| 50 | compact: true, | |
| 51 | pendingAction: null, | |
| 52 | isRunning: true, | |
| 53 | showPlanFollowUpPrompt: false, | |
| 54 | promptHasText: false, | |
| 55 | isSendBusy: false, | |
| 56 | sendDisabledReason: null, | |
| 57 | isConnecting: false, | |
| 58 | isEnvironmentUnavailable: false, | |
| 59 | isPreparingWorktree: false, | |
| 60 | hasSendableContent: false, | |
| 61 | onPreviousPendingQuestion: () => {}, | |
| 62 | onInterrupt: () => {}, | |
| 63 | onImplementPlanInNewThread: () => {}, | |
| 64 | }), | |
| 65 | ); | |
| 66 | } | |
| 67 | ||
| 68 | function renderSendButton() { | |
| 69 | return renderToStaticMarkup( | |
| 70 | createElement(ComposerPrimaryActions, { | |
| 71 | compact: true, | |
| 72 | pendingAction: null, | |
| 73 | isRunning: false, | |
| 74 | showPlanFollowUpPrompt: false, | |
| 75 | promptHasText: true, | |
| 76 | isSendBusy: false, | |
| 77 | sendDisabledReason: null, | |
| 78 | isConnecting: false, | |
| 79 | isEnvironmentUnavailable: false, | |
| 80 | isPreparingWorktree: false, | |
| 81 | hasSendableContent: true, | |
| 82 | onPreviousPendingQuestion: () => {}, | |
| 83 | onInterrupt: () => {}, | |
| 84 | onImplementPlanInNewThread: () => {}, | |
| 85 | }), | |
| 86 | ); | |
| 87 | } | |
| 88 | ||
| 89 | afterEach(() => { | |
| 90 | stageArtworkState.mode = "none"; | |
| 91 | stageArtworkState.variant = null; | |
| 92 | }); | |
| 93 | ||
| 94 | describe("formatPendingPrimaryActionLabel", () => { | |
| 95 | it("returns 'Submitting...' while responding", () => { | |
| 96 | expect( | |
| 97 | formatPendingPrimaryActionLabel({ | |
| 98 | compact: false, | |
| 99 | isLastQuestion: false, | |
| 100 | isResponding: true, | |
| 101 | questionIndex: 0, | |
| 102 | }), | |
| 103 | ).toBe("Submitting..."); | |
| 104 | }); | |
| 105 | ||
| 106 | it("returns 'Submitting...' while responding regardless of other flags", () => { | |
| 107 | expect( | |
| 108 | formatPendingPrimaryActionLabel({ | |
| 109 | compact: true, | |
| 110 | isLastQuestion: true, | |
| 111 | isResponding: true, | |
| 112 | questionIndex: 3, | |
| 113 | }), | |
| 114 | ).toBe("Submitting..."); | |
| 115 | }); | |
| 116 | ||
| 117 | it("returns 'Submit' in compact mode on the last question", () => { | |
| 118 | expect( | |
| 119 | formatPendingPrimaryActionLabel({ | |
| 120 | compact: true, | |
| 121 | isLastQuestion: true, | |
| 122 | isResponding: false, | |
| 123 | questionIndex: 0, | |
| 124 | }), | |
| 125 | ).toBe("Submit"); | |
| 126 | }); | |
| 127 | ||
| 128 | it("returns 'Next' in compact mode when not the last question", () => { | |
| 129 | expect( | |
| 130 | formatPendingPrimaryActionLabel({ | |
| 131 | compact: true, | |
| 132 | isLastQuestion: false, | |
| 133 | isResponding: false, | |
| 134 | questionIndex: 1, | |
| 135 | }), | |
| 136 | ).toBe("Next"); | |
| 137 | }); | |
| 138 | ||
| 139 | it("returns 'Next question' when not the last question", () => { | |
| 140 | expect( | |
| 141 | formatPendingPrimaryActionLabel({ | |
| 142 | compact: false, | |
| 143 | isLastQuestion: false, | |
| 144 | isResponding: false, | |
| 145 | questionIndex: 0, | |
| 146 | }), | |
| 147 | ).toBe("Next question"); | |
| 148 | }); | |
| 149 | ||
| 150 | it("returns singular 'Submit answer' on the last question when it is the only question", () => { | |
| 151 | expect( | |
| 152 | formatPendingPrimaryActionLabel({ | |
| 153 | compact: false, | |
| 154 | isLastQuestion: true, | |
| 155 | isResponding: false, | |
| 156 | questionIndex: 0, | |
| 157 | }), | |
| 158 | ).toBe("Submit answer"); | |
| 159 | }); | |
| 160 | ||
| 161 | it("returns plural 'Submit answers' on the last question when there are multiple questions", () => { | |
| 162 | expect( | |
| 163 | formatPendingPrimaryActionLabel({ | |
| 164 | compact: false, | |
| 165 | isLastQuestion: true, | |
| 166 | isResponding: false, | |
| 167 | questionIndex: 1, | |
| 168 | }), | |
| 169 | ).toBe("Submit answers"); | |
| 170 | }); | |
| 171 | ||
| 172 | it("returns plural 'Submit answers' for higher question indices", () => { | |
| 173 | expect( | |
| 174 | formatPendingPrimaryActionLabel({ | |
| 175 | compact: false, | |
| 176 | isLastQuestion: true, | |
| 177 | isResponding: false, | |
| 178 | questionIndex: 5, | |
| 179 | }), | |
| 180 | ).toBe("Submit answers"); | |
| 181 | }); | |
| 182 | }); | |
| 183 | ||
| 184 | describe("ComposerPrimaryActions", () => { | |
| 185 | it("offers Stop generation while a running turn is waiting for user input", () => { | |
| 186 | expect(renderPendingActions(true)).toContain('aria-label="Stop generation"'); | |
| 187 | }); | |
| 188 | ||
| 189 | it("does not offer Stop generation for a pending request without a running turn", () => { | |
| 190 | expect(renderPendingActions(false)).not.toContain('aria-label="Stop generation"'); | |
| 191 | }); | |
| 192 | ||
| 193 | it("matches the small pending action size without changing the standalone size", () => { | |
| 194 | expect(renderPendingActions(true)).toContain("size-8 sm:size-7"); | |
| 195 | expect(renderStandaloneStop()).toContain("size-8 sm:h-8 sm:w-8"); | |
| 196 | expect(renderStandaloneStop()).not.toContain("sm:size-7"); | |
| 197 | }); | |
| 198 | ||
| 199 | it("renders stage artwork inside the send button when artwork identification is active", () => { | |
| 200 | stageArtworkState.mode = "artwork"; | |
| 201 | stageArtworkState.variant = "nightly"; | |
| 202 | ||
| 203 | const markup = renderSendButton(); | |
| 204 | ||
| 205 | expect(markup).toContain("stage-nightly"); | |
| 206 | expect(markup).toContain("bg-transparent text-white"); | |
| 207 | expect(markup).not.toContain("bg-message-action text-message-action-foreground"); | |
| 208 | }); | |
| 209 | ||
| 210 | it("keeps the normal send-button fill when artwork identification is inactive", () => { | |
| 211 | stageArtworkState.variant = "nightly"; | |
| 212 | ||
| 213 | const markup = renderSendButton(); | |
| 214 | ||
| 215 | expect(markup).not.toContain("stage-nightly"); | |
| 216 | expect(markup).toContain("bg-message-action text-message-action-foreground"); | |
| 217 | }); | |
| 218 | }); |
apps/web/src/components/chat/ComposerPrimaryActions.test.tsx created+263| ... | ... | @@ -0,0 +1,263 @@ |
| 1 | import { createElement } from "react"; | |
| 2 | import { renderToStaticMarkup } from "react-dom/server"; | |
| 3 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; | |
| 4 | ||
| 5 | const stageArtworkState = vi.hoisted(() => ({ | |
| 6 | mode: "none" as "artwork" | "none", | |
| 7 | variant: null as "nightly" | "dev" | null, | |
| 8 | })); | |
| 9 | ||
| 10 | vi.mock("~/hooks/useSettings", () => ({ | |
| 11 | useEnvironmentIdentificationMode: () => stageArtworkState.mode, | |
| 12 | })); | |
| 13 | vi.mock("../SidebarStageBackdrop", () => ({ | |
| 14 | StageBackdropButtonArt: ({ variant }: { variant: string }) => `stage-${variant}`, | |
| 15 | useSidebarStageBackdropVariant: (enabled = true) => (enabled ? stageArtworkState.variant : null), | |
| 16 | })); | |
| 17 | ||
| 18 | import { ComposerPrimaryActions, formatPendingPrimaryActionLabel } from "./ComposerPrimaryActions"; | |
| 19 | ||
| 20 | function renderPendingActions(isRunning: boolean) { | |
| 21 | return renderToStaticMarkup( | |
| 22 | createElement(ComposerPrimaryActions, { | |
| 23 | compact: true, | |
| 24 | pendingAction: { | |
| 25 | questionIndex: 0, | |
| 26 | isLastQuestion: true, | |
| 27 | canAdvance: true, | |
| 28 | isResponding: false, | |
| 29 | isComplete: true, | |
| 30 | }, | |
| 31 | isRunning, | |
| 32 | showPlanFollowUpPrompt: false, | |
| 33 | promptHasText: false, | |
| 34 | isSendBusy: false, | |
| 35 | sendDisabledReason: null, | |
| 36 | isConnecting: false, | |
| 37 | isEnvironmentUnavailable: false, | |
| 38 | isPreparingWorktree: false, | |
| 39 | hasSendableContent: false, | |
| 40 | onPreviousPendingQuestion: () => {}, | |
| 41 | onInterrupt: () => {}, | |
| 42 | onImplementPlanInNewThread: () => {}, | |
| 43 | }), | |
| 44 | ); | |
| 45 | } | |
| 46 | ||
| 47 | function renderStandaloneStop() { | |
| 48 | return renderToStaticMarkup( | |
| 49 | createElement(ComposerPrimaryActions, { | |
| 50 | compact: true, | |
| 51 | pendingAction: null, | |
| 52 | isRunning: true, | |
| 53 | showPlanFollowUpPrompt: false, | |
| 54 | promptHasText: false, | |
| 55 | isSendBusy: false, | |
| 56 | sendDisabledReason: null, | |
| 57 | isConnecting: false, | |
| 58 | isEnvironmentUnavailable: false, | |
| 59 | isPreparingWorktree: false, | |
| 60 | hasSendableContent: false, | |
| 61 | onPreviousPendingQuestion: () => {}, | |
| 62 | onInterrupt: () => {}, | |
| 63 | onImplementPlanInNewThread: () => {}, | |
| 64 | }), | |
| 65 | ); | |
| 66 | } | |
| 67 | ||
| 68 | function renderRunningActions(showSendWhileRunning: boolean, hasSendableContent: boolean) { | |
| 69 | return renderToStaticMarkup( | |
| 70 | createElement(ComposerPrimaryActions, { | |
| 71 | compact: true, | |
| 72 | pendingAction: null, | |
| 73 | isRunning: true, | |
| 74 | showPlanFollowUpPrompt: false, | |
| 75 | promptHasText: hasSendableContent, | |
| 76 | isSendBusy: false, | |
| 77 | sendDisabledReason: null, | |
| 78 | isConnecting: false, | |
| 79 | isEnvironmentUnavailable: false, | |
| 80 | isPreparingWorktree: false, | |
| 81 | hasSendableContent, | |
| 82 | showSendWhileRunning, | |
| 83 | onPreviousPendingQuestion: () => {}, | |
| 84 | onInterrupt: () => {}, | |
| 85 | onImplementPlanInNewThread: () => {}, | |
| 86 | }), | |
| 87 | ); | |
| 88 | } | |
| 89 | ||
| 90 | function renderSendButton() { | |
| 91 | return renderToStaticMarkup( | |
| 92 | createElement(ComposerPrimaryActions, { | |
| 93 | compact: true, | |
| 94 | pendingAction: null, | |
| 95 | isRunning: false, | |
| 96 | showPlanFollowUpPrompt: false, | |
| 97 | promptHasText: true, | |
| 98 | isSendBusy: false, | |
| 99 | sendDisabledReason: null, | |
| 100 | isConnecting: false, | |
| 101 | isEnvironmentUnavailable: false, | |
| 102 | isPreparingWorktree: false, | |
| 103 | hasSendableContent: true, | |
| 104 | onPreviousPendingQuestion: () => {}, | |
| 105 | onInterrupt: () => {}, | |
| 106 | onImplementPlanInNewThread: () => {}, | |
| 107 | }), | |
| 108 | ); | |
| 109 | } | |
| 110 | ||
| 111 | afterEach(() => { | |
| 112 | stageArtworkState.mode = "none"; | |
| 113 | stageArtworkState.variant = null; | |
| 114 | }); | |
| 115 | ||
| 116 | describe("formatPendingPrimaryActionLabel", () => { | |
| 117 | it("returns 'Submitting...' while responding", () => { | |
| 118 | expect( | |
| 119 | formatPendingPrimaryActionLabel({ | |
| 120 | compact: false, | |
| 121 | isLastQuestion: false, | |
| 122 | isResponding: true, | |
| 123 | questionIndex: 0, | |
| 124 | }), | |
| 125 | ).toBe("Submitting..."); | |
| 126 | }); | |
| 127 | ||
| 128 | it("returns 'Submitting...' while responding regardless of other flags", () => { | |
| 129 | expect( | |
| 130 | formatPendingPrimaryActionLabel({ | |
| 131 | compact: true, | |
| 132 | isLastQuestion: true, | |
| 133 | isResponding: true, | |
| 134 | questionIndex: 3, | |
| 135 | }), | |
| 136 | ).toBe("Submitting..."); | |
| 137 | }); | |
| 138 | ||
| 139 | it("returns 'Submit' in compact mode on the last question", () => { | |
| 140 | expect( | |
| 141 | formatPendingPrimaryActionLabel({ | |
| 142 | compact: true, | |
| 143 | isLastQuestion: true, | |
| 144 | isResponding: false, | |
| 145 | questionIndex: 0, | |
| 146 | }), | |
| 147 | ).toBe("Submit"); | |
| 148 | }); | |
| 149 | ||
| 150 | it("returns 'Next' in compact mode when not the last question", () => { | |
| 151 | expect( | |
| 152 | formatPendingPrimaryActionLabel({ | |
| 153 | compact: true, | |
| 154 | isLastQuestion: false, | |
| 155 | isResponding: false, | |
| 156 | questionIndex: 1, | |
| 157 | }), | |
| 158 | ).toBe("Next"); | |
| 159 | }); | |
| 160 | ||
| 161 | it("returns 'Next question' when not the last question", () => { | |
| 162 | expect( | |
| 163 | formatPendingPrimaryActionLabel({ | |
| 164 | compact: false, | |
| 165 | isLastQuestion: false, | |
| 166 | isResponding: false, | |
| 167 | questionIndex: 0, | |
| 168 | }), | |
| 169 | ).toBe("Next question"); | |
| 170 | }); | |
| 171 | ||
| 172 | it("returns singular 'Submit answer' on the last question when it is the only question", () => { | |
| 173 | expect( | |
| 174 | formatPendingPrimaryActionLabel({ | |
| 175 | compact: false, | |
| 176 | isLastQuestion: true, | |
| 177 | isResponding: false, | |
| 178 | questionIndex: 0, | |
| 179 | }), | |
| 180 | ).toBe("Submit answer"); | |
| 181 | }); | |
| 182 | ||
| 183 | it("returns plural 'Submit answers' on the last question when there are multiple questions", () => { | |
| 184 | expect( | |
| 185 | formatPendingPrimaryActionLabel({ | |
| 186 | compact: false, | |
| 187 | isLastQuestion: true, | |
| 188 | isResponding: false, | |
| 189 | questionIndex: 1, | |
| 190 | }), | |
| 191 | ).toBe("Submit answers"); | |
| 192 | }); | |
| 193 | ||
| 194 | it("returns plural 'Submit answers' for higher question indices", () => { | |
| 195 | expect( | |
| 196 | formatPendingPrimaryActionLabel({ | |
| 197 | compact: false, | |
| 198 | isLastQuestion: true, | |
| 199 | isResponding: false, | |
| 200 | questionIndex: 5, | |
| 201 | }), | |
| 202 | ).toBe("Submit answers"); | |
| 203 | }); | |
| 204 | }); | |
| 205 | ||
| 206 | describe("ComposerPrimaryActions", () => { | |
| 207 | it("offers Stop generation while a running turn is waiting for user input", () => { | |
| 208 | expect(renderPendingActions(true)).toContain('aria-label="Stop generation"'); | |
| 209 | }); | |
| 210 | ||
| 211 | it("does not offer Stop generation for a pending request without a running turn", () => { | |
| 212 | expect(renderPendingActions(false)).not.toContain('aria-label="Stop generation"'); | |
| 213 | }); | |
| 214 | ||
| 215 | it("matches the small pending action size without changing the standalone size", () => { | |
| 216 | expect(renderPendingActions(true)).toContain("size-8 sm:size-7"); | |
| 217 | expect(renderStandaloneStop()).toContain("size-8 sm:h-8 sm:w-8"); | |
| 218 | expect(renderStandaloneStop()).not.toContain("sm:size-7"); | |
| 219 | }); | |
| 220 | ||
| 221 | it("renders stage artwork inside the send button when artwork identification is active", () => { | |
| 222 | stageArtworkState.mode = "artwork"; | |
| 223 | stageArtworkState.variant = "nightly"; | |
| 224 | ||
| 225 | const markup = renderSendButton(); | |
| 226 | ||
| 227 | expect(markup).toContain("stage-nightly"); | |
| 228 | expect(markup).toContain("bg-transparent text-white"); | |
| 229 | expect(markup).not.toContain("bg-message-action text-message-action-foreground"); | |
| 230 | }); | |
| 231 | ||
| 232 | it("keeps the normal send-button fill when artwork identification is inactive", () => { | |
| 233 | stageArtworkState.variant = "nightly"; | |
| 234 | ||
| 235 | const markup = renderSendButton(); | |
| 236 | ||
| 237 | expect(markup).not.toContain("stage-nightly"); | |
| 238 | expect(markup).toContain("bg-message-action text-message-action-foreground"); | |
| 239 | }); | |
| 240 | ||
| 241 | it("only renders stop while running when Enter-to-send is available", () => { | |
| 242 | const markup = renderRunningActions(false, true); | |
| 243 | ||
| 244 | expect(markup).toContain('aria-label="Stop generation"'); | |
| 245 | expect(markup).not.toContain('aria-label="Send message"'); | |
| 246 | }); | |
| 247 | ||
| 248 | it("renders send alongside stop while running when Enter-to-send is unavailable", () => { | |
| 249 | const markup = renderRunningActions(true, true); | |
| 250 | ||
| 251 | expect(markup).toContain('aria-label="Stop generation"'); | |
| 252 | expect(markup).toContain('aria-label="Send message"'); | |
| 253 | expect(markup).toContain('type="submit"'); | |
| 254 | expect(markup).toContain("size-9 sm:size-8"); | |
| 255 | }); | |
| 256 | ||
| 257 | it("keeps stop as the only action while running with an empty composer", () => { | |
| 258 | const markup = renderRunningActions(true, false); | |
| 259 | ||
| 260 | expect(markup).toContain('aria-label="Stop generation"'); | |
| 261 | expect(markup).not.toContain('aria-label="Send message"'); | |
| 262 | }); | |
| 263 | }); |
apps/web/src/components/chat/ComposerPrimaryActions.tsx+21-6| ... | ... | @@ -28,6 +28,9 @@ interface ComposerPrimaryActionsProps { |
| 28 | 28 | isPreparingWorktree: boolean; |
| 29 | 29 | hasSendableContent: boolean; |
| 30 | 30 | preserveComposerFocusOnPointerDown?: boolean; |
| 31 | /** Enter-to-send is disabled on mobile viewports, where stop would otherwise | |
| 32 | * be the only primary action and a running turn could not be steered. */ | |
| 33 | showSendWhileRunning?: boolean; | |
| 31 | 34 | onPreviousPendingQuestion: () => void; |
| 32 | 35 | onInterrupt: () => void; |
| 33 | 36 | onImplementPlanInNewThread: () => void; |
| ... | ... | @@ -68,6 +71,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ |
| 68 | 71 | isPreparingWorktree, |
| 69 | 72 | hasSendableContent, |
| 70 | 73 | preserveComposerFocusOnPointerDown = false, |
| 74 | showSendWhileRunning = false, | |
| 71 | 75 | onPreviousPendingQuestion, |
| 72 | 76 | onInterrupt, |
| 73 | 77 | onImplementPlanInNewThread, |
| ... | ... | @@ -86,7 +90,11 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ |
| 86 | 90 | type="button" |
| 87 | 91 | className={cn( |
| 88 | 92 | "flex cursor-pointer items-center justify-center rounded-full bg-destructive/90 text-white shadow-xs shadow-destructive/24 inset-shadow-[0_1px_--theme(--color-white/16%)] transition-all duration-150 hover:bg-destructive hover:scale-105 active:inset-shadow-[0_1px_--theme(--color-black/8%)] active:shadow-none", |
| 89 | insidePendingAction ? "size-8 sm:size-7" : "size-8 sm:h-8 sm:w-8", | |
| 93 | insidePendingAction | |
| 94 | ? "size-8 sm:size-7" | |
| 95 | : showSendWhileRunning && hasSendableContent | |
| 96 | ? "size-9 sm:size-8" | |
| 97 | : "size-8 sm:h-8 sm:w-8", | |
| 90 | 98 | )} |
| 91 | 99 | {...pointerFocusProps} |
| 92 | 100 | onClick={onInterrupt} |
| ... | ... | @@ -153,10 +161,6 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ |
| 153 | 161 | ); |
| 154 | 162 | } |
| 155 | 163 | |
| 156 | if (isRunning) { | |
| 157 | return renderStopGenerationButton(false); | |
| 158 | } | |
| 159 | ||
| 160 | 164 | if (showPlanFollowUpPrompt) { |
| 161 | 165 | if (promptHasText) { |
| 162 | 166 | return ( |
| ... | ... | @@ -214,7 +218,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ |
| 214 | 218 | ); |
| 215 | 219 | } |
| 216 | 220 | |
| 217 | return ( | |
| 221 | const sendButton = ( | |
| 218 | 222 | <button |
| 219 | 223 | type="submit" |
| 220 | 224 | className={cn( |
| ... | ... | @@ -265,4 +269,15 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ |
| 265 | 269 | )} |
| 266 | 270 | </button> |
| 267 | 271 | ); |
| 272 | ||
| 273 | if (!isRunning) { | |
| 274 | return sendButton; | |
| 275 | } | |
| 276 | ||
| 277 | return ( | |
| 278 | <> | |
| 279 | {renderStopGenerationButton(false)} | |
| 280 | {showSendWhileRunning && hasSendableContent ? sendButton : null} | |
| 281 | </> | |
| 282 | ); | |
| 268 | 283 | }); |