Skip to content

Commit 66eea74

Browse files
fixes
1 parent dca77e9 commit 66eea74

4 files changed

Lines changed: 115 additions & 63 deletions

File tree

coverage-thresholds.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"_agent_pmo": "f481f8d",
33
"_doc": "Single source of truth for code coverage thresholds. See REPO-STANDARDS-SPEC [COVERAGE-THRESHOLDS-JSON]. Enforced by tools/check-coverage.mjs via `make test`. Ratchet UP only. Extended format (per-metric) overrides the spec's single default_threshold to enforce both line AND branch coverage per [COVERAGE-THRESHOLDS] (VS Code extension: 80% line / 70% branch — measured values here are well above).",
4-
"lines": 91.19,
5-
"functions": 93.08,
6-
"branches": 86.76,
7-
"statements": 91.19
4+
"lines": 91.29,
5+
"functions": 93.16,
6+
"branches": 86.59,
7+
"statements": 91.29
88
}

src/db/lifecycle.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* SPEC: database-schema, DB-LOCK-RECOVERY
3-
* Singleton lifecycle management for the database.
3+
* Lifecycle management for the database. State lives in appState.
44
*/
55

66
import * as fs from "fs";
@@ -11,23 +11,22 @@ import { openDatabase, initSchema, closeDatabase } from "./db";
1111
import type { Result } from "../models/Result";
1212
import { ok, err } from "../models/Result";
1313
import { isLockError, removeLockFiles as removeLockFilesPure } from "./lockArtifacts";
14+
import { appState } from "../state";
1415

1516
const COMMANDTREE_DIR = ".commandtree";
1617
const DB_FILENAME = "commandtree.sqlite3";
1718
const LOCK_RETRY_INTERVAL_MS = 1000;
1819
const LOCK_RETRY_MAX_MS = 10000;
1920

