Surface the real failure cause in the error dialog (roadmap A3)

On failure the error window showed a generic message with the whole log
buried in an expander. Lead instead with a focused summary: which step
failed (the process label), its command, and the last stderr lines, while
keeping the full log below. The runner passes the failing process to
error_window, which reads the executor's bounded stderr_data.

build_summary/_stderr_tail are static and unit-tested (tests/
test_error_summary.py): tail extraction, CR handling, blank-line skipping,
stdout fallback, long-line truncation, and Pango-markup escaping of paths/
messages containing & < ' so set_markup can't choke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-05 12:22:55 -07:00
parent 00fdbd50df
commit 8cb6e1aa52
3 changed files with 127 additions and 3 deletions

View file

@ -14,14 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
from gi.repository import Gtk, Gdk
from gi.repository import Gtk, Gdk, GLib
import os
import devedeng.configuration_data
class error_window:
def __init__(self):
def __init__(self, process=None):
self.config = devedeng.configuration_data.configuration.get_config()
@ -34,10 +34,60 @@ class error_window:
wdebug_buffer = builder.get_object("debug_buffer")
wdebug_buffer.set_text(self.config.get_log())
# lead with a focused summary of what actually failed, so the cause is
# visible without digging through the full log in the expander
wlabel = builder.get_object("label1")
if (wlabel is not None) and (process is not None):
wlabel.set_line_wrap(True)
wlabel.set_max_width_chars(80)
wlabel.set_selectable(True)
wlabel.set_markup(error_window.build_summary(process))
werror_window.show_all()
werror_window.run()
werror_window.destroy()
@staticmethod
def build_summary(process):
esc = GLib.markup_escape_text
parts = ["<b>" + esc(_("The disc could not be created.")) + "</b>"]
step = getattr(process, "text", "") or ""
if step:
parts.append("")
parts.append(esc(_("Failed step: %s") % step))
command = getattr(process, "command_var", None)
if command:
cmd_str = " ".join(str(c) for c in command)
if len(cmd_str) > 400:
cmd_str = cmd_str[:400] + " ..."
parts.append(esc(_("Command: %s") % cmd_str))
tail = error_window._stderr_tail(process)
if tail:
parts.append("")
parts.append(esc(_("Last messages:")))
parts.append("<tt>" + esc(tail) + "</tt>")
parts.append("")
parts.append(esc(_("See the full log below for details.")))
return "\n".join(parts)
@staticmethod
def _stderr_tail(process, max_lines=12, max_line_len=200):
raw = getattr(process, "stderr_data", "") or ""
if not raw:
raw = getattr(process, "stdout_data", "") or ""
if not raw:
return ""
lines = [l.strip() for l in raw.replace("\r", "\n").split("\n")]
lines = [l for l in lines if l]
tail = lines[-max_lines:]
tail = [(l[:max_line_len] + " ...") if len(l) > max_line_len else l
for l in tail]
return "\n".join(tail)
def on_copy_clicked(self, b):
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)

View file

@ -132,7 +132,7 @@ class runner(GObject.GObject):
for element in self.proc_list:
element.cancel()
self.wprogress.destroy()
devedeng.error.error_window()
devedeng.error.error_window(process)
self.emit("done", 1) # there was an error
return