diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85c5e19..a9f34ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,12 @@ All notable changes to this project are documented here. The format follows
## [Unreleased]
+## [0.15.1] — 2026-09-14
+
+### Fixed
+
+- Changing conversation filters preserves the visible prompt and its position in both the outline and transcript, rather than jumping when hidden responses reappear.
+
## [0.15.0] — 2026-09-14
### Fixed
@@ -792,7 +798,8 @@ First public release.
nothing compiles; `uv`/`pipx` from the GitHub release everywhere else. Apache 2.0 license; full README,
installation, configuration, troubleshooting, and architecture documentation.
-[Unreleased]: https://github.com/danialfarid/termdeck/compare/v0.15.0...HEAD
+[Unreleased]: https://github.com/danialfarid/termdeck/compare/v0.15.1...HEAD
+[0.15.1]: https://github.com/danialfarid/termdeck/compare/v0.15.0...v0.15.1
[0.15.0]: https://github.com/danialfarid/termdeck/compare/v0.14.0...v0.15.0
[0.14.0]: https://github.com/danialfarid/termdeck/compare/v0.13.0...v0.14.0
[0.13.0]: https://github.com/danialfarid/termdeck/compare/v0.12.3...v0.13.0
diff --git a/termdeck/__init__.py b/termdeck/__init__.py
index 9da2f8f..903e77c 100644
--- a/termdeck/__init__.py
+++ b/termdeck/__init__.py
@@ -1 +1 @@
-__version__ = "0.15.0"
+__version__ = "0.15.1"
diff --git a/termdeck/static/app_markdown_files.js b/termdeck/static/app_markdown_files.js
index 1b74703..37d5527 100644
--- a/termdeck/static/app_markdown_files.js
+++ b/termdeck/static/app_markdown_files.js
@@ -3231,7 +3231,7 @@ Object.assign(TermdeckApp.prototype, {
const turns = this.historyTurnsBySession.get(sessionId) || this.conversationOutlineTurnsBySession.get(sessionId) || this.historyTurns;
if (this.historyOpen && sessionId) {
this.historyFingerprint = "";
- this.applyHistoryTurns(sessionId, turns, { preserveScroll: true, forceRender: true });
+ this.applyHistoryTurns(sessionId, turns, { preserveScroll: true, forceRender: true, preserveFilterAnchor: true });
requestAnimationFrame(() => this.scheduleFilteredHistoryContinuation(sessionId));
}
if (this.conversationOutlineOpen && sessionId) this.renderConversationOutline(turns, { preserveScroll: true });
@@ -3624,7 +3624,12 @@ Object.assign(TermdeckApp.prototype, {
},
- captureHistoryScroll(body, turns = this.historyTurns) {
+ historyScrollElementKey(element) {
+ return element.dataset?.outlineKey || this.historyElementPreserveKey(element);
+ },
+
+
+ captureHistoryScroll(body, turns = this.historyTurns, preferredKey = "") {
const snapshot = {
top: body.scrollTop,
atBottom: body.scrollHeight - body.clientHeight - body.scrollTop < 80,
@@ -3635,7 +3640,10 @@ Object.assign(TermdeckApp.prototype, {
};
const bodyTop = body.getBoundingClientRect().top;
const children = [...body.children];
+ const preferredIndex = preferredKey ? children.findIndex((child) => child.dataset.outlineKey === preferredKey &&
+ child.getBoundingClientRect().bottom > bodyTop && child.getBoundingClientRect().top < bodyTop + body.clientHeight) : -1;
for (let index = 0; index < children.length; index += 1) {
+ if (preferredIndex >= 0 && index !== preferredIndex) continue;
if (children[index].getBoundingClientRect().bottom > bodyTop + 1) {
snapshot.anchorIndex = index;
snapshot.anchorOffset = children[index].getBoundingClientRect().top - bodyTop;
@@ -3649,9 +3657,9 @@ Object.assign(TermdeckApp.prototype, {
if (snapshot.anchorIndex >= 0 && children[snapshot.anchorIndex]) {
// Keyed off the DOM, not the turns array: after paged loads the two segment
// differently mid-array, and a turns-index anchor lands on the wrong element.
- snapshot.anchorKey = this.historyElementPreserveKey(children[snapshot.anchorIndex]);
+ snapshot.anchorKey = this.historyScrollElementKey(children[snapshot.anchorIndex]);
snapshot.anchorOccurrence = children.slice(0, snapshot.anchorIndex + 1)
- .filter((child) => this.historyElementPreserveKey(child) === snapshot.anchorKey).length - 1;
+ .filter((child) => this.historyScrollElementKey(child) === snapshot.anchorKey).length - 1;
}
return snapshot;
},
@@ -3677,7 +3685,7 @@ Object.assign(TermdeckApp.prototype, {
if (snapshot.anchorKey) {
let occurrence = 0;
for (const child of body.children) {
- if (this.historyElementPreserveKey(child) !== snapshot.anchorKey) continue;
+ if (this.historyScrollElementKey(child) !== snapshot.anchorKey) continue;
if (occurrence++ === snapshot.anchorOccurrence) { anchor = child; break; }
}
}
@@ -3751,8 +3759,10 @@ Object.assign(TermdeckApp.prototype, {
const scrollSnapshot = followLatest
? null
: preserveScroll
- ? this.captureHistoryScroll(body, this.historyTurns)
+ ? this.captureHistoryScroll(body, this.historyTurns, options.preserveFilterAnchor && this.conversationOutlineSessionId === sessionId
+ ? this.conversationOutlineSelectedTurnKey : "")
: (this.historyScrollBySession.get(sessionId) || null);
+ if (options.preserveFilterAnchor && scrollSnapshot) scrollSnapshot.atBottom = false;
const fingerprint = `${renderedTurns.length}|${JSON.stringify(renderedTurns.slice(-3).map((turn) =>
[turn.role, turn.kind, turn.text, turn.timestamp, turn.diff?.length, turn.diff_files, turn.plan, turn.items,
turn.folded_responses?.length, turn.pending_id, turn.pending_delivery_state]))}`;
@@ -5477,7 +5487,10 @@ Object.assign(TermdeckApp.prototype, {
const list = this.$("conversation-outline-list");
const sessionId = options.sessionId || this.activeId;
const sessionChanged = this.conversationOutlineSessionId !== sessionId;
- const previousScrollTop = list.scrollTop;
+ if (sessionChanged) this.conversationOutlineSelectedTurnKey = "";
+ const scrollSnapshot = options.preserveScroll && !sessionChanged
+ ? this.captureHistoryScroll(list, turns, this.conversationOutlineSelectedTurnKey) : null;
+ if (scrollSnapshot) scrollSnapshot.atBottom = false;
list.textContent = "";
const messages = this.filteredHistoryTurns(turns).filter((turn) =>
(["user", "assistant"].includes(turn.role) && String(turn.text || "").trim()) || turn.kind === "edit");
@@ -5491,6 +5504,7 @@ Object.assign(TermdeckApp.prototype, {
const question = !prompt && !edit && /[??]\s*$/.test(String(turn.text || "").trim());
const messageType = prompt ? "prompt" : edit ? "edit" : question ? "question" : "response";
item.className = `conversation-outline-item ${messageType}`;
+ item.dataset.outlineKey = this.conversationOutlineTurnKey(turn);
const role = document.createElement("span");
role.className = `conversation-outline-role codicon codicon-${prompt ? "arrow-right" : edit ? "diff" : question ? "question" : "sparkle"}`;
const label = document.createElement("span");
@@ -5527,7 +5541,7 @@ Object.assign(TermdeckApp.prototype, {
list.scrollTop = Math.max(0, latestPromptItem.offsetTop - list.offsetTop - 6);
});
} else if (options.preserveScroll && !sessionChanged) {
- list.scrollTop = previousScrollTop;
+ this.restoreHistoryScroll(list, scrollSnapshot, turns, true);
} else {
list.scrollTop = 0;
}
@@ -5536,6 +5550,7 @@ Object.assign(TermdeckApp.prototype, {
openConversationOutlineTurn(turn) {
const key = this.conversationOutlineTurnKey(turn);
+ this.conversationOutlineSelectedTurnKey = key;
if (!this.historyOpen) this.setHistoryMode(true);
const reveal = (attempt = 0) => {
if (!this.historyOpen || attempt > 12) return;
diff --git a/termdeck/static/index.html b/termdeck/static/index.html
index 1875d55..64a7e46 100644
--- a/termdeck/static/index.html
+++ b/termdeck/static/index.html
@@ -624,7 +624,7 @@
the app_*.js files attach method groups to its prototype; app_boot.js instantiates. Order matters. -->
-
+