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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion termdeck/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.15.0"
__version__ = "0.15.1"
31 changes: 23 additions & 8 deletions termdeck/static/app_markdown_files.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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;
},
Expand All @@ -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; }
}
}
Expand Down Expand Up @@ -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]))}`;
Expand Down Expand Up @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion termdeck/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@
the app_*.js files attach method groups to its prototype; app_boot.js instantiates. Order matters. -->
<script src="/static/app.js?v=20260912-remember-address"></script>
<script src="/static/app_search_git.js?v=20260914-submenu-position"></script>
<script src="/static/app_markdown_files.js?v=20260914-agent-paste"></script>
<script src="/static/app_markdown_files.js?v=20260914-filter-anchor"></script>
<script src="/static/app_terminal.js?v=20260903-claude-renderer-resume"></script>
<script src="/static/app_settings_ui.js?v=20260909-compact-update"></script>
<script src="/static/app_misc_ui.js?v=20260911-update-safety"></script>
Expand Down