Skip to content

Commit 6cc093f

Browse files
Fixes
1 parent 4f8c4fc commit 6cc093f

10 files changed

Lines changed: 234 additions & 96 deletions

File tree

src/extension.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ function setupFileWatcher(context: vscode.ExtensionContext, workspaceRoot: strin
232232
clearTimeout(debounceTimer);
233233
}
234234
debounceTimer = setTimeout(() => {
235-
syncQuickTasks().catch((e: unknown) => {
235+
syncAndSummarise(workspaceRoot).catch((e: unknown) => {
236236
logger.error('Sync failed', { error: e instanceof Error ? e.message : 'Unknown' });
237237
});
238238
}, 2000);
@@ -272,6 +272,15 @@ async function syncQuickTasks(): Promise<void> {
272272
logger.info('syncQuickTasks END');
273273
}
274274

275+
async function syncAndSummarise(workspaceRoot: string): Promise<void> {
276+
await syncQuickTasks();
277+
await registerDiscoveredCommands(workspaceRoot);
278+
const aiEnabled = vscode.workspace.getConfiguration('commandtree').get<boolean>('enableAiSummaries', true);
279+
if (isAiEnabled(aiEnabled)) {
280+
await runSummarisation(workspaceRoot);
281+
}
282+
}
283+
275284
interface TagPattern {
276285
readonly id?: string;
277286
readonly type?: string;
@@ -375,7 +384,7 @@ async function pickOrCreateTag(existingTags: string[], taskLabel: string): Promi
375384
}
376385

377386
function initAiSummaries(workspaceRoot: string): void {
378-
const aiEnabled = vscode.workspace.getConfiguration('commandtree').get<boolean>('enableAiSummaries', false);
387+
const aiEnabled = vscode.workspace.getConfiguration('commandtree').get<boolean>('enableAiSummaries', true);
379388
if (!isAiEnabled(aiEnabled)) { return; }
380389
vscode.commands.executeCommand('setContext', 'commandtree.aiSummariesEnabled', true);
381390
runSummarisation(workspaceRoot).catch((e: unknown) => {

src/semantic/summaryPipeline.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ async function findPendingSummaries(params: {
5151
const hash = computeContentHash(content);
5252
const existing = getRow({ handle: params.handle, commandId: task.id });
5353
const needsSummary = !existing.ok
54-
|| existing.value?.contentHash !== hash;
54+
|| existing.value === undefined
55+
|| existing.value.summary === ''
56+
|| existing.value.contentHash !== hash;
5557
if (needsSummary) {
5658
pending.push({ task, content, hash });
5759
}

src/test/unit/command-registration.unit.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ suite('Command Registration Unit Tests', function () {
9393
assert.strictEqual(lintRow.contentHash, 'h2', 'Hash should reflect latest registration');
9494
});
9595

96+
test('registered command with empty summary needs summarisation even when hash matches', () => {
97+
// registerCommand writes empty summary + correct hash
98+
const hash = computeContentHash('tsc && node dist/index.js');
99+
registerCommand({ handle, commandId: 'npm:build', contentHash: hash });
100+
101+
const row = getRow({ handle, commandId: 'npm:build' });
102+
assert.ok(row.ok && row.value !== undefined);
103+
// Hash matches but summary is empty — summary pipeline MUST detect this
104+
assert.strictEqual(row.value.contentHash, hash);
105+
assert.strictEqual(row.value.summary, '', 'Summary is empty');
106+
107+
// Simulate what findPendingSummaries should do:
108+
const needsSummary = row.value.summary === '' || row.value.contentHash !== hash;
109+
assert.ok(needsSummary, 'Command with empty summary MUST be queued for summarisation');
110+
});
111+
96112
test('all discovered commands land in DB with correct content hashes', () => {
97113
const commands = [
98114
{ id: 'npm:build', content: 'tsc && node dist/index.js' },

src/test/unit/model-selection.unit.test.ts

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ const ALL_WITH_AUTO: readonly ModelRef[] = [AUTO, OPUS, HAIKU];
1818

1919
function makeDeps(overrides: Partial<ModelSelectionDeps>): ModelSelectionDeps {
2020
return {
21-
getSavedId: () => '',
22-
fetchById: async () => [],
23-
fetchAll: async () => ALL_MODELS,
24-
promptUser: async () => undefined,
25-
saveId: async () => { /* noop */ },
21+
getSavedId: (): string => '',
22+
fetchById: async (): Promise<readonly ModelRef[]> => { await Promise.resolve(); return []; },
23+
fetchAll: async (): Promise<readonly ModelRef[]> => { await Promise.resolve(); return ALL_MODELS; },
24+
promptUser: async (): Promise<ModelRef | undefined> => { await Promise.resolve(); return undefined; },
25+
saveId: async (): Promise<void> => { await Promise.resolve(); },
2626
...overrides
2727
};
2828
}
@@ -31,8 +31,8 @@ suite('Model Selection (resolveModel)', () => {
3131

3232
test('returns saved model when setting matches', async () => {
3333
const deps = makeDeps({
34-
getSavedId: () => HAIKU.id,
35-
fetchById: async (id) => id === HAIKU.id ? [HAIKU] : []
34+
getSavedId: (): string => HAIKU.id,
35+
fetchById: async (id: string): Promise<readonly ModelRef[]> => { await Promise.resolve(); return id === HAIKU.id ? [HAIKU] : []; }
3636
});
3737

3838
const result = await resolveModel(deps);
@@ -45,9 +45,9 @@ suite('Model Selection (resolveModel)', () => {
4545
test('does NOT call fetchAll when saved model found', async () => {
4646
let fetchAllCalled = false;
4747
const deps = makeDeps({
48-
getSavedId: () => HAIKU.id,
49-
fetchById: async () => [HAIKU],
50-
fetchAll: async () => { fetchAllCalled = true; return ALL_MODELS; }
48+
getSavedId: (): string => HAIKU.id,
49+
fetchById: async (): Promise<readonly ModelRef[]> => { await Promise.resolve(); return [HAIKU]; },
50+
fetchAll: async (): Promise<readonly ModelRef[]> => { await Promise.resolve(); fetchAllCalled = true; return ALL_MODELS; }
5151
});
5252

5353
await resolveModel(deps);
@@ -58,9 +58,9 @@ suite('Model Selection (resolveModel)', () => {
5858
test('does NOT call promptUser when saved model found', async () => {
5959
let promptCalled = false;
6060
const deps = makeDeps({
61-
getSavedId: () => HAIKU.id,
62-
fetchById: async () => [HAIKU],
63-
promptUser: async () => { promptCalled = true; return HAIKU; }
61+
getSavedId: (): string => HAIKU.id,
62+
fetchById: async (): Promise<readonly ModelRef[]> => { await Promise.resolve(); return [HAIKU]; },
63+
promptUser: async (): Promise<ModelRef | undefined> => { await Promise.resolve(); promptCalled = true; return HAIKU; }
6464
});
6565

6666
await resolveModel(deps);
@@ -71,10 +71,10 @@ suite('Model Selection (resolveModel)', () => {
7171
test('prompts user when no saved setting', async () => {
7272
let promptedModels: readonly ModelRef[] = [];
7373
const deps = makeDeps({
74-
getSavedId: () => '',
75-
fetchAll: async () => ALL_MODELS,
76-
promptUser: async (models) => { promptedModels = models; return HAIKU; },
77-
saveId: async () => { /* noop */ }
74+
getSavedId: (): string => '',
75+
fetchAll: async (): Promise<readonly ModelRef[]> => { await Promise.resolve(); return ALL_MODELS; },
76+
promptUser: async (models: readonly ModelRef[]): Promise<ModelRef | undefined> => { await Promise.resolve(); promptedModels = models; return HAIKU; },
77+
saveId: async (): Promise<void> => { await Promise.resolve(); }
7878
});
7979

8080
const result = await resolveModel(deps);
@@ -87,10 +87,10 @@ suite('Model Selection (resolveModel)', () => {
8787
test('saves picked model ID to settings', async () => {
8888
let savedId = '';
8989
const deps = makeDeps({
90-
getSavedId: () => '',
91-
fetchAll: async () => ALL_MODELS,
92-
promptUser: async () => HAIKU,
93-
saveId: async (id) => { savedId = id; }
90+
getSavedId: (): string => '',
91+
fetchAll: async (): Promise<readonly ModelRef[]> => { await Promise.resolve(); return ALL_MODELS; },
92+
promptUser: async (): Promise<ModelRef | undefined> => { await Promise.resolve(); return HAIKU; },
93+
saveId: async (id: string): Promise<void> => { await Promise.resolve(); savedId = id; }
9494
});
9595

9696
await resolveModel(deps);
@@ -100,8 +100,8 @@ suite('Model Selection (resolveModel)', () => {
100100

101101
test('returns error when no models available', async () => {
102102
const deps = makeDeps({
103-
getSavedId: () => '',
104-
fetchAll: async () => []
103+
getSavedId: (): string => '',
104+
fetchAll: async (): Promise<readonly ModelRef[]> => { await Promise.resolve(); return []; }
105105
});
106106

107107
const result = await resolveModel(deps);
@@ -111,9 +111,9 @@ suite('Model Selection (resolveModel)', () => {
111111

112112
test('returns error when user cancels quickpick', async () => {
113113
const deps = makeDeps({
114-
getSavedId: () => '',
115-
fetchAll: async () => ALL_MODELS,
116-
promptUser: async () => undefined
114+
getSavedId: (): string => '',
115+
fetchAll: async (): Promise<readonly ModelRef[]> => { await Promise.resolve(); return ALL_MODELS; },
116+
promptUser: async (): Promise<ModelRef | undefined> => { await Promise.resolve(); return undefined; }
117117
});
118118

119119
const result = await resolveModel(deps);
@@ -124,11 +124,11 @@ suite('Model Selection (resolveModel)', () => {
124124
test('falls back to prompt when saved model ID not found', async () => {
125125
let promptCalled = false;
126126
const deps = makeDeps({
127-
getSavedId: () => 'nonexistent-model',
128-
fetchById: async () => [],
129-
fetchAll: async () => ALL_MODELS,
130-
promptUser: async () => { promptCalled = true; return HAIKU; },
131-
saveId: async () => { /* noop */ }
127+
getSavedId: (): string => 'nonexistent-model',
128+
fetchById: async (): Promise<readonly ModelRef[]> => { await Promise.resolve(); return []; },
129+
fetchAll: async (): Promise<readonly ModelRef[]> => { await Promise.resolve(); return ALL_MODELS; },
130+
promptUser: async (): Promise<ModelRef | undefined> => { await Promise.resolve(); promptCalled = true; return HAIKU; },
131+
saveId: async (): Promise<void> => { await Promise.resolve(); }
132132
});
133133

134134
const result = await resolveModel(deps);
@@ -143,14 +143,16 @@ suite('pickConcreteModel (auto resolution)', () => {
143143

144144
test('returns specific model when preferredId is not auto', () => {
145145
const result = pickConcreteModel({ models: ALL_MODELS, preferredId: HAIKU.id });
146-
assert.strictEqual(result?.id, HAIKU.id);
147-
assert.strictEqual(result?.name, HAIKU.name);
146+
assert.ok(result, 'Expected a model to be returned');
147+
assert.strictEqual(result.id, HAIKU.id);
148+
assert.strictEqual(result.name, HAIKU.name);
148149
});
149150

150151
test('skips auto and returns first concrete model', () => {
151152
const result = pickConcreteModel({ models: ALL_WITH_AUTO, preferredId: AUTO_MODEL_ID });
152-
assert.strictEqual(result?.id, OPUS.id, 'Must skip auto and pick first concrete model');
153-
assert.notStrictEqual(result?.id, AUTO_MODEL_ID, 'Must NOT return auto model');
153+
assert.ok(result, 'Expected a concrete model');
154+
assert.strictEqual(result.id, OPUS.id, 'Must skip auto and pick first concrete model');
155+
assert.notStrictEqual(result.id, AUTO_MODEL_ID, 'Must NOT return auto model');
154156
});
155157

156158
test('returns undefined when specific model not in list', () => {
@@ -170,6 +172,7 @@ suite('pickConcreteModel (auto resolution)', () => {
170172

171173
test('auto with only concrete models picks first', () => {
172174
const result = pickConcreteModel({ models: ALL_MODELS, preferredId: AUTO_MODEL_ID });
173-
assert.strictEqual(result?.id, OPUS.id, 'Should pick first model when no auto in list');
175+
assert.ok(result, 'Expected a model');
176+
assert.strictEqual(result.id, OPUS.id, 'Should pick first model when no auto in list');
174177
});
175178
});

website/eleventy.config.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default function(eleventyConfig) {
55
site: {
66
name: "CommandTree",
77
url: "https://commandtree.dev",
8-
description: "One sidebar. Every command in your workspace.",
8+
description: "One sidebar. Every command in your workspace, one click away.",
99
stylesheet: "/assets/css/styles.css",
1010
},
1111
features: {
@@ -54,6 +54,49 @@ export default function(eleventyConfig) {
5454
return content.replace(original, replacement);
5555
});
5656

57+
const blogHeroBanner = [
58+
'<div class="blog-hero-banner">',
59+
' <div class="blog-hero-glow"></div>',
60+
' <img src="/assets/images/logo.png" alt="CommandTree logo" class="blog-hero-logo">',
61+
' <div class="blog-hero-branches">',
62+
' <span class="branch branch-1"></span>',
63+
' <span class="branch branch-2"></span>',
64+
' <span class="branch branch-3"></span>',
65+
' </div>',
66+
'</div>',
67+
].join("\n");
68+
69+
eleventyConfig.addTransform("blogHero", function(content) {
70+
if (!this.page.outputPath?.endsWith(".html")) {
71+
return content;
72+
}
73+
if (!this.page.url?.startsWith("/blog/")) {
74+
return content;
75+
}
76+
if (this.page.url === "/blog/") {
77+
return content.replaceAll(
78+
'<article class="blog-post">',
79+
'<article class="blog-post">\n' + blogHeroBanner
80+
);
81+
}
82+
return content.replace(
83+
'<div class="blog-post-content">',
84+
'<div class="blog-post-content">\n' + blogHeroBanner
85+
);
86+
});
87+
88+
eleventyConfig.addTransform("llmsTxt", function(content) {
89+
if (!this.page.outputPath?.endsWith("llms.txt")) {
90+
return content;
91+
}
92+
const apiLine = "- API Reference: https://commandtree.dev/api/";
93+
const extras = [
94+
"- GitHub: https://github.com/melbournedeveloper/CommandTree",
95+
"- VS Code Marketplace: https://marketplace.visualstudio.com/items?itemName=nimblesite.commandtree",
96+
].join("\n");
97+
return content.replace(apiLine, extras);
98+
});
99+
57100
eleventyConfig.addTransform("customScripts", function(content) {
58101
if (!this.page.outputPath?.endsWith(".html")) {
59102
return content;

website/src/_data/site.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"title": "CommandTree",
3-
"description": "One sidebar. Every command in your workspace.",
3+
"description": "One sidebar. Every command in your workspace, one click away.",
44
"url": "https://commandtree.dev",
55
"stylesheet": "/assets/css/styles.css",
66
"author": "Christian Findlay",

website/src/llms.txt.njk

Lines changed: 0 additions & 28 deletions
This file was deleted.

website/tests/docs.spec.ts

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,32 +41,24 @@ test.describe('Documentation', () => {
4141
test('discovery page loads with all sections', async ({ page }) => {
4242
await page.goto('/docs/discovery/');
4343
await expect(page.locator('h1')).toContainText('Discovery');
44-
await expect(page.locator('text=Shell Scripts')).toBeVisible();
45-
await expect(page.locator('text=NPM Scripts')).toBeVisible();
46-
await expect(page.locator('text=Makefile Targets')).toBeVisible();
47-
await expect(page.locator('text=Launch Configurations')).toBeVisible();
48-
await expect(page.locator('text=Python Scripts')).toBeVisible();
49-
await expect(page.locator('text=PowerShell Scripts')).toBeVisible();
50-
await expect(page.locator('text=Gradle Tasks')).toBeVisible();
51-
await expect(page.locator('text=Cargo Tasks')).toBeVisible();
52-
await expect(page.locator('text=Maven Goals')).toBeVisible();
53-
await expect(page.locator('text=Ant Targets')).toBeVisible();
54-
await expect(page.locator('text=Just Recipes')).toBeVisible();
55-
await expect(page.locator('text=Taskfile Tasks')).toBeVisible();
56-
await expect(page.locator('text=Deno Tasks')).toBeVisible();
57-
await expect(page.locator('text=Rake Tasks')).toBeVisible();
58-
await expect(page.locator('text=Composer Scripts')).toBeVisible();
59-
await expect(page.locator('text=Docker Compose')).toBeVisible();
60-
await expect(page.locator('.docs-content h2', { hasText: '.NET Projects' })).toBeVisible();
61-
await expect(page.locator('text=Markdown Files')).toBeVisible();
44+
const sections = [
45+
'Shell Scripts', 'NPM Scripts', 'Makefile Targets', 'Launch Configurations',
46+
'Python Scripts', 'PowerShell Scripts', 'Gradle Tasks', 'Cargo Tasks',
47+
'Maven Goals', 'Ant Targets', 'Just Recipes', 'Taskfile Tasks',
48+
'Deno Tasks', 'Rake Tasks', 'Composer Scripts', 'Docker Compose',
49+
'.NET Projects', 'Markdown Files',
50+
];
51+
for (const name of sections) {
52+
await expect(page.getByRole('heading', { name, exact: true, level: 2 })).toBeVisible();
53+
}
6254
});
6355

6456
test('execution page loads with all sections', async ({ page }) => {
6557
await page.goto('/docs/execution/');
6658
await expect(page.locator('h1')).toContainText('Execution');
67-
await expect(page.locator('text=Run in New Terminal')).toBeVisible();
68-
await expect(page.locator('text=Run in Current Terminal')).toBeVisible();
69-
await expect(page.locator('.docs-content h2', { hasText: 'Debug' })).toBeVisible();
59+
await expect(page.locator('h2', { hasText: 'Run in New Terminal' })).toBeVisible();
60+
await expect(page.locator('h2', { hasText: 'Run in Current Terminal' })).toBeVisible();
61+
await expect(page.locator('h2', { hasText: 'Debug' })).toBeVisible();
7062
});
7163

7264
test('execution page has commands table', async ({ page }) => {
@@ -81,9 +73,9 @@ test.describe('Documentation', () => {
8173
await page.goto('/docs/configuration/');
8274
await expect(page.locator('h1')).toContainText('Configuration');
8375
await expect(page.locator('h2', { hasText: 'Settings' })).toBeVisible();
84-
await expect(page.locator('text=Quick Launch')).toBeVisible();
85-
await expect(page.locator('text=Tagging')).toBeVisible();
86-
await expect(page.locator('text=Filtering')).toBeVisible();
76+
await expect(page.locator('h2', { hasText: 'Quick Launch' })).toBeVisible();
77+
await expect(page.locator('h2', { hasText: 'Tagging' })).toBeVisible();
78+
await expect(page.locator('h2', { hasText: 'Filtering' })).toBeVisible();
8779
});
8880

8981
test('configuration page has sort order table', async ({ page }) => {

website/tests/homepage.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ test.describe('Homepage', () => {
7676
'Markdown Files',
7777
];
7878
for (const name of expectedTypes) {
79-
await expect(page.locator('.command-type', { hasText: name })).toBeVisible();
79+
await expect(page.getByRole('heading', { name, exact: true, level: 4 })).toBeVisible();
8080
}
8181
});
8282

0 commit comments

Comments
 (0)