Dismiss broken sessions, and hide ones untouched for a week

A broken session that will never be repaired kept the bar's warning dot lit
forever. repair-session now takes --ignore/--unignore/--unignore-all/--ignored,
recorded in ~/.claude-mode/ignored-sessions.json, and the scan hides broken
transcripts older than --max-age days (default 7, 0 disables,
CM_IGNORE_AGE_DAYS sets it). Hidden sessions move to a separate ignored[]
list with a reason, and --all always names how many it held back.

A repair clears the session's dismissal, and entries whose transcript is gone
are pruned, so a session that breaks again is never silently hidden.

The panel gets an Ignore button beside Repair and a collapsed "hidden (N)"
section with Restore. The dot still counts broken sessions only.

Bumps to 1.10.0 and fixes the widget manifest, which still said 1.8.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
smoido
2026-09-14 23:40:24 +03:00
co-authored by Claude Opus 5
parent f776ce099e
commit cbd3b1d8a9
7 changed files with 543 additions and 69 deletions
+192 -7
View File
@@ -32,6 +32,7 @@ Panel {
function open() {
root.expandedMode = ""
root.lastError = ""
root.hiddenExpanded = false
root.resetFlow()
if (widget) widget.refresh()
root.controller.show()
@@ -183,6 +184,47 @@ Panel {
}
}
// ---- Hidden transcripts
//
// Ignoring only changes whether the scan counts a session - the transcript is
// not touched and Restore is one click away - so unlike Repair it needs no
// confirmation. Age-hidden ones are never called ignored: nobody chose that.
readonly property var ignored: widget && widget.ignoredSessions ? widget.ignoredSessions : []
readonly property var dismissedList: ignored.filter(function (e) { return String(e.reason) === "dismissed" })
readonly property var staleList: ignored.filter(function (e) { return String(e.reason) !== "dismissed" })
readonly property int ageDays: widget ? widget.ignoreAgeDays : 7
property bool hiddenExpanded: false
// The card clamps rather than scrolls, so a long list would be cut off at
// the bottom edge instead of becoming reachable. The CLI has the full list.
readonly property int hiddenCap: 4
function projectLabel(p) { return String(p).replace(/^-/, "").replace(/-/g, "/") }
function setIgnored(entry, on) {
if (root.busy || !entry) return
root.busy = true
root.lastError = ""
ignoreProc.command = root.cli(["repair-session", on ? "--ignore" : "--unignore", String(entry.sessionId)])
ignoreProc.running = true
}
Process {
id: ignoreProc
running: false
stderr: StdioCollector {
waitForEnd: true
onStreamFinished: {
var msg = String(text || "").replace(/\x1b\[[0-9;]*m/g, "").trim()
var lines = msg.split("\n").filter(function (l) { return l.trim() !== "" })
if (lines.length > 0) root.lastError = lines[lines.length - 1].replace(/^\s*(FAIL|warn)\s*/, "").trim()
}
}
onExited: function (code) {
root.busy = false
if (widget) widget.scanSessions()
}
}
function storeServerKey() {
remedyProc.command = root.cli(["set-key", root.serverKeyRef, "--terminal"])
remedyProc.running = true
@@ -1014,13 +1056,14 @@ Panel {
// point is to be noticed without going looking.
Column {
width: parent.width
visible: root.stage === "list" && root.broken.length > 0
visible: root.stage === "list" && (root.broken.length > 0 || root.ignored.length > 0)
spacing: Style.space(4)
PanelSeparator { width: parent.width }
Text {
width: parent.width
visible: root.broken.length > 0
text: root.broken.length === 1
? "1 session cannot be resumed"
: root.broken.length + " sessions cannot be resumed"
@@ -1035,21 +1078,22 @@ Panel {
model: root.stage === "list" ? root.broken : []
Item {
id: brokenRow
required property var modelData
width: column.width
height: Style.space(26)
Column {
anchors.left: parent.left
anchors.right: repairBtn.left
anchors.right: ignoreBtn.left
anchors.rightMargin: Style.space(6)
anchors.verticalCenter: parent.verticalCenter
spacing: 0
Text {
width: parent.width
text: String(parent.parent.modelData.sessionId).substring(0, 8)
+ " · " + String(parent.parent.modelData.project).replace(/^-/, "").replace(/-/g, "/")
text: String(brokenRow.modelData.sessionId).substring(0, 8)
+ " · " + root.projectLabel(brokenRow.modelData.project)
color: Color.popups.text
elide: Text.ElideMiddle
font.family: root.bar ? root.bar.fontFamily : Style.font.family
@@ -1058,7 +1102,7 @@ Panel {
Text {
width: parent.width
text: parent.parent.modelData.dropLines + " turn lines after the last good message"
text: brokenRow.modelData.dropLines + " turn lines after the last good message"
color: Color.muted
elide: Text.ElideRight
font.family: root.bar ? root.bar.fontFamily : Style.font.family
@@ -1066,15 +1110,156 @@ Panel {
}
}
PillButton {
id: ignoreBtn
anchors.right: repairBtn.left
anchors.rightMargin: Style.space(5)
anchors.verticalCenter: parent.verticalCenter
label: "Ignore"
onTriggered: root.setIgnored(brokenRow.modelData, true)
}
PillButton {
id: repairBtn
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
label: "Repair"
onTriggered: root.askRepair(parent.modelData)
onTriggered: root.askRepair(brokenRow.modelData)
}
}
}
// ---- Hidden: dismissed here, or broken but untouched for longer than
// the age limit. Collapsed, because the point of hiding them was that
// they stop taking up attention - but never gone without a trace.
Item {
width: parent.width
height: Style.space(20)
visible: root.ignored.length > 0
Text {
id: hiddenLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: "hidden (" + root.ignored.length + ")"
color: hiddenArea.containsMouse ? Color.popups.text : Color.muted
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.space(10)
}
Text {
anchors.left: hiddenLabel.right
anchors.leftMargin: Style.space(4)
anchors.verticalCenter: parent.verticalCenter
text: String.fromCodePoint(root.hiddenExpanded ? 0xF0140 : 0xF0142)
color: hiddenLabel.color
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.space(11)
}
MouseArea {
id: hiddenArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.hiddenExpanded = !root.hiddenExpanded
}
}
Column {
width: parent.width
visible: root.hiddenExpanded && root.ignored.length > 0
spacing: Style.space(3)
Text {
visible: root.dismissedList.length > 0
text: "Ignored (" + root.dismissedList.length + ")"
color: Color.popups.text
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.space(10)
font.bold: true
}
Repeater {
model: root.hiddenExpanded ? root.dismissedList.slice(0, root.hiddenCap) : []
Item {
id: dismissedRow
required property var modelData
width: column.width
height: Style.space(26)
Text {
anchors.left: parent.left
anchors.right: restoreBtn.left
anchors.rightMargin: Style.space(6)
anchors.verticalCenter: parent.verticalCenter
text: String(dismissedRow.modelData.sessionId).substring(0, 8)
+ " · " + root.projectLabel(dismissedRow.modelData.project)
color: Color.muted
elide: Text.ElideMiddle
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.space(10)
}
PillButton {
id: restoreBtn
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
label: "Restore"
onTriggered: root.setIgnored(dismissedRow.modelData, false)
}
}
}
Text {
visible: root.dismissedList.length > root.hiddenCap
text: "… and " + (root.dismissedList.length - root.hiddenCap)
+ " more · claude-mode repair-session --ignored"
color: Color.muted
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.space(9)
}
Text {
visible: root.staleList.length > 0
text: "Older than " + root.ageDays + " days (" + root.staleList.length + ")"
color: Color.popups.text
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.space(10)
font.bold: true
}
Repeater {
model: root.hiddenExpanded ? root.staleList.slice(0, root.hiddenCap) : []
Text {
required property var modelData
width: column.width
text: String(modelData.sessionId).substring(0, 8)
+ " · " + Math.floor((Date.now() / 1000 - Number(modelData.mtime)) / 86400) + "d"
+ " · " + root.projectLabel(modelData.project)
color: Color.muted
elide: Text.ElideMiddle
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.space(10)
}
}
Text {
width: parent.width
visible: root.staleList.length > 0
text: (root.staleList.length > root.hiddenCap
? "… and " + (root.staleList.length - root.hiddenCap) + " more. " : "")
+ "Not counted because nothing has touched them since. "
+ "claude-mode repair-session --all --max-age 0 lists them."
color: Color.muted
opacity: 0.85
wrapMode: Text.WordWrap
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.space(9)
}
}
}
// ---- Confirming one repair.
@@ -1193,7 +1378,7 @@ Panel {
Text {
width: parent.width
text: root.busy
? "switching…"
? (ignoreProc.running ? "updating…" : (repairProc.running ? "repairing…" : "switching…"))
: (root.known && root.stage === "list" ? "Restart claude to pick this up." : "")
visible: text !== ""
color: Color.muted