20-
let dbHandle: DbHandle | null = null;
21-
2221
/**
2322
* SPEC: DB-LOCK-RECOVERY
24-
* Initialises the SQLite database singleton.
23+
* Initialises the SQLite database.
2524
* If the database is locked, retries for 10 seconds then
2625
* forcefully removes lock/journal files and retries.
2726
*/
2827
export async function initDb(workspaceRoot: string): Promise<Result<DbHandle, string>> {
29-
if (dbHandle !== null && fs.existsSync(dbHandle.path)) {
30-
return ok(dbHandle);
28+
if (appState.dbHandle !== null && fs.existsSync(appState.dbHandle.path)) {
29+
return ok(appState.dbHandle);
3130
}
3231
resetStaleHandle();
3332

@@ -60,8 +59,8 @@ export async function initDb(workspaceRoot: string): Promise<Result<DbHandle, st
6059
* Returns error if the database has not been initialised.
6160
*/
6261
export function getDb(): Result<DbHandle, string> {
63-
if (dbHandle !== null && fs.existsSync(dbHandle.path)) {
64-
return ok(dbHandle);
62+
if (appState.dbHandle !== null && fs.existsSync(appState.dbHandle.path)) {
63+
return ok(appState.dbHandle);
6564
}
6665
resetStaleHandle();
6766
return err("Database not initialised. Call initDb first.");
@@ -80,18 +79,18 @@ export function getDbOrThrow(): DbHandle {
8079
}
8180

8281
function resetStaleHandle(): void {
83-
if (dbHandle !== null) {
84-
closeDatabase(dbHandle);
85-
dbHandle = null;
82+
if (appState.dbHandle !== null) {
83+
closeDatabase(appState.dbHandle);
84+
appState.dbHandle = null;
8685
}
8786
}
8887

8988
/**
9089
* Disposes the database connection.
9190
*/
9291
export function disposeDb(): void {
93-
const currentDb = dbHandle;
94-
dbHandle = null;
92+
const currentDb = appState.dbHandle;
93+
appState.dbHandle = null;
9594
if (currentDb !== null) {
9695
closeDatabase(currentDb);
9796
}
@@ -110,7 +109,7 @@ function tryOpenAndInit(dbPath: string): Result<DbHandle, string> {
110109
const msg = e instanceof Error ? e.message : String(e);
111110
return err(msg);
112111
}
113-
dbHandle = openResult.value;
112+
appState.dbHandle = openResult.value;
114113
logger.info("SQLite database initialised", { path: dbPath });
115114
return ok(openResult.value);
116115
}
@@ -154,5 +153,5 @@ async function sleep(ms: number): Promise<void> {
154153

155154
// Test-only: reset internal state
156155
export function resetForTesting(): void {
157-
dbHandle = null;
156+
appState.dbHandle = null;
158157
}

src/extension.ts

Lines changed: 69 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,60 @@ import { forceSelectModel } from "./semantic/summariser";
1616
import { syncTagsFromConfig } from "./tags/tagSync";
1717
import { setupFileWatchers } from "./watchers";
1818
import { PrivateTaskDecorationProvider } from "./tree/PrivateTaskDecorationProvider";
19+
import { appState } from "./state";
1920

2021
const MAKE_EXECUTABLE_COMMAND = "commandtree.makeExecutable";
2122
const EXECUTE_PERMISSION_BITS = 0o111;
2223
const WINDOWS_PLATFORM = "win32";
2324

24-
let treeProvider: CommandTreeProvider;
25-
let quickTasksProvider: QuickTasksProvider;
26-
let taskRunner: TaskRunner;
27-
2825
export interface ExtensionExports {
2926
commandTreeProvider: CommandTreeProvider;
3027
quickTasksProvider: QuickTasksProvider;
3128
}
3229

30+
function getTreeProvider(): CommandTreeProvider {
31+
if (appState.treeProvider === undefined) {
32+
throw new Error("CommandTree extension not activated");
33+
}
34+
return appState.treeProvider;
35+
}
36+
37+
function getQuickTasksProvider(): QuickTasksProvider {
38+
if (appState.quickTasksProvider === undefined) {
39+
throw new Error("CommandTree extension not activated");
40+
}
41+
return appState.quickTasksProvider;
42+
}
43+
44+
function getTaskRunner(): TaskRunner {
45+
if (appState.taskRunner === undefined) {
46+
throw new Error("CommandTree extension not activated");
47+
}
48+
return appState.taskRunner;
49+
}
50+
3351
export async function activate(context: vscode.ExtensionContext): Promise<ExtensionExports | undefined> {
3452
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
3553
logger.info("Extension activating", { workspaceRoot });
3654
if (workspaceRoot === undefined) {
3755
logger.warn("No workspace root found, extension not activating");
3856
return undefined;
3957
}
40-
await initDatabaseSafe(workspaceRoot);
41-
treeProvider = new CommandTreeProvider(workspaceRoot);
42-
quickTasksProvider = new QuickTasksProvider();
43-
taskRunner = new TaskRunner();
58+
if (appState.activated && appState.treeProvider !== undefined && appState.quickTasksProvider !== undefined) {
59+
logger.info("Extension already activated; reusing existing state");
60+
return { commandTreeProvider: appState.treeProvider, quickTasksProvider: appState.quickTasksProvider };
61+
}
62+
appState.treeProvider = new CommandTreeProvider(workspaceRoot);
63+
appState.quickTasksProvider = new QuickTasksProvider();
64+
appState.taskRunner = new TaskRunner();
65+
appState.activated = true;
4466
registerTreeViews(context);
4567
registerCommands(context);
4668
setupWatchers(context, workspaceRoot);
69+
await initDatabaseSafe(workspaceRoot);
4770
runBackgroundStartup(workspaceRoot);
4871
logger.info("Extension activation complete");
49-
return { commandTreeProvider: treeProvider, quickTasksProvider };
72+
return { commandTreeProvider: appState.treeProvider, quickTasksProvider: appState.quickTasksProvider };
5073
}
5174

5275
function runBackgroundStartup(workspaceRoot: string): void {
@@ -90,22 +113,24 @@ function setupWatchers(context: vscode.ExtensionContext, workspaceRoot: string):
90113

91114
async function initialDiscovery(workspaceRoot: string): Promise<void> {
92115
await syncQuickTasks();
93-
logger.info("syncQuickTasks complete", { taskCount: treeProvider.getAllTasks().length });
116+
logger.info("syncQuickTasks complete", { taskCount: getTreeProvider().getAllTasks().length });
94117
await registerDiscoveredCommands(workspaceRoot);
95118
await syncTagsFromJson(workspaceRoot);
96119
}
97120

98121
function registerTreeViews(context: vscode.ExtensionContext): void {
122+
const tp = getTreeProvider();
123+
const qp = getQuickTasksProvider();
99124
context.subscriptions.push(
100125
vscode.window.createTreeView("commandtree", {
101-
treeDataProvider: treeProvider,
126+
treeDataProvider: tp,
102127
showCollapseAll: true,
103-
dragAndDropController: treeProvider,
128+
dragAndDropController: tp,
104129
}),
105130
vscode.window.createTreeView("commandtree-quick", {
106-
treeDataProvider: quickTasksProvider,
131+
treeDataProvider: qp,
107132
showCollapseAll: true,
108-
dragAndDropController: quickTasksProvider,
133+
dragAndDropController: qp,
109134
}),
110135
vscode.window.registerFileDecorationProvider(new PrivateTaskDecorationProvider())
111136
);
@@ -123,18 +148,18 @@ function registerCommands(context: vscode.ExtensionContext): void {
123148
function registerCoreCommands(context: vscode.ExtensionContext): void {
124149
context.subscriptions.push(
125150
vscode.commands.registerCommand("commandtree.refresh", async () => {
126-
await treeProvider.refresh();
127-
quickTasksProvider.updateTasks(treeProvider.getAllTasks());
151+
await getTreeProvider().refresh();
152+
getQuickTasksProvider().updateTasks(getTreeProvider().getAllTasks());
128153
vscode.window.showInformationMessage("CommandTree refreshed");
129154
}),
130155
vscode.commands.registerCommand("commandtree.run", async (item: CommandTreeItem | undefined) => {
131156
if (item !== undefined && isCommandItem(item.data)) {
132-
await taskRunner.run(item.data, "newTerminal");
157+
await getTaskRunner().run(item.data, "newTerminal");
133158
}
134159
}),
135160
vscode.commands.registerCommand("commandtree.runInCurrentTerminal", async (item: CommandTreeItem | undefined) => {
136161
if (item !== undefined && isCommandItem(item.data)) {
137-
await taskRunner.run(item.data, "currentTerminal");
162+
await getTaskRunner().run(item.data, "currentTerminal");
138163
}
139164
}),
140165
vscode.commands.registerCommand("commandtree.openPreview", async (item: CommandTreeItem | undefined) => {
@@ -160,7 +185,7 @@ function registerFilterCommands(context: vscode.ExtensionContext): void {
160185
context.subscriptions.push(
161186
vscode.commands.registerCommand("commandtree.filterByTag", handleFilterByTag),
162187
vscode.commands.registerCommand("commandtree.clearFilter", () => {
163-
treeProvider.clearFilters();
188+
getTreeProvider().clearFilters();
164189
updateFilterContext();
165190
}),
166191
vscode.commands.registerCommand("commandtree.generateSummaries", async () => {
@@ -198,9 +223,9 @@ function registerQuickCommands(context: vscode.ExtensionContext): void {
198223
async (item: CommandTreeItem | CommandItem | undefined) => {
199224
const task = extractTask(item);
200225
if (task !== undefined) {
201-
quickTasksProvider.addToQuick(task);
202-
await treeProvider.refresh();
203-
quickTasksProvider.updateTasks(treeProvider.getAllTasks());
226+
getQuickTasksProvider().addToQuick(task);
227+
await getTreeProvider().refresh();
228+
getQuickTasksProvider().updateTasks(getTreeProvider().getAllTasks());
204229
}
205230
}
206231
),
@@ -209,20 +234,20 @@ function registerQuickCommands(context: vscode.ExtensionContext): void {
209234
async (item: CommandTreeItem | CommandItem | undefined) => {
210235
const task = extractTask(item);
211236
if (task !== undefined) {
212-
quickTasksProvider.removeFromQuick(task);
213-
await treeProvider.refresh();
214-
quickTasksProvider.updateTasks(treeProvider.getAllTasks());
237+
getQuickTasksProvider().removeFromQuick(task);
238+
await getTreeProvider().refresh();
239+
getQuickTasksProvider().updateTasks(getTreeProvider().getAllTasks());
215240
}
216241
}
217242
),
218243
vscode.commands.registerCommand("commandtree.refreshQuick", () => {
219-
quickTasksProvider.refresh();
244+
getQuickTasksProvider().refresh();
220245
})
221246
);
222247
}
223248

224249
async function handleFilterByTag(): Promise<void> {
225-
const tags = treeProvider.getAllTags();
250+
const tags = getTreeProvider().getAllTags();
226251
if (tags.length === 0) {
227252
await vscode.window.showInformationMessage("No tags defined. Right-click commands to add tags.");
228253
return;
@@ -235,7 +260,7 @@ async function handleFilterByTag(): Promise<void> {
235260
placeHolder: "Select tag to filter by",
236261
});
237262
if (selected) {
238-
treeProvider.setTagFilter(selected.tag);
263+
getTreeProvider().setTagFilter(selected.tag);
239264
updateFilterContext();
240265
}
241266
}
@@ -296,12 +321,12 @@ async function handleAddTag(item: CommandTreeItem | CommandItem | undefined, tag
296321
if (task === undefined) {
297322
return;
298323
}
299-
const tagName = tagNameArg ?? (await pickOrCreateTag(treeProvider.getAllTags(), task.label));
324+
const tagName = tagNameArg ?? (await pickOrCreateTag(getTreeProvider().getAllTags(), task.label));
300325
if (tagName === undefined || tagName === "") {
301326
return;
302327
}
303-
await treeProvider.addTaskToTag(task, tagName);
304-
quickTasksProvider.updateTasks(treeProvider.getAllTasks());
328+
await getTreeProvider().addTaskToTag(task, tagName);
329+
getQuickTasksProvider().updateTasks(getTreeProvider().getAllTasks());
305330
}
306331

307332
async function handleRemoveTag(item: CommandTreeItem | CommandItem | undefined, tagNameArg?: string): Promise<void> {
@@ -324,22 +349,22 @@ async function handleRemoveTag(item: CommandTreeItem | CommandItem | undefined,
324349
}
325350
tagToRemove = selected.tag;
326351
}
327-
await treeProvider.removeTaskFromTag(task, tagToRemove);
328-
quickTasksProvider.updateTasks(treeProvider.getAllTasks());
352+
await getTreeProvider().removeTaskFromTag(task, tagToRemove);
353+
getQuickTasksProvider().updateTasks(getTreeProvider().getAllTasks());
329354
}
330355

331356
async function syncQuickTasks(): Promise<void> {
332-
await treeProvider.refresh();
333-
const allTasks = treeProvider.getAllTasks();
334-
quickTasksProvider.updateTasks(allTasks);
357+
await getTreeProvider().refresh();
358+
const allTasks = getTreeProvider().getAllTasks();
359+
getQuickTasksProvider().updateTasks(allTasks);
335360
}
336361

337362
async function syncTagsFromJson(workspaceRoot: string): Promise<void> {
338-
const allTasks = treeProvider.getAllTasks();
363+
const allTasks = getTreeProvider().getAllTasks();
339364
const synced = syncTagsFromConfig({ allTasks, workspaceRoot });
340365
if (synced) {
341-
await treeProvider.refresh();
342-
quickTasksProvider.updateTasks(treeProvider.getAllTasks());
366+
await getTreeProvider().refresh();
367+
getQuickTasksProvider().updateTasks(getTreeProvider().getAllTasks());
343368
}
344369
}
345370

@@ -370,7 +395,7 @@ async function pickOrCreateTag(existingTags: string[], taskLabel: string): Promi
370395
}
371396

372397
async function registerDiscoveredCommands(workspaceRoot: string): Promise<void> {
373-
const tasks = treeProvider.getAllTasks();
398+
const tasks = getTreeProvider().getAllTasks();
374399
if (tasks.length === 0) {
375400
return;
376401
}
@@ -400,7 +425,7 @@ function initAiSummaries(workspaceRoot: string): void {
400425
}
401426

402427
async function runSummarisation(workspaceRoot: string): Promise<void> {
403-
const tasks = treeProvider.getAllTasks();
428+
const tasks = getTreeProvider().getAllTasks();
404429
logger.info("[SUMMARY] Starting", { taskCount: tasks.length });
405430
if (tasks.length === 0) {
406431
logger.warn("[SUMMARY] No tasks to summarise");
@@ -420,8 +445,8 @@ async function runSummarisation(workspaceRoot: string): Promise<void> {
420445
return;
421446
}
422447
if (summaryResult.value > 0) {
423-
await treeProvider.refresh();
424-
quickTasksProvider.updateTasks(treeProvider.getAllTasks());
448+
await getTreeProvider().refresh();
449+
getQuickTasksProvider().updateTasks(getTreeProvider().getAllTasks());
425450
}
426451
vscode.window.showInformationMessage(`CommandTree: Summarised ${summaryResult.value} commands`);
427452
}
@@ -436,7 +461,7 @@ async function syncAndSummarise(workspaceRoot: string): Promise<void> {
436461
}
437462

438463
function updateFilterContext(): void {
439-
vscode.commands.executeCommand("setContext", "commandtree.hasFilter", treeProvider.hasFilter());
464+
vscode.commands.executeCommand("setContext", "commandtree.hasFilter", getTreeProvider().hasFilter());
440465
}
441466

442467
export function deactivate(): void {

src/state.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Centralized runtime state for the CommandTree extension.
3+
* All mutable global state lives here — no module-level `let` anywhere else.
4+
* A single `appState` instance owns the DB handle, tree providers, and runner.
5+
*/
6+
7+
import type { DbHandle } from "./db/db";
8+
import type { CommandTreeProvider } from "./CommandTreeProvider";
9+
import type { QuickTasksProvider } from "./QuickTasksProvider";
10+
import type { TaskRunner } from "./runners/TaskRunner";
11+
12+
class AppState {
13+
public dbHandle: DbHandle | null = null;
14+
public treeProvider: CommandTreeProvider | undefined = undefined;
15+
public quickTasksProvider: QuickTasksProvider | undefined = undefined;
16+
public taskRunner: TaskRunner | undefined = undefined;
17+
public activated = false;
18+
19+
public reset(): void {
20+
this.dbHandle = null;
21+
this.treeProvider = undefined;
22+
this.quickTasksProvider = undefined;
23+
this.taskRunner = undefined;
24+
this.activated = false;
25+
}
26+
}
27+
28+
export const appState = new AppState();

0 commit comments

Comments
 (0)