From feab25bfa191e0de69665672b985b6e57ac75703 Mon Sep 17 00:00:00 2001 From: "Tom Liu (Liu Hsiu-Chi)" <0xtomlord1122@gmail.com> Date: Sun, 18 Oct 2026 22:41:00 +0800 Subject: [PATCH 1/3] fix(skills): clear the skills badge when opening skills The sidebar dot stayed on after the user opened Skills because only an opened live skill was marked seen, and the page defaults to drafts. Opening Skills now clears the update badge. Parent write_skill also publishes the same update event so a direct skill write lights the dot. Signed-off-by: Tom Liu (Liu Hsiu-Chi) <0xtomlord1122@gmail.com> --- .../shell/app-shell-shortcuts.svelte.ts | 2 + .../components/sidebar/SidebarFooter.svelte | 5 ++- .../lib/stores/skill-updates.svelte.test.ts | 5 +++ cometmind/internal/tools/registry_add.go | 2 +- .../internal/tools/skills/write_skill.go | 19 +++++++- .../internal/tools/skills/write_skill_test.go | 43 +++++++++++++++++++ 6 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 cometmind/internal/tools/skills/write_skill_test.go diff --git a/cometline/src/lib/features/shell/app-shell-shortcuts.svelte.ts b/cometline/src/lib/features/shell/app-shell-shortcuts.svelte.ts index 5c3f6075..95ffd9ef 100644 --- a/cometline/src/lib/features/shell/app-shell-shortcuts.svelte.ts +++ b/cometline/src/lib/features/shell/app-shell-shortcuts.svelte.ts @@ -11,6 +11,7 @@ import { openSettings } from '#lib/actions/open-settings.js'; import { shouldUseWorkspacePanelHistory } from '#lib/features/shell/focus-nav.js'; import { isReloadShortcut, matchesShortcut, type ShortcutAction } from '#lib/keyboard-shortcuts.js'; import { inboxStore } from '#lib/stores/inbox.svelte.js'; +import { skillUpdatesStore } from '#lib/stores/skill-updates.svelte.js'; import { sessionStore } from '#lib/stores/session.svelte.js'; import { settingsStore } from '#lib/stores/settings.svelte.js'; import { shellStore } from '#lib/stores/shell.svelte.js'; @@ -218,6 +219,7 @@ export function createAppShellShortcuts(deps: { case 'openSkillDrafts': if (shellStore.settingsOpen) shellStore.closeSettings(); inboxStore.closeDrawer(); + skillUpdatesStore.markSeen(); void goto(resolve('skills')); return; case 'openGallery': diff --git a/cometline/src/lib/features/sidebar/components/sidebar/SidebarFooter.svelte b/cometline/src/lib/features/sidebar/components/sidebar/SidebarFooter.svelte index 29d119b8..1bc5623c 100644 --- a/cometline/src/lib/features/sidebar/components/sidebar/SidebarFooter.svelte +++ b/cometline/src/lib/features/sidebar/components/sidebar/SidebarFooter.svelte @@ -36,7 +36,10 @@ class="nav-badge" class:has-badge={skillDraftsStore.hasDrafts || skillUpdatesStore.hasUnseen} class:active={page.url.pathname === '/skills' || page.url.pathname === '/skill-drafts'} - onclick={() => goto(resolve('skills'))} + onclick={() => { + skillUpdatesStore.markSeen(); + void goto(resolve('skills')); + }} > diff --git a/cometline/src/lib/stores/skill-updates.svelte.test.ts b/cometline/src/lib/stores/skill-updates.svelte.test.ts index b53f7e0f..1fb98d67 100644 --- a/cometline/src/lib/stores/skill-updates.svelte.test.ts +++ b/cometline/src/lib/stores/skill-updates.svelte.test.ts @@ -22,5 +22,10 @@ describe('skillUpdatesStore', () => { skillUpdatesStore.note(['ship-checklist']); expect(skillUpdatesStore.hasUnseen).toBe(false); + + skillUpdatesStore.note(['ship-checklist', 'retry-pattern']); + expect(skillUpdatesStore.unseen).toEqual(['retry-pattern']); + skillUpdatesStore.markSeen(); + expect(skillUpdatesStore.hasUnseen).toBe(false); }); }); diff --git a/cometmind/internal/tools/registry_add.go b/cometmind/internal/tools/registry_add.go index 9c6ac96d..93a18e31 100644 --- a/cometmind/internal/tools/registry_add.go +++ b/cometmind/internal/tools/registry_add.go @@ -53,7 +53,7 @@ func (r *Registry) addSkillTools(surface ToolSurface, opt RegistryOptions) { r.Add(ReadSkillDraft{}) } if surface.SkillMut { - r.Add(WriteSkill{}) + r.Add(WriteSkill{Events: opt.MemoryEvents}) r.Add(PromoteSkillDraft{}) } } diff --git a/cometmind/internal/tools/skills/write_skill.go b/cometmind/internal/tools/skills/write_skill.go index b962b97d..7da33cd3 100644 --- a/cometmind/internal/tools/skills/write_skill.go +++ b/cometmind/internal/tools/skills/write_skill.go @@ -3,13 +3,17 @@ package skills import ( "context" "encoding/json" + "strings" + "github.com/Cometline/cometline/cometmind/internal/event" "github.com/Cometline/cometline/cometmind/internal/skills" "github.com/Cometline/cometline/cometmind/internal/tools/toolkit" ) // WriteSkill creates or updates an Agent Skill under ~/.cometmind/skills. -type WriteSkill struct{} +type WriteSkill struct { + Events *event.Hub +} func (WriteSkill) Spec() ToolSpec { return ToolSpec{ @@ -27,7 +31,7 @@ func (WriteSkill) Spec() ToolSpec { } } -func (WriteSkill) Execute(_ context.Context, input json.RawMessage) (Result, error) { +func (t WriteSkill) Execute(ctx context.Context, input json.RawMessage) (Result, error) { var in struct { Name *string `json:"name"` Content *string `json:"content"` @@ -47,5 +51,16 @@ func (WriteSkill) Execute(_ context.Context, input json.RawMessage) (Result, err if err := skills.WriteSkill(name, content, in.Overwrite); err != nil { return Result{OK: false, Output: err.Error()}, nil } + if t.Events != nil { + action := "created" + if in.Overwrite { + action = "updated" + } + t.Events.Publish(event.SkillReviewUpdated(toolkit.ToolSessionFrom(ctx), []event.SkillReviewChange{{ + Name: name, + Action: action, + Description: strings.TrimSpace(skills.SkillMarkdownDescription(content)), + }})) + } return Result{OK: true, Output: "skill " + name + " written to ~/.cometmind/skills/" + name + "/SKILL.md"}, nil } diff --git a/cometmind/internal/tools/skills/write_skill_test.go b/cometmind/internal/tools/skills/write_skill_test.go new file mode 100644 index 00000000..b59a5516 --- /dev/null +++ b/cometmind/internal/tools/skills/write_skill_test.go @@ -0,0 +1,43 @@ +package skills + +import ( + "encoding/json" + "testing" + "time" + + "github.com/Cometline/cometline/cometmind/internal/event" +) + +func TestWriteSkillPublishesUpdate(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + hub := event.NewHub() + sub := hub.Subscribe() + defer sub.Close() + + content := "---\nname: ship-checklist\ndescription: Ship a reviewed change\n---\n\n# Steps\n" + payload, err := json.Marshal(map[string]any{"name": "ship-checklist", "content": content}) + if err != nil { + t.Fatal(err) + } + ctx := WithToolSession(t.Context(), "sess-1") + res, err := (WriteSkill{Events: hub}).Execute(ctx, payload) + if err != nil { + t.Fatal(err) + } + if !res.OK { + t.Fatalf("write failed: %s", res.Output) + } + + select { + case ev := <-sub.Events: + if ev.Kind != event.KindSkillReviewUpdated || ev.SessionID != "sess-1" || len(ev.SkillReviews) != 1 { + t.Fatalf("event = %+v", ev) + } + got := ev.SkillReviews[0] + if got.Name != "ship-checklist" || got.Action != "created" || got.Description != "Ship a reviewed change" { + t.Fatalf("change = %+v", got) + } + case <-time.After(time.Second): + t.Fatal("missing skill update event") + } +} From f4aad4c1852915d9c2c80c5f6a712d2ab4a95057 Mon Sep 17 00:00:00 2001 From: "Tom Liu (Liu Hsiu-Chi)" <0xtomlord1122@gmail.com> Date: Sun, 18 Oct 2026 22:41:03 +0800 Subject: [PATCH 2/3] feat(skills): show when self-improvement skills expire Unpinned self-improvement skills archive 30 days after last use, then delete 10 days after archive. The skills page now shows that countdown on the list, preview, and archived rows. Signed-off-by: Tom Liu (Liu Hsiu-Chi) <0xtomlord1122@gmail.com> --- .../skills-page/SkillsBrowseView.svelte | 32 +++++++++++++++++-- .../lib/features/skills/skill-expiry.test.ts | 23 +++++++++++++ .../src/lib/features/skills/skill-expiry.ts | 15 +++++++++ .../lib/generated/cometmind-api/types.gen.ts | 4 +++ cometmind/internal/apigen/types.gen.go | 3 ++ .../internal/server/skill_curator_handlers.go | 3 ++ cometmind/internal/server/skill_handlers.go | 8 ++++- cometmind/internal/skillcurator/service.go | 29 +++++++++++++++-- cometmind/internal/skills/curator.go | 26 +++++++++++++++ cometmind/internal/skills/curator_test.go | 24 ++++++++++++++ cometmind/openapi.yaml | 4 +++ 11 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 cometline/src/lib/features/skills/skill-expiry.test.ts create mode 100644 cometline/src/lib/features/skills/skill-expiry.ts diff --git a/cometline/src/lib/features/skills/components/skills-page/SkillsBrowseView.svelte b/cometline/src/lib/features/skills/components/skills-page/SkillsBrowseView.svelte index 2f8525a1..7011879e 100644 --- a/cometline/src/lib/features/skills/components/skills-page/SkillsBrowseView.svelte +++ b/cometline/src/lib/features/skills/components/skills-page/SkillsBrowseView.svelte @@ -1,6 +1,7 @@