Skip to content
Merged
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
18 changes: 13 additions & 5 deletions packages/app-bundle/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -1073,7 +1077,7 @@
"extracted_at": "2026-10-29T13:09:53.406Z",
"per_package": {
"packages/app": {
"A": 289,
"A": 290,
"M": 166,
"D": 0
},
Expand Down Expand Up @@ -1108,7 +1112,7 @@
"D": 0
},
"packages/session-ui": {
"A": 49,
"A": 52,
"M": 32,
"D": 0
},
Expand All @@ -1119,7 +1123,7 @@
}
},
"counts": {
"overlay_total": 1048,
"overlay_total": 1052,
"deletions": 7,
"server_coupled": 0
},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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("<ComposerResizeHandle")
expect(v1).toContain("createComposerResize(")
expect(v1).toContain("resize.height()")
})

test("the v2 composer mounts the handle and pins the editor height from it", () => {
expect(v2).toContain("<ComposerResizeHandle")
expect(v2).toContain("createComposerResize(")
expect(v2).toContain("composerHeightStyle(resize.height())")
})

test("the handle is an accessible, keyboard-operable horizontal separator", () => {
expect(handle).toContain('role="separator"')
expect(handle).toContain('aria-orientation="horizontal"')
expect(handle).toContain('tabindex="0"')
expect(handle).toContain("onKeyDown")
expect(handle).toContain("onDblClick")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -134,9 +135,17 @@ export const PromptInput: Component<PromptInputProps> = (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
Expand Down Expand Up @@ -1482,6 +1491,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
[props.class ?? ""]: !!props.class,
}}
>
<ComposerResizeHandle resize={resize} gripClass="bg-text-weak" />
<PromptDragOverlay
type={store.draggingType}
label={language.t(store.draggingType === "@mention" ? "prompt.dropzone.file.label" : "prompt.dropzone.label")}
Expand Down Expand Up @@ -1524,7 +1534,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<div
class="relative max-h-[240px] overflow-y-auto no-scrollbar"
ref={(el) => (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`,
}),
}}
>
<div
data-component="prompt-input"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
PromptInputV2Suggestion,
} from "./types"
import type { PromptInputV2Interaction, PromptInputV2SelectControl } from "./interaction"
import { ComposerResizeHandle, composerHeightStyle, createComposerResize } from "./resize-handle"
import "./attachments.css"

export type {
Expand Down Expand Up @@ -75,6 +76,9 @@ export function PromptInputV2(props: PromptInputV2Props) {
const view = props.controller.view
let editor: HTMLDivElement | undefined
let localInput = false
// amicode#612: drag the form's top edge to make the text area taller. The
// chosen height persists (localStorage) across sessions; unset = stock sizing.
const resize = createComposerResize(() => editor?.getBoundingClientRect().height)
const updateCursor = () => {
if (!editor || !window.getSelection()?.isCollapsed) return
props.controller.onCursor(promptInputV2Cursor(editor))
Expand Down Expand Up @@ -147,6 +151,8 @@ export function PromptInputV2(props: PromptInputV2Props) {
onDragLeave={props.controller.onDragLeave}
onDrop={props.controller.onDrop}
>
<ComposerResizeHandle resize={resize} gripClass="bg-v2-text-text-faint" />

<Show when={state.drag === "active"}>
<div class="pointer-events-none absolute inset-0 z-20 grid place-items-center rounded-xl bg-v2-background-bg-base/90 text-v2-text-text-base">
Drop files to attach
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<number | undefined>
resizer: ComposerResizer
}

export function createComposerResize(measure: () => number | undefined): ComposerResize {
const [height, setHeight] = createSignal<number | undefined>(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 (
<div
data-component="composer-resize-handle"
role="separator"
aria-orientation="horizontal"
aria-label={props.label ?? "Resize message box"}
aria-valuemin={COMPOSER_MIN_HEIGHT}
aria-valuemax={composerMaxHeight(typeof window === "undefined" ? 800 : window.innerHeight)}
aria-valuenow={props.resize.height()}
tabindex="0"
title="Drag to resize · double-click to reset"
class="group/resize absolute inset-x-0 top-0 z-30 flex h-2 cursor-ns-resize touch-none select-none items-start justify-center outline-none"
onPointerDown={(event) => {
if (event.button !== 0) return
event.preventDefault()
event.currentTarget.setPointerCapture(event.pointerId)
resizer().begin(event.clientY)
setDragging(true)
}}
onPointerMove={(event) => resizer().move(event.clientY)}
onPointerUp={finish}
onPointerCancel={finish}
onLostPointerCapture={finish}
onDblClick={() => resizer().reset()}
onKeyDown={(event) => {
if (resizer().key(event.key)) event.preventDefault()
}}
>
<div
class={`mt-0.5 h-1 w-10 rounded-full transition-opacity ${props.gripClass ?? "bg-current"}`}
classList={{
"opacity-0": !dragging(),
"group-hover/resize:opacity-40 group-focus-visible/resize:opacity-40": !dragging(),
"opacity-60": dragging(),
}}
/>
</div>
)
}
Loading
Loading