diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index c4f0c7ffc..bde698ef1 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -152,7 +152,7 @@ "packages/app/src/components/profile-popover.tsx": "eff742e90544455f13234a9351bb1b82189663cba2cd3172bdcea1a8a37284e8", "packages/app/src/components/prompt-input-clipboard-structure.test.ts": "946b718c2164da394602f26e4d9919d752da0d0b9bee714f345ba16c274542da", "packages/app/src/components/prompt-input-v2.tsx": "f0b637e46737e3c7345957a0167c878c8e9870a15bb850057665a4444ea91fe8", - "packages/app/src/components/prompt-input.tsx": "c127f81e2c0de4bdbc21a349473df39c623d8d7e2df8df0c03ea60bbef101a25", + "packages/app/src/components/prompt-input.tsx": "6eaaba582ed28b73aa7273ea5e33dd0a0b81255b8b8db2c572e1f369ec14ae86", "packages/app/src/components/prompt-project-selector.tsx": "0157c954b938bce2b5aca9a82f4a72338e361f4c9027e55a7fd5a14a1654d294", "packages/app/src/components/question-inbox.tsx": "5ce03a30b7110092ddffccf3bf38e6c1b1a79624f378c819be7019725c96478a", "packages/app/src/components/remote-create-arm.test.ts": "388132209e16b6f00d79ac7c700f8c2bfb5f80343eee34cee73451b32842d3f2", @@ -194,6 +194,7 @@ "packages/app/src/components/vault-panel.tsx": "1d9f06f42b94ffb94a28bfd6d2693a36c75ceb969bcd434081b4cd2fbf99665c", "packages/app/src/components/windows-app-menu.tsx": "262d085facace692084d79763a6bd8eba6c5ceb53b952a134e12c7e8a8a12946", "packages/app/src/components/workbench-panel.tsx": "d16f477044098842a214a2e16f9be719e7688a04bcb575889ca544988f23c690", + "packages/app/src/components/prompt-input-resize-structure.test.ts": "38244b103702afc4b45cd40176f338c2a632d64b8ee9a719ee2073b6d7aaf062", "packages/app/src/context/command.tsx": "388d6cb12b6dc43ecb0827e3fb918370cb9d16d96161a5990dec10fff05af172", "packages/app/src/context/concise-mode.test.ts": "534258a1d82d66bd8b482bb3a8ca6a24d508328505abd0f7f52e1b75cdf57fa2", "packages/app/src/context/concise-mode.ts": "b03ac887222543e590d31781c944e69aca97bff32a661751dd817d46bf4ead64", @@ -1038,7 +1039,10 @@ "packages/sdk/js/src/v2/gen/sdk.gen.ts": "6610638b4fb71bac498da98d232ef9adcd4b011b63e5e81a63fc6dc20d311b17", "packages/sdk/js/src/v2/gen/types.gen.ts": "20dfc17423861bd51c197fe6a2fcb6efbd7ad11853b28e0bb79844115256e729", "packages/session-ui/src/v2/components/prompt-input/attachments.ts": "ff66078f2edc9a67a4df0d7dfd76a49be5eab27ac332b303f3412721ef7268ea", - "packages/session-ui/src/v2/components/prompt-input/index.tsx": "5c5c852ad55ae759f5869fd2e48ade6da99a98b6ab3c6dc102f3e062898e3f60", + "packages/session-ui/src/v2/components/prompt-input/index.tsx": "f393de5a7600796df174c877ffd5af64139df7fc65294c2bb9023cdc29945f66", + "packages/session-ui/src/v2/components/prompt-input/resize-handle.tsx": "e0c47cd2c8140eda79d96562d311c7c5e2ff9c162695691e9f6acd2a8c1786a1", + "packages/session-ui/src/v2/components/prompt-input/resize.test.ts": "c049ad1cd60e5ac4ec779dd37ab4b2c8a6244b5b6cbbc59240d1c43a449cfabd", + "packages/session-ui/src/v2/components/prompt-input/resize.ts": "bf1323b437bd0cc342084ac9d21035c8ea6d12fcdf644b5587fd88453638f83c", "packages/opencode/src/server/routes/instance/httpapi/server.ts": "8f142f16accad5a5a7ca84507f5c95863416af9f85b11131e5cd97cb6b128d90", "packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts": "9da41a900811e8526f3d6796341b787f7e3c64bdc9cc3a547977f05780c1f79f", "packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts": "944a0aee215a917e867fd9482f3c2bd40f49082bd3174c8a7d967ca40e34ec15", @@ -1073,7 +1077,7 @@ "extracted_at": "2026-10-29T13:09:53.406Z", "per_package": { "packages/app": { - "A": 289, + "A": 290, "M": 166, "D": 0 }, @@ -1108,7 +1112,7 @@ "D": 0 }, "packages/session-ui": { - "A": 49, + "A": 52, "M": 32, "D": 0 }, @@ -1119,7 +1123,7 @@ } }, "counts": { - "overlay_total": 1048, + "overlay_total": 1052, "deletions": 7, "server_coupled": 0 }, @@ -1310,6 +1314,7 @@ "packages/app/src/components/vault-panel.tsx": "A", "packages/app/src/components/windows-app-menu.tsx": "M", "packages/app/src/components/workbench-panel.tsx": "A", + "packages/app/src/components/prompt-input-resize-structure.test.ts": "A", "packages/app/src/context/command.tsx": "M", "packages/app/src/context/concise-mode.test.ts": "A", "packages/app/src/context/concise-mode.ts": "A", @@ -2155,6 +2160,9 @@ "packages/sdk/js/src/v2/gen/types.gen.ts": "M", "packages/session-ui/src/v2/components/prompt-input/attachments.ts": "M", "packages/session-ui/src/v2/components/prompt-input/index.tsx": "M", + "packages/session-ui/src/v2/components/prompt-input/resize-handle.tsx": "A", + "packages/session-ui/src/v2/components/prompt-input/resize.test.ts": "A", + "packages/session-ui/src/v2/components/prompt-input/resize.ts": "A", "packages/opencode/src/server/routes/instance/httpapi/server.ts": "M", "packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts": "M", "packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts": "M", diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input-resize-structure.test.ts b/packages/app-bundle/overlay/packages/app/src/components/prompt-input-resize-structure.test.ts new file mode 100644 index 000000000..8b9b961bf --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input-resize-structure.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "bun:test" +import { readFileSync } from "node:fs" +import { join } from "node:path" + +// amicode#612 — both composers mount the shared drag-to-resize handle and +// apply the persisted height to the text area. Source-structure checks, in the +// same style as prompt-input-clipboard-structure.test.ts: the behavior itself +// is covered in session-ui's resize.test.ts, this pins the wiring. +const v1 = readFileSync(join(import.meta.dir, "prompt-input.tsx"), "utf8") +const v2 = readFileSync( + join(import.meta.dir, "../../../session-ui/src/v2/components/prompt-input/index.tsx"), + "utf8", +) +const handle = readFileSync( + join(import.meta.dir, "../../../session-ui/src/v2/components/prompt-input/resize-handle.tsx"), + "utf8", +) + +describe("composer drag-to-resize wiring (#612)", () => { + test("the legacy composer mounts the handle and pins the scroller height from it", () => { + expect(v1).toContain(" { + expect(v2).toContain(" { + expect(handle).toContain('role="separator"') + expect(handle).toContain('aria-orientation="horizontal"') + expect(handle).toContain('tabindex="0"') + expect(handle).toContain("onKeyDown") + expect(handle).toContain("onDblClick") + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx b/packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx index 95002e2b6..fada4f546 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx @@ -56,6 +56,7 @@ import { usePlatform } from "@/context/platform" import { createSessionTabs } from "@/pages/session/helpers" import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom" import { createPromptAttachments } from "./prompt-input/attachments" +import { ComposerResizeHandle, createComposerResize } from "@opencode-ai/session-ui/v2/prompt-input/resize-handle" import { readClipboardImageViaBridge, readClipboardViaBridge } from "./prompt-input/clipboard-bridge" import { ACCEPTED_FILE_TYPES, pickAttachmentFiles } from "./prompt-input/files" import { @@ -134,9 +135,17 @@ export const PromptInput: Component = (props) => { let restoreEndOnFocus = true let savedCursor: number | null = null + // amicode#612: drag the composer's top edge to make the text area taller; + // shares behavior + persisted height with the v2 composer. The stored height + // is the TEXT area; this scroller also holds the 56px control-row inset below + // the text, so that is added when applying and removed when measuring. const mirror = { input: false } const inset = 56 const space = `${inset}px` + const resize = createComposerResize(() => { + const h = scrollRef?.getBoundingClientRect().height + return h === undefined ? undefined : h - inset + }) const scrollCursorIntoView = () => { const container = scrollRef @@ -1482,6 +1491,7 @@ export const PromptInput: Component = (props) => { [props.class ?? ""]: !!props.class, }} > + = (props) => {
(scrollRef = el)} - style={{ "scroll-padding-bottom": space }} + style={{ + "scroll-padding-bottom": space, + // Pins the text area to the chosen height (overrides max-h-[240px]); + // unset keeps the stock auto-grow. + ...(resize.height() === undefined + ? {} + : { + height: `${resize.height()! + inset}px`, + "max-height": `${resize.height()! + inset}px`, + }), + }} >
editor?.getBoundingClientRect().height) const updateCursor = () => { if (!editor || !window.getSelection()?.isCollapsed) return props.controller.onCursor(promptInputV2Cursor(editor)) @@ -147,6 +151,8 @@ export function PromptInputV2(props: PromptInputV2Props) { onDragLeave={props.controller.onDragLeave} onDrop={props.controller.onDrop} > + +
Drop files to attach @@ -185,6 +191,7 @@ export function PromptInputV2(props: PromptInputV2Props) { autocomplete="off" class="relative z-10 block min-h-[60px] max-h-[180px] w-full overflow-y-auto whitespace-pre-wrap bg-transparent px-4 pt-4 pb-2 text-[13px] font-[440] leading-5 text-v2-text-text-base focus:outline-none empty:before:content-['\200B'] [&_[data-mention=file]]:text-syntax-property [&_[data-mention=agent]]:text-syntax-type [&_[data-mention=reference]]:text-syntax-keyword" classList={{ "font-mono!": state.mode === "shell", "opacity-50": props.disabled }} + style={composerHeightStyle(resize.height())} onInput={(event) => { const cursor = promptInputV2Cursor(event.currentTarget) const prompt = parsePromptInputV2Editor(event.currentTarget) diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/resize-handle.tsx b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/resize-handle.tsx new file mode 100644 index 000000000..20baa81f7 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/resize-handle.tsx @@ -0,0 +1,97 @@ +import { createSignal, onCleanup, onMount, type Accessor } from "solid-js" +import { + COMPOSER_MIN_HEIGHT, + composerMaxHeight, + composerStorage, + createComposerResizer, + type ComposerResizer, +} from "./resize" + +// Solid glue for the composer drag-to-resize handle (amicode#612). Shared by +// the legacy `PromptInput` (app package) and `PromptInputV2`; all behavior +// lives in ./resize.ts, this file is signals + markup only. + +export type ComposerResize = { + /** User-chosen text-area height in px, or undefined for the stock auto height. */ + height: Accessor + resizer: ComposerResizer +} + +export function createComposerResize(measure: () => number | undefined): ComposerResize { + const [height, setHeight] = createSignal(undefined) + const viewport = () => (typeof window === "undefined" ? 800 : window.innerHeight) + const resizer = createComposerResizer({ + measure: () => measure() ?? COMPOSER_MIN_HEIGHT, + viewport, + storage: composerStorage(), + onChange: setHeight, + }) + setHeight(resizer.height()) + + onMount(() => { + const refit = () => resizer.refit() + window.addEventListener("resize", refit) + onCleanup(() => window.removeEventListener("resize", refit)) + }) + + return { height, resizer } +} + +/** Inline style that pins the text area to the user's chosen height. */ +export function composerHeightStyle(height: number | undefined) { + if (height === undefined) return undefined + return { "min-height": `${height}px`, "max-height": `${height}px` } +} + +export function ComposerResizeHandle(props: { + resize: ComposerResize + label?: string + /** Colour classes for the grip pill; the pill is only visible on hover/focus/drag. */ + gripClass?: string +}) { + const resizer = () => props.resize.resizer + const [dragging, setDragging] = createSignal(false) + const finish = () => { + resizer().end() + setDragging(false) + } + + return ( +