Notice broken sessions in the bar, and offer to repair them there
Nothing tells you a session is unresumable until you try to resume it, and by then you have usually forgotten which one it was. The widget now scans every project on a timer and whenever the panel opens, marks its icon when something needs fixing, and lists the affected sessions with a repair button that says what it will drop and what it will keep before doing anything. The scan had to get roughly eighty times cheaper first. Classifying a transcript needs two facts - what its last message id is, and whether any Anthropic id exists at all - and the first settles the common case alone. Reading the tail of each file and only opening the whole thing when the tail already looks wrong takes the sweep from ~5s to ~60ms across 59 transcripts, which is the difference between something that can sit on a timer and something that cannot. `--all` uses the same path, and `--json` exposes it. The badge is a dot beside the mark rather than a recolouring of it: this widget's job is to report which provider is active, and tinting it red to mean something else entirely would be a lie about that. Verified by planting a genuinely corrupted transcript, watching the scan find it and the dot appear, then removing it and watching both clear.
This commit is contained in:
@@ -388,6 +388,23 @@ session untouched.
|
|||||||
It refuses to touch a transcript written to in the last 90 seconds, since that
|
It refuses to touch a transcript written to in the last 90 seconds, since that
|
||||||
one belongs to a session still alive.
|
one belongs to a session still alive.
|
||||||
|
|
||||||
|
### The bar notices for you
|
||||||
|
|
||||||
|
Nothing tells you a session is unresumable until you try to resume it, by which
|
||||||
|
point you have usually forgotten which one it was. So the widget scans every
|
||||||
|
project on a timer (and whenever the panel opens) and puts a dot on its icon
|
||||||
|
when there is something to fix. Clicking through lists the affected sessions and
|
||||||
|
offers to repair each one, after saying what it will drop and what it will keep.
|
||||||
|
|
||||||
|
The dot is a dot rather than a colour change, because recolouring the mark would
|
||||||
|
misreport the active mode — which is the widget's actual job.
|
||||||
|
|
||||||
|
That scan is only affordable because it reads the *tail* of each transcript
|
||||||
|
first: if the last message is Anthropic's, the transcript is healthy and the
|
||||||
|
rest of the file is never opened. Since that is the overwhelmingly common case,
|
||||||
|
the whole sweep costs ~60ms for 59 transcripts, against ~5s for the obvious
|
||||||
|
version that reads every byte of every one.
|
||||||
|
|
||||||
This is why a switch now asks before it writes rather than reporting afterwards.
|
This is why a switch now asks before it writes rather than reporting afterwards.
|
||||||
So restart affected sessions — and `claude-mode sessions --restart` will do it
|
So restart affected sessions — and `claude-mode sessions --restart` will do it
|
||||||
for you:
|
for you:
|
||||||
|
|||||||
+28
-26
@@ -2122,7 +2122,7 @@ cm_project_dir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_repair_session() {
|
cmd_repair_session() {
|
||||||
local target='' apply=0 reinject=1 scan_all=0 dir='' a file verdict age
|
local target='' apply=0 reinject=1 scan_all=0 as_json=0 dir='' a file verdict age
|
||||||
for a in "$@"; do
|
for a in "$@"; do
|
||||||
case "$a" in
|
case "$a" in
|
||||||
--apply) apply=1 ;;
|
--apply) apply=1 ;;
|
||||||
@@ -2130,6 +2130,7 @@ cmd_repair_session() {
|
|||||||
--no-reinject) reinject=0 ;;
|
--no-reinject) reinject=0 ;;
|
||||||
--list) target='--list' ;;
|
--list) target='--list' ;;
|
||||||
--all) scan_all=1 ;;
|
--all) scan_all=1 ;;
|
||||||
|
--json) as_json=1; scan_all=1 ;;
|
||||||
-*) err "unknown option '$a'"; return 1 ;;
|
-*) err "unknown option '$a'"; return 1 ;;
|
||||||
*) target="$a" ;;
|
*) target="$a" ;;
|
||||||
esac
|
esac
|
||||||
@@ -2139,32 +2140,33 @@ cmd_repair_session() {
|
|||||||
# position from which to remember which project it belonged to. --all drops
|
# position from which to remember which project it belonged to. --all drops
|
||||||
# the working-directory scoping and reports only what is actually broken.
|
# the working-directory scoping and reports only what is actually broken.
|
||||||
if [ "$scan_all" -eq 1 ]; then
|
if [ "$scan_all" -eq 1 ]; then
|
||||||
head_ 'scanning every session transcript'
|
local scan
|
||||||
local f v bad=0 total=0
|
scan="$("$PY" "$JSON" scan-sessions "$CM_SETTINGS_DIR/projects" 2>/dev/null)" || {
|
||||||
for f in "$CM_SETTINGS_DIR"/projects/*/*.jsonl; do
|
err 'could not scan transcripts'; return 1; }
|
||||||
[ -f "$f" ] || continue
|
|
||||||
total=$((total + 1))
|
if [ "$as_json" -eq 1 ]; then
|
||||||
v="$("$PY" "$JSON" repair-session "$f" 2>/dev/null)" || continue
|
printf '%s\n' "$scan"
|
||||||
printf '%s' "$v" | "$PY" -c "
|
return 0
|
||||||
import json,sys,os
|
|
||||||
d=json.load(sys.stdin)
|
|
||||||
# Only 'repairable' is damage this tool can or should act on.
|
|
||||||
if d['kind'] != 'repairable': raise SystemExit(0)
|
|
||||||
print(' %-38s %s' % (os.path.basename(d['path'])[:-6], os.path.basename(os.path.dirname(d['path']))))
|
|
||||||
print(' would drop %d line(s); %d foreign id(s)' % (d['dropLines'], len(d['foreignIds'])))
|
|
||||||
raise SystemExit(9)
|
|
||||||
" && continue
|
|
||||||
bad=$((bad + 1))
|
|
||||||
done
|
|
||||||
printf '\n'
|
|
||||||
if [ "$bad" -eq 0 ]; then
|
|
||||||
ok "nothing to repair across $total transcript(s)"
|
|
||||||
else
|
|
||||||
warn "$bad of $total transcript(s) were cut short by a mode switch"
|
|
||||||
printf ' %sclaude-mode repair-session <id> --apply%s\n' "$C_DIM" "$C_RESET"
|
|
||||||
fi
|
fi
|
||||||
printf ' %ssessions that ran entirely on a gateway are not listed: they carry that%s\n' "$C_DIM" "$C_RESET"
|
|
||||||
printf ' %sprovider'"'"'s ids by design and resume fine under it%s\n' "$C_DIM" "$C_RESET"
|
head_ 'scanning every session transcript'
|
||||||
|
printf '%s' "$scan" | "$PY" -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
G,Y,D,X='\033[32m','\033[33m','\033[90m','\033[0m'
|
||||||
|
for b in d['broken']:
|
||||||
|
print(' %-38s %s' % (b['sessionId'], b['project']))
|
||||||
|
prov = ', '.join(b['providers']) or 'another provider'
|
||||||
|
print(' %d line(s) after the last good message, from %s' % (b['dropLines'], prov))
|
||||||
|
print()
|
||||||
|
if not d['broken']:
|
||||||
|
print(' %sok %s nothing to repair across %d transcript(s)' % (G,X,d['scanned']))
|
||||||
|
else:
|
||||||
|
print(' %swarn%s %d of %d transcript(s) were cut short by a mode switch' % (Y,X,d['count'],d['scanned']))
|
||||||
|
print(' %sclaude-mode repair-session <id> --apply%s' % (D,X))
|
||||||
|
print(' %ssessions that ran entirely on a gateway are not listed: they carry that%s' % (D,X))
|
||||||
|
print(' %sprovider\'s ids by design and resume fine under it%s' % (D,X))
|
||||||
|
"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Subcommands:
|
|||||||
presets <dir> "name<TAB>provider<TAB>desc"
|
presets <dir> "name<TAB>provider<TAB>desc"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -837,11 +838,116 @@ def _safe(line):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _last_msg_id_from_tail(path, window=262144):
|
||||||
|
"""The id of the last message-bearing line, read from the end of the file.
|
||||||
|
|
||||||
|
Classifying a transcript needs two facts: what the last message id is, and
|
||||||
|
whether any Anthropic id exists at all. The first decides the common case on
|
||||||
|
its own - if the last one is `msg_`, the transcript is healthy and the rest
|
||||||
|
of the file never has to be touched. Since that is 40-odd of the 59 here,
|
||||||
|
and some are 10MB, reading the tail first is the difference between a scan
|
||||||
|
that can run on a timer and one that cannot.
|
||||||
|
|
||||||
|
Returns (id_or_None, saw_whole_file).
|
||||||
|
"""
|
||||||
|
size = os.path.getsize(path)
|
||||||
|
with open(path, "rb") as fh:
|
||||||
|
start = max(0, size - window)
|
||||||
|
fh.seek(start)
|
||||||
|
chunk = fh.read()
|
||||||
|
whole = start == 0
|
||||||
|
if not whole:
|
||||||
|
nl = chunk.find(b"\n") # drop the partial first line
|
||||||
|
chunk = chunk[nl + 1:] if nl >= 0 else b""
|
||||||
|
for raw in reversed(chunk.splitlines()):
|
||||||
|
if not raw.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
d = json.loads(raw.decode("utf-8", "replace"))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
mid = _msg_id(d)
|
||||||
|
if mid:
|
||||||
|
return mid, whole
|
||||||
|
return None, whole
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_scan_sessions(argv):
|
||||||
|
"""scan-sessions <projects-dir> - JSON list of transcripts worth repairing.
|
||||||
|
|
||||||
|
Only `repairable` is reported: a transcript carrying a genuine Anthropic
|
||||||
|
message with a different provider's output after it. A session that ran
|
||||||
|
entirely on a gateway, or never got a reply, is not damage.
|
||||||
|
"""
|
||||||
|
root = argv[0]
|
||||||
|
broken, scanned, skipped = [], 0, 0
|
||||||
|
|
||||||
|
for proj in sorted(glob.glob(os.path.join(root, "*"))):
|
||||||
|
if not os.path.isdir(proj):
|
||||||
|
continue
|
||||||
|
for path in sorted(glob.glob(os.path.join(proj, "*.jsonl"))):
|
||||||
|
scanned += 1
|
||||||
|
try:
|
||||||
|
last, whole = _last_msg_id_from_tail(path)
|
||||||
|
except OSError:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
# Healthy is decided by the tail alone, and is the common case.
|
||||||
|
if last is not None and last.startswith("msg_"):
|
||||||
|
continue
|
||||||
|
if last is None and whole:
|
||||||
|
continue # no replies at all; nothing to fix
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8", errors="replace") as fh:
|
||||||
|
lines = fh.read().splitlines()
|
||||||
|
except OSError:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
last_good, foreign = -1, []
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
d = json.loads(line)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
mid = _msg_id(d)
|
||||||
|
if not mid:
|
||||||
|
continue
|
||||||
|
if mid.startswith("msg_"):
|
||||||
|
last_good = i
|
||||||
|
elif mid.startswith("gen-"):
|
||||||
|
foreign.append(str((d.get("message") or {}).get("model", "")))
|
||||||
|
|
||||||
|
if last_good < 0:
|
||||||
|
continue # gateway-native or reply-less
|
||||||
|
tail = [i for i in range(len(lines) - 1, last_good, -1)
|
||||||
|
if lines[i].strip() and _msg_id(_safe(lines[i])) is not None]
|
||||||
|
if not tail:
|
||||||
|
continue
|
||||||
|
|
||||||
|
broken.append({
|
||||||
|
"sessionId": os.path.basename(path)[:-6],
|
||||||
|
"project": os.path.basename(proj),
|
||||||
|
"path": path,
|
||||||
|
"lines": len(lines),
|
||||||
|
"dropLines": len(lines) - (last_good + 1),
|
||||||
|
"providers": sorted(set(f for f in foreign if f)),
|
||||||
|
"mtime": int(os.path.getmtime(path)),
|
||||||
|
})
|
||||||
|
|
||||||
|
print(json.dumps({"scanned": scanned, "skipped": skipped,
|
||||||
|
"broken": broken, "count": len(broken)}))
|
||||||
|
|
||||||
COMMANDS = {
|
COMMANDS = {
|
||||||
"health": cmd_health,
|
"health": cmd_health,
|
||||||
"preflight-json": cmd_preflight_json,
|
"preflight-json": cmd_preflight_json,
|
||||||
"sessions-json": cmd_sessions_json,
|
"sessions-json": cmd_sessions_json,
|
||||||
"repair-session": cmd_repair_session,
|
"repair-session": cmd_repair_session,
|
||||||
|
"scan-sessions": cmd_scan_sessions,
|
||||||
"stale-models": cmd_stale_models,
|
"stale-models": cmd_stale_models,
|
||||||
"strip-tags": cmd_strip_tags,
|
"strip-tags": cmd_strip_tags,
|
||||||
"or-models": cmd_or_models,
|
"or-models": cmd_or_models,
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ BarWidget {
|
|||||||
// bar stays quiet exactly when nothing unusual is configured.
|
// bar stays quiet exactly when nothing unusual is configured.
|
||||||
readonly property bool gateway: known && mode !== "anthropic"
|
readonly property bool gateway: known && mode !== "anthropic"
|
||||||
|
|
||||||
|
readonly property color urgentColor: {
|
||||||
|
var c = Color.urgent
|
||||||
|
var mx = Math.max(c.r, c.g, c.b)
|
||||||
|
var mn = Math.min(c.r, c.g, c.b)
|
||||||
|
var l = (mx + mn) / 2
|
||||||
|
var d = 1 - Math.abs(2 * l - 1)
|
||||||
|
return (d > 0.0001 ? (mx - mn) / d : 0) >= 0.15 ? c : "#d2685f"
|
||||||
|
}
|
||||||
|
|
||||||
readonly property color activeColor: !known
|
readonly property color activeColor: !known
|
||||||
? Color.muted
|
? Color.muted
|
||||||
: (gateway ? Color.accent : (bar ? bar.barForeground : Color.foreground))
|
: (gateway ? Color.accent : (bar ? bar.barForeground : Color.foreground))
|
||||||
@@ -111,10 +120,48 @@ BarWidget {
|
|||||||
running: false
|
running: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Broken transcripts
|
||||||
|
//
|
||||||
|
// A session cut short by a mode switch cannot be resumed, and nothing tells
|
||||||
|
// you until you try - by which time you have usually forgotten which session
|
||||||
|
// it was. So the bar checks periodically and marks itself when there is
|
||||||
|
// something to fix.
|
||||||
|
//
|
||||||
|
// The scan reads the tail of each transcript first and only opens the whole
|
||||||
|
// file when the tail already looks wrong, which is what makes it cheap enough
|
||||||
|
// to sit on a timer: ~60ms for 59 transcripts here, against 5s for the naive
|
||||||
|
// version that read every byte of every one.
|
||||||
|
property var brokenSessions: []
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: scanProc
|
||||||
|
running: false
|
||||||
|
command: [root.cmRoot + "/bin/claude-mode", "repair-session", "--json"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
waitForEnd: true
|
||||||
|
onStreamFinished: {
|
||||||
|
var d = null
|
||||||
|
try { d = JSON.parse(String(text || "")) } catch (e) { d = null }
|
||||||
|
root.brokenSessions = (d && d.broken) ? d.broken : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanSessions() { if (!scanProc.running) scanProc.running = true }
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 20 * 60 * 1000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: root.scanSessions()
|
||||||
|
}
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
healthFile.reload()
|
healthFile.reload()
|
||||||
stateFile.reload()
|
stateFile.reload()
|
||||||
if (!root.health) seedProc.running = true
|
if (!root.health) seedProc.running = true
|
||||||
|
root.scanSessions()
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: Qt.callLater(root.refresh)
|
Component.onCompleted: Qt.callLater(root.refresh)
|
||||||
@@ -134,14 +181,34 @@ BarWidget {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: root.gap
|
spacing: root.gap
|
||||||
|
|
||||||
BrandIcon {
|
Item {
|
||||||
id: icon
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: icon.width
|
||||||
|
height: icon.height
|
||||||
visible: root.logo !== ""
|
visible: root.logo !== ""
|
||||||
pathData: root.logo
|
|
||||||
opticalScale: Modes.logoScale(root.mode)
|
BrandIcon {
|
||||||
color: root.activeColor
|
id: icon
|
||||||
iconSize: root.iconPx
|
anchors.fill: parent
|
||||||
|
pathData: root.logo
|
||||||
|
opticalScale: Modes.logoScale(root.mode)
|
||||||
|
color: root.activeColor
|
||||||
|
iconSize: root.iconPx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recolouring the mark would misreport the mode, which is this widget's
|
||||||
|
// whole job, so the warning gets its own dot instead.
|
||||||
|
Rectangle {
|
||||||
|
visible: root.brokenSessions.length > 0
|
||||||
|
width: Math.max(4, Math.round(root.iconPx / 3.2))
|
||||||
|
height: width
|
||||||
|
radius: width / 2
|
||||||
|
color: root.urgentColor
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.rightMargin: -Math.round(width / 3)
|
||||||
|
anchors.topMargin: -Math.round(width / 4)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only reached when claude-mode is not installed - there is no brand to
|
// Only reached when claude-mode is not installed - there is no brand to
|
||||||
@@ -185,6 +252,11 @@ BarWidget {
|
|||||||
var models = root.health && root.health.models ? root.health.models : null
|
var models = root.health && root.health.models ? root.health.models : null
|
||||||
if (models && models.opus) lines.push("opus " + models.opus)
|
if (models && models.opus) lines.push("opus " + models.opus)
|
||||||
if (models && models.sonnet) lines.push("sonnet " + models.sonnet)
|
if (models && models.sonnet) lines.push("sonnet " + models.sonnet)
|
||||||
|
if (root.brokenSessions.length > 0) {
|
||||||
|
lines.push(root.brokenSessions.length === 1
|
||||||
|
? "1 session needs repair"
|
||||||
|
: root.brokenSessions.length + " sessions need repair")
|
||||||
|
}
|
||||||
return lines.join(" · ")
|
return lines.join(" · ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +271,7 @@ BarWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onHealthChanged: syncTooltip()
|
onHealthChanged: syncTooltip()
|
||||||
|
onBrokenSessionsChanged: syncTooltip()
|
||||||
|
|
||||||
// Hover must come from a HoverHandler: once triggerPress() exists the bar's
|
// Hover must come from a HoverHandler: once triggerPress() exists the bar's
|
||||||
// own slot MouseArea accepts hover events and swallows them before any
|
// own slot MouseArea accepts hover events and swallows them before any
|
||||||
|
|||||||
@@ -142,6 +142,47 @@ Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Broken transcripts
|
||||||
|
//
|
||||||
|
// Repair truncates a transcript back to its last resumable point. That is
|
||||||
|
// destructive enough to confirm, and reversible enough to offer: the original
|
||||||
|
// is backed up, the cut turns are written out as Markdown, and the same text
|
||||||
|
// is handed back to the session so it still knows what it did.
|
||||||
|
readonly property var broken: widget && widget.brokenSessions ? widget.brokenSessions : []
|
||||||
|
property var repairTarget: null
|
||||||
|
|
||||||
|
function askRepair(entry) {
|
||||||
|
root.repairTarget = entry
|
||||||
|
root.stage = "repair"
|
||||||
|
}
|
||||||
|
|
||||||
|
function doRepair() {
|
||||||
|
if (root.busy || !root.repairTarget) return
|
||||||
|
root.busy = true
|
||||||
|
repairProc.command = root.cli(["repair-session", String(root.repairTarget.sessionId), "--apply"])
|
||||||
|
repairProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: repairProc
|
||||||
|
running: false
|
||||||
|
stderr: StdioCollector {
|
||||||
|
waitForEnd: true
|
||||||
|
onStreamFinished: {
|
||||||
|
var msg = String(text || "").replace(/\x1b\[[0-9;]*m/g, "").trim()
|
||||||
|
if (msg !== "") root.lastError = msg.split("\n").pop().trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExited: function (code) {
|
||||||
|
root.busy = false
|
||||||
|
root.repairTarget = null
|
||||||
|
root.stage = "list"
|
||||||
|
// The scan is what drives the bar's warning dot, so re-run it rather than
|
||||||
|
// trusting this side to have guessed the new state.
|
||||||
|
if (widget) widget.scanSessions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function storeServerKey() {
|
function storeServerKey() {
|
||||||
remedyProc.command = root.cli(["set-key", root.serverKeyRef, "--terminal"])
|
remedyProc.command = root.cli(["set-key", root.serverKeyRef, "--terminal"])
|
||||||
remedyProc.running = true
|
remedyProc.running = true
|
||||||
@@ -154,6 +195,7 @@ Panel {
|
|||||||
root.pendingMode = ""
|
root.pendingMode = ""
|
||||||
root.pendingPreset = ""
|
root.pendingPreset = ""
|
||||||
root.serverPreset = ""
|
root.serverPreset = ""
|
||||||
|
root.repairTarget = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchTo(mode, presetName) {
|
function switchTo(mode, presetName) {
|
||||||
@@ -967,6 +1009,127 @@ Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- Sessions that a switch cut short. Shown in the list, because the
|
||||||
|
// point is to be noticed without going looking.
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.stage === "list" && root.broken.length > 0
|
||||||
|
spacing: Style.space(4)
|
||||||
|
|
||||||
|
PanelSeparator { width: parent.width }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.broken.length === 1
|
||||||
|
? "1 session cannot be resumed"
|
||||||
|
: root.broken.length + " sessions cannot be resumed"
|
||||||
|
color: root.urgentColor
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||||
|
font.pixelSize: Style.space(11)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.stage === "list" ? root.broken : []
|
||||||
|
|
||||||
|
Item {
|
||||||
|
required property var modelData
|
||||||
|
width: column.width
|
||||||
|
height: Style.space(26)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: repairBtn.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, "/")
|
||||||
|
color: Color.popups.text
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: parent.parent.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
|
||||||
|
font.pixelSize: Style.space(9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PillButton {
|
||||||
|
id: repairBtn
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
label: "Repair"
|
||||||
|
onTriggered: root.askRepair(parent.modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Confirming one repair.
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.stage === "repair" && root.repairTarget !== null
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "Repair " + (root.repairTarget ? String(root.repairTarget.sessionId).substring(0, 8) : "")
|
||||||
|
color: Color.popups.text
|
||||||
|
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||||
|
font.pixelSize: Style.space(13)
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: {
|
||||||
|
if (!root.repairTarget) return ""
|
||||||
|
var p = (root.repairTarget.providers || []).join(", ")
|
||||||
|
return "This session took " + (p !== "" ? p + "'s" : "another provider's")
|
||||||
|
+ " output while the mode was switched under it, and Anthropic will not "
|
||||||
|
+ "resume it. Rolling it back to its last good message drops "
|
||||||
|
+ root.repairTarget.dropLines + " lines."
|
||||||
|
}
|
||||||
|
color: Color.muted
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
lineHeight: 1.2
|
||||||
|
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "The original is backed up, the dropped turns are saved as Markdown, "
|
||||||
|
+ "and handed back to the session so it still knows what it did."
|
||||||
|
color: Color.muted
|
||||||
|
opacity: 0.85
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
lineHeight: 1.2
|
||||||
|
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||||
|
font.pixelSize: Style.space(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.space(7)
|
||||||
|
|
||||||
|
PillButton { label: "Repair it"; primary: true; onTriggered: root.doRepair() }
|
||||||
|
PillButton { label: "Cancel"; onTriggered: root.resetFlow() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PanelSeparator { width: parent.width; visible: root.modelRows.length > 0 && root.stage === "list" }
|
PanelSeparator { width: parent.width; visible: root.modelRows.length > 0 && root.stage === "list" }
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
|||||||
Reference in New Issue
Block a user