Skip to content

Commit 6cb8ae9

Browse files
committed
v0.2.6: skill footprint estimates, agent budget meter, tooltips
- Add shared skill-footprint module (listing slice vs full SKILL.md, aggregates) - Extend parser/scanner/skill JSON and RPC schema with footprint fields - Skills UI: dual metrics, agent-filter totals, Settings budget/window hints - Per-metric native tooltips; compact tok display without ~ prefix - Bun tests for footprint; exclude *.test.ts from root/node tsc Made-with: Cursor
1 parent fd30400 commit 6cb8ae9

15 files changed

Lines changed: 514 additions & 8 deletions

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "skiller",
3-
"version": "0.2.5",
3+
"version": "0.2.6",
44
"description": "A desktop app for managing AI agent skills across Claude Code, Cursor, Copilot, Gemini CLI, and more",
55
"license": "MIT",
66
"author": {
@@ -19,6 +19,7 @@
1919
"build": "electron-vite build",
2020
"preview": "electron-vite preview",
2121
"typecheck": "tsc --noEmit && tsc -p tsconfig.node.json --noEmit",
22+
"test": "bun test src/shared/skill-footprint.test.ts",
2223
"dist:mac": "electron-vite build && electron-builder --mac && bash scripts/repack-dmg.sh",
2324
"dist:win": "electron-vite build && electron-builder --win",
2425
"dist:linux": "electron-vite build && electron-builder --linux && node scripts/patch-appimage-runtime.mjs artifacts"

src/main/parser.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
import { readFileSync, statSync } from "node:fs";
22
import { parse as parseYaml } from "yaml";
33

4+
function parseFrontmatterBool(v: unknown): boolean | undefined {
5+
if (v === true) return true;
6+
if (v === false) return false;
7+
if (typeof v === "string") {
8+
const s = v.trim().toLowerCase();
9+
if (s === "true" || s === "1" || s === "yes") return true;
10+
if (s === "false" || s === "0" || s === "no") return false;
11+
}
12+
return undefined;
13+
}
14+
415
export type ParsedSkillMd = {
516
name?: string;
617
description?: string;
18+
/** Combined with description for listing-size estimates (capped per skill in footprint). */
19+
when_to_use?: string;
20+
/** When true, description slice is omitted from normal listing (manual invoke). */
21+
disable_model_invocation?: boolean;
722
metadata?: unknown;
823
body: string;
24+
/** Full SKILL.md character count (entire file). */
25+
skill_md_char_count: number;
926
};
1027

1128
type ParsedSkillCacheEntry = {
@@ -59,10 +76,22 @@ export function parseSkillMdFile(path: string): ParsedSkillMd {
5976

6077
export function parseSkillMdContent(content: string): ParsedSkillMd {
6178
const { fm, body } = splitFrontmatter(content);
79+
const whenRaw =
80+
typeof fm.when_to_use === "string"
81+
? fm.when_to_use
82+
: typeof (fm as { whenToUse?: unknown }).whenToUse === "string"
83+
? ((fm as { whenToUse: string }).whenToUse as string)
84+
: undefined;
85+
const disableRaw =
86+
parseFrontmatterBool(fm.disable_model_invocation) ??
87+
parseFrontmatterBool((fm as { disableModelInvocation?: unknown }).disableModelInvocation);
6288
return {
6389
name: typeof fm.name === "string" ? fm.name : undefined,
6490
description: typeof fm.description === "string" ? fm.description : undefined,
91+
when_to_use: whenRaw,
92+
disable_model_invocation: disableRaw,
6593
metadata: fm.metadata,
6694
body,
95+
skill_md_char_count: content.length,
6796
};
6897
}

src/main/repos.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { parse as parseToml } from "@iarna/toml";
1313
import simpleGit from "simple-git";
1414
import type { SkillJson } from "../shared/rpc-schema";
1515
import type { RepoEntryJson } from "../shared/rpc-schema";
16+
import { computeSkillFootprint } from "../shared/skill-footprint";
1617
import { installSkillFromPath } from "./install";
1718
import { parseSkillMdFile } from "./parser";
1819
import { writeProvenance } from "./provenance";
@@ -172,6 +173,14 @@ function listRepoSkillsSync(repoIdParam: string): Skill[] {
172173

173174
const dirName = basename(candidate.dir) || "unknown-skill";
174175
const skillName = candidate.parsed_name ?? dirName;
176+
const fp = computeSkillFootprint({
177+
description: parsed.description,
178+
when_to_use: parsed.when_to_use,
179+
disable_model_invocation: parsed.disable_model_invocation,
180+
skill_md_char_count: parsed.skill_md_char_count,
181+
display_name: skillName,
182+
skill_id: dirName,
183+
});
175184

176185
const source = repoUrl
177186
? ({
@@ -188,12 +197,18 @@ function listRepoSkillsSync(repoIdParam: string): Skill[] {
188197
id: dirName,
189198
name: skillName,
190199
description: parsed.description,
200+
when_to_use: parsed.when_to_use ?? null,
191201
canonical_path: candidate.dir,
192202
source,
193203
metadata: parsed.metadata,
194204
collection: null,
195205
scope: { kind: "SharedGlobal" },
196206
installations: [],
207+
footprint_listing_source_chars: fp.footprint_listing_source_chars,
208+
footprint_listing_slice_chars: fp.footprint_listing_slice_chars,
209+
footprint_name_chars: fp.footprint_name_chars,
210+
footprint_skill_md_chars: fp.footprint_skill_md_chars,
211+
listing_excluded: fp.listing_excluded,
197212
});
198213
}
199214

src/main/scanner.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { existsSync, lstatSync, readdirSync, statSync } from "node:fs";
22
import { basename, dirname, join } from "node:path";
3+
import { computeSkillFootprint } from "../shared/skill-footprint";
4+
import type { ParsedSkillMd } from "./parser";
35
import type { AgentConfig } from "./types";
46
import { parseSkillMdFile } from "./parser";
57
import { readProvenanceRaw } from "./provenance";
@@ -96,6 +98,25 @@ function collectionFromRealPath(realOrLink: string, skillsRoot: string): string
9698
return undefined;
9799
}
98100

101+
function listingFootprintFromParsed(parsed: ParsedSkillMd, rawName: string, dirName: string) {
102+
const fp = computeSkillFootprint({
103+
description: parsed.description,
104+
when_to_use: parsed.when_to_use,
105+
disable_model_invocation: parsed.disable_model_invocation,
106+
skill_md_char_count: parsed.skill_md_char_count,
107+
display_name: rawName,
108+
skill_id: dirName,
109+
});
110+
return {
111+
when_to_use: parsed.when_to_use ?? null,
112+
footprint_listing_source_chars: fp.footprint_listing_source_chars,
113+
footprint_listing_slice_chars: fp.footprint_listing_slice_chars,
114+
footprint_name_chars: fp.footprint_name_chars,
115+
footprint_skill_md_chars: fp.footprint_skill_md_chars,
116+
listing_excluded: fp.listing_excluded,
117+
};
118+
}
119+
99120
function resolveSource(
100121
skillId: string,
101122
canonical: string,
@@ -173,6 +194,7 @@ function scanInheritedRoot(
173194
const dirName = basename(skillDir);
174195
const collection = detectCollection(skillDir, root);
175196
const skillName = parsed.name ?? dirName;
197+
const fp = listingFootprintFromParsed(parsed, skillName, dirName);
176198

177199
const installation: SkillInstallation = {
178200
agent_slug: agent.slug,
@@ -186,6 +208,7 @@ function scanInheritedRoot(
186208
id: dirName,
187209
name: skillName,
188210
description: parsed.description,
211+
...fp,
189212
canonical_path: canonical,
190213
source: resolveSource(dirName, canonical, provenance),
191214
metadata: parsed.metadata,
@@ -234,6 +257,7 @@ function scanSkillMdRoot(
234257
const symlink = isSymlink(skillDir);
235258
const collection = detectCollection(skillDir, root);
236259
const skillId = dirName;
260+
const fp = listingFootprintFromParsed(parsed, rawName, dirName);
237261

238262
const installation: SkillInstallation = {
239263
agent_slug: agent.slug,
@@ -249,6 +273,7 @@ function scanSkillMdRoot(
249273
id: skillId,
250274
name: rawName,
251275
description: parsed.description,
276+
...fp,
252277
canonical_path: canonical,
253278
source: resolveSource(skillId, canonical, provenance),
254279
metadata: parsed.metadata,

src/main/skill-json.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,18 @@ export function skillToJson(s: Skill): SkillJson {
4343
id: s.id,
4444
name: s.name,
4545
description: s.description ?? null,
46+
when_to_use: s.when_to_use ?? null,
4647
canonical_path: s.canonical_path,
4748
source: s.source ? skillSourceToJson(s.source) : null,
4849
metadata: s.metadata ?? null,
4950
collection: s.collection ?? null,
5051
scope: skillScopeToJson(s.scope),
5152
installations: s.installations.map(skillInstallationToJson),
53+
footprint_listing_source_chars: s.footprint_listing_source_chars,
54+
footprint_listing_slice_chars: s.footprint_listing_slice_chars,
55+
footprint_name_chars: s.footprint_name_chars,
56+
footprint_skill_md_chars: s.footprint_skill_md_chars,
57+
listing_excluded: s.listing_excluded,
5258
};
5359
}
5460

src/main/skill-types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,24 @@ export interface Skill {
2323
id: string;
2424
name: string;
2525
description?: string | null;
26+
/** Frontmatter when_to_use (combined with description for listing-size estimate). */
27+
when_to_use?: string | null;
2628
canonical_path: string;
2729
source?: SkillSource | null;
2830
metadata?: unknown;
2931
collection?: string | null;
3032
scope: SkillScope;
3133
installations: SkillInstallation[];
34+
/** Len(description + when_to_use) before per-entry cap. */
35+
footprint_listing_source_chars: number;
36+
/** Capped description+when slice used for listing estimate (0 if disable_model_invocation). */
37+
footprint_listing_slice_chars: number;
38+
/** Display name length (names always included in listing). */
39+
footprint_name_chars: number;
40+
/** Full SKILL.md file size in characters. */
41+
footprint_skill_md_chars: number;
42+
/** True when model listing omits description slice (manual invoke). */
43+
listing_excluded: boolean;
3244
}
3345

3446
export interface UpdateProgress {

src/mainview/hooks/useSkills.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,19 @@ export interface Skill {
1717
id: string;
1818
name: string;
1919
description: string | null;
20+
when_to_use?: string | null;
2021
canonical_path: string;
2122
source: unknown;
2223
metadata: unknown;
2324
collection: string | null;
2425
scope: SkillScope;
2526
installations: SkillInstallation[];
27+
/** Listing estimate: raw len(description + when_to_use) before per-skill cap. */
28+
footprint_listing_source_chars?: number;
29+
footprint_listing_slice_chars?: number;
30+
footprint_name_chars?: number;
31+
footprint_skill_md_chars?: number;
32+
listing_excluded?: boolean;
2633
}
2734

2835
/** Direct (non-inherited) agent slugs */

src/mainview/i18n/en.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,20 @@ const en = {
170170
skillRowMenu: "Skill actions",
171171
expandCollection: "Expand collection",
172172
collapseCollection: "Collapse collection",
173+
packageSizeLabel: "Size",
174+
tokenTooltipListing:
175+
"Listing side: description + when_to_use (capped per skill). Approximate tokens = character count ÷ 4; not your model tokenizer.",
176+
tokenTooltipFull:
177+
"Full skill file (SKILL.md). Approximate tokens = character count ÷ 4; not your model tokenizer.",
178+
agentTokensBarOverview:
179+
"Approximate token totals for skills in this view. Hover each value for what it measures.",
180+
agentTokenTooltipListingSum:
181+
"Sum of listing-side estimates: each skill’s description + when_to_use slice (capped), then chars ÷ 4.",
182+
agentTokenTooltipFullSum:
183+
"Sum of full-file estimates: entire SKILL.md per skill, chars ÷ 4.",
184+
listingExcludedTooltip:
185+
"Hidden from the agent’s automatic skill list; invoke manually if your tool supports it.",
186+
listingExcludedShort: "Excluded from listing",
173187
},
174188

175189
// === Marketplace ===

src/mainview/pages/Settings.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ interface AppSettings {
2424
path_overrides: Record<string, string[]> | null
2525
close_action: string | null
2626
macos_window_blur?: boolean | null
27+
assumed_listing_char_budget?: number | null
28+
assumed_context_window_chars?: number | null
2729
}
2830

2931
const DEFAULT_SETTINGS: AppSettings = {

0 commit comments

Comments
 (0)