From 236ac86ca09a24300e724c2372f3576bf3f6ff84 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 29 Sep 2026 20:06:24 +0200 Subject: fix: open a conversation's first message itself for --message --message naming the first message of a multi-message conversation selected the conversation row, so the pane showed the dashboard rather than the message. The recovery matched the conversation row on its own message id, which is the root's. The rule is that --message always targets the message, shown inside its expanded thread, and that --thread is the way to ask for the conversation. Since item 177 setThreadMessages keeps a conversation's first message as child 0, so the root already has a row of its own. applyPendingRecovery() now lets the thread row answer for a named message only when that row is not a conversation, which leaves a thread of one opening on its message as before, and finds the root among the children like any reply. The provisional pass still selects the thread while the tree loads and is refined to the root's row when the children arrive. The same recovery serves the stale notice and a double-click. A stale notice raised while reading the root's row now reopens that row, which is the same request. A double-click on a conversation row asks for the conversation, so it no longer names the root and still lands on the dashboard. recoveryOnTheFirstMessageSelectsTheThreadRow asserted the old behaviour on the pre-177 premise that the root has no child row; it is retargeted as recoveryOnTheFirstMessageSelectsItsOwnRow. Co-Authored-By: Claude Opus 5.5 --- src/mainwindow.cpp | 49 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 15 deletions(-) (limited to 'src/mainwindow.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 19bf3c2..434f5d0 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5382,8 +5382,9 @@ void MainWindow::applySelectors(const LaunchSelectors &requested) // the thread when its row arrives and selects it. Item 91's // double-click already reuses it; this is the third caller. // - // The empty message id is meaningful to it: land on the ROOT row, - // which is the thread's first message. + // The empty message id is meaningful to it: land on the thread's own + // row, the dashboard for a conversation and the message for a thread + // of one. recoverStaleThread(selectors.threadId, QString()); // After the query, which clears it: this is that query's own miss. m_launchMiss = tr("No thread matched '%1'.").arg(selectors.threadId); @@ -5492,10 +5493,16 @@ void MainWindow::onRowDoubleClicked(const QModelIndex &index) messageId = node.messageId; } else { threadId = m_model->data(index, ThreadListModel::ThreadIdRole).toString(); - // The thread's first message, so the pane opens on it rather than on - // nothing. Empty is fine and means the same thing to the recovery: land - // on the root, which IS that message. - messageId = m_model->data(index, ThreadListModel::MessageIdRole).toString(); + // A CONVERSATION row asks for the conversation, so no message is + // named and the recovery lands on the row itself, on the dashboard. A + // named message is always a message row to the recovery, which would + // open the root's own row instead. + // + // A thread of one is its message: named, so the pane opens on it. + // Empty is fine there too and means the same thing to the recovery. + if (!m_model->isConversationRow(index)) + messageId = + m_model->data(index, ThreadListModel::MessageIdRole).toString(); } if (threadId.isEmpty()) @@ -5600,18 +5607,31 @@ void MainWindow::applyPendingRecovery() // happen before any attempt to find one. m_threadView->expand(thread); - // The thread's first message IS the root card rather than a child row: - // setThreadMessages drops depth 0 because the root stands for it, so - // looking for it among the children finds nothing and the selection - // would silently land nowhere. + // A named message is ALWAYS a message row, never the conversation. + // The user's rule for --message: "we target ALWAYS the message. If + // that message is part of a thread we show it inside the thread, but + // still the message. If I want the thread there's --thread." The + // stale notice and a double-click on a message row mean the same. + // + // So the thread row answers for a message only when it is NOT a + // conversation: a thread of one is its message and has no child to + // select. A conversation carries its first message as child 0 since + // item 177 (setThreadMessages keeps it), so the root is found among + // the children below like any reply. Matching the conversation row on + // its own message id, as this did, landed on the dashboard. + // + // An empty id is the request for the thread itself: --thread, a + // double-click on a conversation row, a stale dashboard. // // selectRowAt(), not setCurrentIndex(): a current index without a // selection is what QTreeView sets by itself on focus, and // onThreadSelected() deliberately ignores that, so pointing at the row // renders nothing and leaves the pane blank. if (m_recoverMessageId.isEmpty() - || m_model->data(thread, ThreadListModel::MessageIdRole).toString() - == m_recoverMessageId) { + || (!m_model->isConversationRow(thread) + && m_model->data(thread, ThreadListModel::MessageIdRole) + .toString() + == m_recoverMessageId)) { selectRowAt(thread); m_recoverThreadId.clear(); m_recoverMessageId.clear(); @@ -5627,9 +5647,8 @@ void MainWindow::applyPendingRecovery() // (`src/threadlistmodel.cpp`), so the root check above cannot match yet // and returning here would leave the user looking at a collapsed thread // and a blank pane until the replies happen to arrive. Selecting the - // thread renders its first message immediately, which is the right - // answer outright when that is what they were reading, and is refined - // to the correct reply on the next pass when it is not. + // thread puts something on screen immediately, and it is refined to + // the named message's own row, the root's included, on the next pass. // // The target is deliberately NOT cleared: this pass is provisional. if (m_model->rowCount(thread) == 0) { -- cgit v1.2.3