library: Claude Desktop's skills list shows again, each skill's updatedAt an ISO time (mingxitang, #863) — magpie wrote a skill's updatedAt in the kind lastUpdated had, milliseconds where it is a number, but Desktop's own entries are ISO times or null and listLocalSkills checks the whole list at once: one number and the skills page stayed loading (failed to pass validation). Now updatedAt is always an ISO time and lastUpdated keeps its own kind; an entry written as milliseconds before is written as the same moment in ISO at the next sync, its other keys as they were. TestClaudeDesktopSkills: the entry of a manifest with millisecond lastUpdated, and an old numeric entry fixed at a sync; it fails on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (five timing tests under load 20 passed when run again).

This commit is contained in:
yetone committed 2026-10-05 12:43:24 +08:00
1 parent 69a7744a9f
commit 50d6c9a394
2 files changed
+66 -12

No files matched your search

+41 -10
View File
@@ -106,17 +106,45 @@ func desktopEdited(p string) bool {
return false
}
// desktopStamp is now, as the manifest's other times are written: a
// number of milliseconds where they are numbers, else an ISO time.
// desktopStamp is now as the manifest's lastUpdated is written: a number
// of milliseconds where it is one, else an ISO time.
func desktopStamp(raw []byte) any {
t := gjson.GetBytes(raw, "lastUpdated")
if !t.Exists() {
t = gjson.GetBytes(raw, "skills.0.updatedAt")
}
if t.Type == gjson.Number {
if gjson.GetBytes(raw, "lastUpdated").Type == gjson.Number {
return time.Now().UnixMilli()
}
return time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
return isoStamp(time.Now())
}
// isoStamp is t as a skill's updatedAt is written. Desktop checks the whole
// list of skills at once and takes each updatedAt to be such a string (or
// null), whatever lastUpdated is: one number in it and none of the skills
// are listed (#863).
func isoStamp(t time.Time) string { return t.UTC().Format("2006-01-02T15:04:05.000Z") }
// fixUpdatedAt is the entry with its updatedAt, when a number of
// milliseconds (as magpie wrote them before #863), written as Desktop's
// are, the other keys as they were; "" when it isn't a number.
func fixUpdatedAt(e gjson.Result) string {
if e.Get("updatedAt").Type != gjson.Number {
return ""
}
v, _ := json.Marshal(isoStamp(time.UnixMilli(e.Get("updatedAt").Int())))
var b strings.Builder
b.WriteByte('{')
e.ForEach(func(k, x gjson.Result) bool {
if b.Len() > 1 {
b.WriteByte(',')
}
b.WriteString(k.Raw + ":")
if k.String() == "updatedAt" {
b.Write(v)
} else {
b.WriteString(x.Raw)
}
return true
})
b.WriteByte('}')
return b.String()
}
// manifestEntry is a manifest's entry for the skill with that id, "" for
@@ -140,7 +168,7 @@ func writeManifest(root string, put map[string]bool, drop []string) error {
if err != nil && isFile(path) {
return err
}
stamp := desktopStamp(raw)
stamp, updated := desktopStamp(raw), isoStamp(time.Now())
var list []json.RawMessage
changed := false
done := map[string]bool{}
@@ -156,7 +184,7 @@ func writeManifest(root string, put map[string]bool, drop []string) error {
// in the order Desktop writes them, any other key of the old
// entry after them
keys := []string{"skillId", "name", "description", "creatorType", "syncManaged", "updatedAt", "enabled"}
vals := []any{name, name, desc, "user", false, stamp, enabled}
vals := []any{name, name, desc, "user", false, updated, enabled}
var b strings.Builder
b.WriteByte('{')
for i, k := range keys {
@@ -183,6 +211,9 @@ func writeManifest(root string, put map[string]bool, drop []string) error {
case put[id] && !done[id]:
list = append(list, entry(id, e.Raw))
done[id], changed = true, true
case fixUpdatedAt(e) != "":
list = append(list, json.RawMessage(fixUpdatedAt(e)))
changed = true
default:
list = append(list, json.RawMessage(e.Raw))
}
+25 -2
View File
@@ -130,8 +130,13 @@ func TestClaudeDesktopSkills(t *testing.T) {
if len(es) != 3 || es["frontend-design"].CreatorType != "anthropic" || es["frontend-design"].Enabled || es["test"].Description != "Test" {
t.Errorf("Desktop's own entries changed: %+v", es)
}
if _, ok := desktopEntries(t, b)["pdf"].UpdatedAt.(float64); !ok {
t.Errorf("a manifest of millisecond times got another kind: %+v", desktopEntries(t, b)["pdf"])
// #863: a skill's updatedAt is an ISO time even where lastUpdated is
// milliseconds, which stays so
if u, ok := desktopEntries(t, b)["pdf"].UpdatedAt.(string); !ok || !strings.HasSuffix(u, "Z") {
t.Errorf("updatedAt isn't an ISO time: %+v", desktopEntries(t, b)["pdf"])
}
if !strings.Contains(read(t, filepath.Join(b, desktopManifest)), `"lastUpdated": 17`) {
t.Errorf("lastUpdated's milliseconds changed kind:\n%s", read(t, filepath.Join(b, desktopManifest)))
}
// the other keys of a manifest, and its layout, stay
if s := read(t, filepath.Join(a, desktopManifest)); !strings.HasPrefix(s, "{\n \"lastUpdated\"") {
@@ -206,4 +211,22 @@ func TestClaudeDesktopSkills(t *testing.T) {
if _, err := os.Stat(filepath.Join(b, "skills", "pdf", "SKILL.md")); err != nil {
t.Error(err)
}
// #863: an updatedAt magpie wrote as milliseconds before is written as
// an ISO time at the next sync, the same moment, the entry's other keys
// and Desktop's own entries as they were
write(t, m, strings.Replace(read(t, m), `"skills": [`, `"skills": [{"skillId":"ponytail","name":"ponytail","syncManaged":true,"updatedAt":1759536000000,"enabled":true},`, 1))
ok(t)(Sync())
es = desktopEntries(t, b)
if es["ponytail"].UpdatedAt != "2025-10-04T00:00:00.000Z" || !es["ponytail"].SyncManaged || !es["ponytail"].Enabled {
t.Errorf("the old entry: %+v\n%s", es["ponytail"], read(t, m))
}
for id, e := range es {
if _, num := e.UpdatedAt.(float64); num {
t.Errorf("%s: updatedAt is still a number", id)
}
}
if !strings.Contains(read(t, m), `{"skillId":"ponytail","name":"ponytail","syncManaged":true,"updatedAt":"2025-10-04T00:00:00.000Z","enabled":true}`) {
t.Errorf("the entry's keys moved:\n%s", read(t, m))
}
}