From b0a6db429c259effc27f0387dc06a392953ec4fc Mon Sep 17 00:00:00 2001 From: migsilva89 <85745515+migsilva89@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:49:36 +0100 Subject: [PATCH 1/3] Say it in English, everywhere it is read --- Scripts/frame-timing.py | 6 ++--- Sources/LoadoutApp/SelfCheck.swift | 38 ++++++++++++++-------------- Sources/LoadoutCore/UsageIndex.swift | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Scripts/frame-timing.py b/Scripts/frame-timing.py index c2c3b7b..bddc1cd 100644 --- a/Scripts/frame-timing.py +++ b/Scripts/frame-timing.py @@ -36,7 +36,7 @@ stamps = [(f, i * 0.13) for i, f in enumerate(frames)] if not stamps: - print("sem fotogramas em", folder) + print("no frames in", folder) sys.exit(1) # The first frames of a recording can be a different size from the rest: the window is captured @@ -54,7 +54,7 @@ def size(path): common = max(sizes, key=sizes.get) dropped = [path for path, _ in stamps if size(path) != common] if dropped: - print("→ %d fotogramas de outro tamanho, postos de lado" % len(dropped)) + print("→ %d frames of another size, set aside" % len(dropped)) stamps = [(path, at) for path, at in stamps if size(path) == common] lines = [] @@ -73,4 +73,4 @@ def size(path): with open(os.path.join(folder, "list.txt"), "w") as handle: handle.write("\n".join(lines) + "\n") -print("→ %d fotogramas, %.1f s" % (len(stamps), total)) +print("→ %d frames, %.1f s" % (len(stamps), total)) diff --git a/Sources/LoadoutApp/SelfCheck.swift b/Sources/LoadoutApp/SelfCheck.swift index 0d52af1..e5dcdfb 100644 --- a/Sources/LoadoutApp/SelfCheck.swift +++ b/Sources/LoadoutApp/SelfCheck.swift @@ -44,13 +44,13 @@ enum SelfCheck { } // Create - model.createSkill(name: "auto-teste", description: "Created by the self-check.") - check("creates a skill", model.items.contains { $0.name == "auto-teste" }) - check("selects it", model.selected?.name == "auto-teste") - check("loads the file in the editor", model.draft.contains("name: auto-teste")) + model.createSkill(name: "self-check-skill", description: "Created by the self-check.") + check("creates a skill", model.items.contains { $0.name == "self-check-skill" }) + check("selects it", model.selected?.name == "self-check-skill") + check("loads the file in the editor", model.draft.contains("name: self-check-skill")) // Edit and save - model.draft = "---\nname: auto-teste\ndescription: New résumé.\n---\n\nNew body." + model.draft = "---\nname: self-check-skill\ndescription: New résumé.\n---\n\nNew body." model.isDirty = true model.save() check("saves the edit", model.selected?.description == "New résumé.") @@ -70,21 +70,21 @@ enum SelfCheck { model.loadDraft() // A mutation elsewhere must not wipe an edit in progress either. - model.draft = "---\nname: auto-teste\ndescription: Half edited.\n---\n\nBody." + model.draft = "---\nname: self-check-skill\ndescription: Half edited.\n---\n\nBody." model.isDirty = true model.reload() check("a reload keeps the dirty draft on the same item", model.draft.contains("Half edited")) model.loadDraft() // Disable and enable - let created = model.items.first { $0.name == "auto-teste" }! + let created = model.items.first { $0.name == "self-check-skill" }! let selectionBeforeToggle = model.selectedID model.toggle(created) check("toggling keeps the selection", model.selectedID == selectionBeforeToggle) check( "disabling moves it to skills-off", FileManager.default.fileExists( - atPath: paths.skillsOff.appendingPathComponent("auto-teste/SKILL.md").path + atPath: paths.skillsOff.appendingPathComponent("self-check-skill/SKILL.md").path ) ) model.selection = .skills @@ -93,24 +93,24 @@ enum SelfCheck { // Switching on is a question now: which assistants load it again is the one thing the // app cannot decide for someone, so the switch opens a sheet instead of guessing. - let parked = model.items.first { $0.name == "auto-teste" }! + let parked = model.items.first { $0.name == "self-check-skill" }! model.toggle(parked) - check("enabling asks where it goes", model.restoring?.item.name == "auto-teste") + check("enabling asks where it goes", model.restoring?.item.name == "self-check-skill") check("the sheet proposes what was recorded", model.restoring?.chosen == ["claude"]) check("and says the proposal is remembered", model.restoring?.remembered == true) model.confirmRestore() check( "confirming brings it back", FileManager.default.fileExists( - atPath: paths.skills.appendingPathComponent("auto-teste/SKILL.md").path + atPath: paths.skills.appendingPathComponent("self-check-skill/SKILL.md").path ) ) // Commands: made here rather than by hand in the Finder, switched off without deleting, // and no amber banner about a `name` field they are not supposed to have. model.selection = .commands - model.createCommand(name: "auto-comando", description: "Feito pelo self-check.") - let madeCommand = model.items.first { $0.kind == .command && $0.name == "auto-comando" } + model.createCommand(name: "self-check-command", description: "Created by the self-check.") + let madeCommand = model.items.first { $0.kind == .command && $0.name == "self-check-command" } check("creates a command", madeCommand != nil) check("with no false warning on it", madeCommand?.warning == nil) check("and no name field in the file", !(model.draft.contains("name:"))) @@ -118,16 +118,16 @@ enum SelfCheck { check( "disabling a command moves it next door", FileManager.default.fileExists( - atPath: paths.claude.appendingPathComponent("commands-off/auto-comando.md").path + atPath: paths.claude.appendingPathComponent("commands-off/self-check-command.md").path ) ) - let parkedCommand = model.items.first { $0.kind == .command && $0.name == "auto-comando" }! + let parkedCommand = model.items.first { $0.kind == .command && $0.name == "self-check-command" }! check("it stays in the list, switched off", !parkedCommand.enabled) model.toggle(parkedCommand) check( "enabling it asks nothing and puts it back", model.restoring == nil && FileManager.default.fileExists( - atPath: paths.commands.appendingPathComponent("auto-comando.md").path + atPath: paths.commands.appendingPathComponent("self-check-command.md").path ) ) model.selection = .skills @@ -430,7 +430,7 @@ enum SelfCheck { // The conversation's changes reaching the document. No CLI is run here — what is being // checked is the part Loadout owns: a change accepted in a copy of the folder becomes an // unsaved edit, and only Save writes it, with the snapshot every write takes. - model.select(model.items.first { $0.name == "auto-teste" }?.id) + model.select(model.items.first { $0.name == "self-check-skill" }?.id) let skill = model.selected! let folder = skill.path!.deletingLastPathComponent() let workspaces = AskWorkspaces(paths: paths) @@ -499,9 +499,9 @@ enum SelfCheck { model.showsAskPanel = false // Delete - model.select(model.items.first { $0.name == "auto-teste" }?.id) + model.select(model.items.first { $0.name == "self-check-skill" }?.id) model.deleteSelected() - check("deleting removes it from the list", !model.items.contains { $0.name == "auto-teste" }) + check("deleting removes it from the list", !model.items.contains { $0.name == "self-check-skill" }) print(failures.isEmpty ? "\nAll good: \(total) checks passed." diff --git a/Sources/LoadoutCore/UsageIndex.swift b/Sources/LoadoutCore/UsageIndex.swift index fe88198..d12c705 100644 --- a/Sources/LoadoutCore/UsageIndex.swift +++ b/Sources/LoadoutCore/UsageIndex.swift @@ -410,7 +410,7 @@ public final class UsageIndex: @unchecked Sendable { } /// Attaches usage to a scanned inventory. Items never seen keep `Usage.none`, which is what - /// makes "nunca usada" visible in the list. + /// makes "never used" visible in the list. public func annotate(_ items: [Item], assistants: Set? = nil) -> [Item] { var byKind: [ItemKind: [String: Usage]] = [:] for kind in ItemKind.allCases { byKind[kind] = usage(kind: kind, assistants: assistants) } From c7dbf04e28587b570545c739bea3285702daa198 Mon Sep 17 00:00:00 2001 From: migsilva89 <85745515+migsilva89@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:55:37 +0100 Subject: [PATCH 2/3] Say what changed in the update dialog The feed announced a new version and described it with nothing, so the only thing in front of somebody about to press Install was the version number. Sparkle takes per-version notes from a file named after the archive in the directory generate_appcast scans, so appcast.sh now cuts this version's section out of CHANGELOG.md, turns it into the small amount of HTML the changelog actually uses, and leaves it there in the staging directory - nothing new lands in dist/. The styling follows the Mac's light or dark appearance rather than fixing black on white, which Sparkle's web view would render unreadable at night. A release whose version has no section, or whose section converts to nothing, now fails instead of publishing the blank dialog it was supposed to fix. --- CHANGELOG.md | 12 ++++++ Scripts/appcast.sh | 101 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a86f4e7..bcbebb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ Notable changes, newest first. Dates are the day the work landed on `main`. The versions are what a release is tagged as; between tags, `main` is what is being used daily. +## Unreleased + +### Fixed + +- **The update dialog says what changed.** It announced that a new version was available and then + showed an empty page, so the only way to find out what you were about to install was to go and + look the changelog up yourself — and pressing Install without knowing is what people actually + did. The feed now carries that version's own changelog section, set to be read in the small + window it appears in and following the Mac's light or dark appearance. A release whose version + has nothing written about it in the changelog is now refused, rather than published with an + empty page to show. + ## 0.3.3 — 2026-08-28 ### Changed diff --git a/Scripts/appcast.sh b/Scripts/appcast.sh index 83563cd..49d1018 100755 --- a/Scripts/appcast.sh +++ b/Scripts/appcast.sh @@ -56,11 +56,110 @@ STAGE="$(mktemp -d)" trap 'rm -rf "$STAGE"' EXIT ditto "$DMG" "$STAGE/$(basename "$UPDATE_DMG")" +# Sparkle's dialog says only that a new version is available unless the feed carries a description +# of its own, and the tooling has exactly one way to be given one: a file beside the archive in the +# directory it scans, named after that archive — Loadout--update.html next to +# Loadout--update.dmg. HTML with no DOCTYPE or body tags is embedded in the feed rather +# than linked, which is what the dialog needs, because at that moment it is being read by somebody +# deciding whether to press Install and it cannot go and fetch anything. +NOTES="$STAGE/$(basename "${UPDATE_DMG%.dmg}").html" + +# This version's section and no other: from its own '## ' heading to the next one. A +# dialog describing the previous release's work is worse than one describing none, because it is +# believed. +SECTION="$(awk -v v="$VERSION" ' + $1 == "##" && $2 == v { inside = 1; next } + $1 == "##" && inside { exit } + inside { print } +' "$ROOT/CHANGELOG.md")" + +[ -n "$(printf '%s' "$SECTION" | tr -d '[:space:]')" ] || { + echo "error: CHANGELOG.md has nothing under '## $VERSION' — the update dialog would announce" >&2 + echo " this version and then show the reader nothing about it" >&2 + exit 1 +} + +# Only what this changelog actually writes: '### Fixed'-style subheadings, '- ' bullets whose text +# wraps across lines and sometimes carries a second paragraph, **bold**, `code` and [text](url). +# Every line is escaped on the way in — one stray & or < in a release note would make the signed +# feed invalid XML, and installed copies would reject the whole thing rather than one item. +# +# Colours are left to the reader's system: Sparkle draws this in a small web view that follows dark +# mode, so a hardcoded black on white becomes unreadable the moment the Mac is dark. 'color-scheme' +# tells WebKit to pick both, and the one tint used here is a grey that works either way. +{ + cat <<-'CSS' + + CSS + + printf '%s\n' "$SECTION" | awk ' + function esc(s) { + gsub(/&/, "\\&", s); gsub(//, "\\>", s) + return s + } + function add(t) { buf = (buf == "" ? t : buf " " t) } + function para() { if (buf != "") { print "

" buf "

"; buf = "" } } + function item() { para(); if (li) { print ""; li = 0 } } + function list() { item(); if (ul) { print ""; ul = 0 } } + + /^### / { list(); print "

" esc(substr($0, 5)) "

"; next } + /^- / { item(); if (!ul) { print "