fix(cloud): CHAT-RENEW the held hint rides the structured transport into the composer send state (keeps NativeChatComposerPane under max-lines)

This commit is contained in:
Neil
2026-11-13 06:10:04 -08:00
parent b2f47a8663
commit 659c354d0f
7 changed files with 24 additions and 10 deletions
@@ -88,7 +88,7 @@ const props = {
tabId: TAB_ID
} as const
function composer(sendBlockedReason: string | null, draft: string) {
function composer(sendBlockedReason: string | null, draft: string, heldHint?: string) {
return nativeChatComposerSendState({
agent: 'claude',
draft,
@@ -99,7 +99,8 @@ function composer(sendBlockedReason: string | null, draft: string) {
structuredTransport: {
sendOut: false,
optionSnapshot: [],
...(sendBlockedReason !== null ? { sendBlockedReason } : {})
...(sendBlockedReason !== null ? { sendBlockedReason } : {}),
...(heldHint ? { heldHint } : {})
},
editSession: undefined,
isWorking: false,
@@ -167,10 +168,12 @@ describe('a running idle chat whose strip is expired holds Send with its hint (E
expect(slot.heldHint).toBe(HINT)
expect(slot.sendBlockedReason).toBe(HINT)
for (const draft of ['', 'continue please']) {
const state = composer(slot.sendBlockedReason, draft)
const state = composer(slot.sendBlockedReason, draft, slot.heldHint)
expect(state.sendButtonDisabled).toBe(true)
expect(state.sendHeld).toBe(true)
expect(state.sendBlockedReason).toBe(HINT)
// The composer field shows it as the empty field's placeholder.
expect(state.heldHint).toBe(HINT)
}
})
}
@@ -217,7 +217,8 @@ export function NativeChatStructuredSession(props: Omit<NativeChatStructuredView
setOptionPickerRequest,
onError: reportComposerError,
reveal: submits,
sendBlockedReason: hostSendGate.sendBlockedReason ?? install.sendBlockedReason
sendBlockedReason: hostSendGate.sendBlockedReason ?? install.sendBlockedReason,
...(hostHeld || !install.heldHint ? {} : { heldHint: install.heldHint })
})
const continuation = useNativeChatInterruptedContinuation({
...controller,
@@ -393,7 +394,6 @@ export function NativeChatStructuredSession(props: Omit<NativeChatStructuredView
agent={props.agent}
{...stopControls.composer}
unavailableHint={install.unavailableHint}
heldHint={hostHeld ? undefined : install.heldHint}
steerHead={stopControls.stopping ? undefined : submits.queuedMessages.steerHead}
structuredTransport={transport}
notices={notices}
@@ -42,7 +42,6 @@ export const NativeChatComposerPane = forwardRef<ComposerHandle, C.NativeChatCom
agent,
canSend = true,
unavailableHint,
heldHint,
isWorking = false,
isStopping = false,
afterStop,
@@ -367,7 +366,6 @@ export const NativeChatComposerPane = forwardRef<ComposerHandle, C.NativeChatCom
hasPty={hasPty}
canSend={canSend}
unavailableHint={unavailableHint}
heldHint={heldHint}
autocomplete={autocomplete}
mentionFiles={mentionFiles}
activeSuggestion={activeSuggestion}
@@ -24,8 +24,6 @@ export type NativeChatComposerProps = {
canSend?: boolean
/** Disable unavailable input and show this reason as its placeholder. */
unavailableHint?: string | undefined
/** Input stays open but the transport holds Send: show this reason as the empty field's placeholder. */
heldHint?: string | undefined
/** True while the hosted TUI reports an in-flight turn; swaps Send to Stop. */
isWorking?: boolean
/** This client's Stop request is in flight: the Stop control is disabled and says so. */
@@ -12,6 +12,8 @@ import type { Translate } from '../../../../i18n/use-translate'
export type NativeChatComposerSendState = {
/** Why Send is disabled, when the user can do something about it. */
sendBlockedReason: string | null
/** A host hold's placeholder for the empty field (typing stays open). */
heldHint?: string
/** Nothing may be sent yet: an upload, a paste being saved, or the host holds it. */
sendHeld: boolean
/** The draft holds text or images: while the agent works, Send takes Stop's place. */
@@ -35,7 +37,12 @@ export function nativeChatComposerSendState(
structuredTransport:
| Pick<
NativeChatStructuredComposerTransport,
'sendOut' | 'sendBlockedReason' | 'threadGoal' | 'sendFollowUp' | 'optionSnapshot'
| 'sendOut'
| 'sendBlockedReason'
| 'heldHint'
| 'threadGoal'
| 'sendFollowUp'
| 'optionSnapshot'
>
| undefined
editSession: { canSave: boolean } | undefined
@@ -90,6 +97,7 @@ export function nativeChatComposerSendState(
imageBlock.reason ??
(hostHold && !runsWhileHostHolds ? transport?.sendBlockedReason : null) ??
null,
...(transport?.heldHint ? { heldHint: transport.heldHint } : {}),
sendHeld,
hasDraft,
sendButtonDisabled,
@@ -27,6 +27,8 @@ export type NativeChatStructuredComposerTransport = {
sendOut?: boolean
/** Send is held for this reason, which the disabled button says. */
sendBlockedReason?: string
/** Typing stays open while Send is held: the empty field's placeholder says why. */
heldHint?: string
/** What a plain send does now, and whether Cmd/Ctrl+Enter would send it the other way. */
sendFollowUp?: NativeChatSendFollowUp
/** `onSent` fires once a conversation command leaves for the host, before its reply. */
@@ -85,6 +85,8 @@ export function useNativeChatStructuredComposerTransport(args: {
reveal: { revealLatest: () => void; captureRevealLatest: () => () => void }
/** Host reason for refusing turns and holding Send. */
sendBlockedReason?: string | null
/** END-PASS CHAT-RENEW: the empty field's placeholder while Send is held (typing stays open). */
heldHint?: string
}): NativeChatStructuredComposerTransport {
const { props, controller, forks, sendThroughLaunch, starting, worktreeId, optionPickerRequest } =
args
@@ -92,6 +94,7 @@ export function useNativeChatStructuredComposerTransport(args: {
const { revealLatest: onSubmitted, captureRevealLatest: captureAdmittedReveal } = args.reveal
const { transportEnabled } = args
const sendBlockedReason = args.sendBlockedReason ?? null
const { heldHint } = args
const acceptsImages = useStructuredAgentAcceptsImages(
props.target,
props.agent,
@@ -176,6 +179,7 @@ export function useNativeChatStructuredComposerTransport(args: {
sendOut: controller.sendOut,
sendFollowUp: controller.sendFollowUp,
...(sendBlockedReason !== null ? { sendBlockedReason } : {}),
...(heldHint ? { heldHint } : {}),
optionsSurface: controller.optionSurface,
conversationCommands: controller.conversationCommands,
compactInstructions: controller.compactInstructions,
@@ -216,6 +220,7 @@ export function useNativeChatStructuredComposerTransport(args: {
props.sessionId,
props.target,
sendBlockedReason,
heldHint,
sendThroughLaunch,
setOptionPickerRequest,
starting,