From f99a9329caccdd14d9c530777a5e5b622d34f602 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 16 Oct 2026 14:16:24 +0200 Subject: fix(assistant): keep focus in the card, scope errors, guard links From review of the canvas. Escape stopped working in three ways, each leaving the keyboard grabbed with nothing to receive it: the history field hiding took focus with it, a disabled input could not hold focus while the daemon was down, and a click in a streaming reply focused text the next flush destroyed. The card now gives focus back to the input whenever it falls out, and the input is read-only rather than disabled. Only an error the daemon marks as ending the reply (turn) closes the live row; a command error, possibly another client's, used to freeze the reply mid-stream. The running tool comes from state events, hello included. Scrolling up during a stream no longer snaps back, since following stops when a movement starts. History snippets go through Notify.sanitize like any model output, links open only for http, https and mailto, and a line opening with inline triple-backtick code is no longer taken as a fence. Co-Authored-By: Claude Opus 5.5 --- desktop/assistant/Card.qml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'desktop/assistant/Card.qml') diff --git a/desktop/assistant/Card.qml b/desktop/assistant/Card.qml index 543edd1..ab80d1b 100644 --- a/desktop/assistant/Card.qml +++ b/desktop/assistant/Card.qml @@ -34,6 +34,21 @@ Rectangle { Component.onCompleted: input.forceActiveFocus() + // Focus falls out of the card when its holder goes: the history field + // hiding, or a streaming reply's text rebuilt under a click. With the + // keyboard grabbed, nothing would then reach Escape. Give it back. + Connections { + target: card.Window.window + function onActiveFocusItemChanged() { + const f = card.Window.activeFocusItem; + if (!f || f === card.Window.contentItem) Qt.callLater(() => input.forceActiveFocus()); + } + } + Connections { + target: card.canvas + function onHistoryOpenChanged() { if (!card.canvas.historyOpen) input.forceActiveFocus(); } + } + // --- header --- Item { @@ -91,6 +106,7 @@ Rectangle { // Follow the bottom while a reply streams, unless the user scrolled // up to read; scrolling back to the end resumes following. property bool stick: true + onMovementStarted: stick = false onMovementEnded: stick = atYEnd onContentHeightChanged: if (stick) Qt.callLater(positionViewAtEnd) Component.onCompleted: positionViewAtEnd() @@ -153,7 +169,9 @@ Rectangle { width: scroller.width wrapMode: TextEdit.Wrap selectByMouse: true - enabled: card.canvas.connected + // readOnly, not disabled: a disabled item cannot hold the + // focus that Escape needs to reach the card. + readOnly: !card.canvas.connected color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } onCursorRectangleChanged: scroller.follow(cursorRectangle) -- cgit v1.2.3