Skip to content

Commit b995c55

Browse files
committed
Surface progress file errors in UI and LLM contexts
1 parent 738be0a commit b995c55

2 files changed

Lines changed: 100 additions & 48 deletions

File tree

source/vscode/src/gh-copilot/learningTools.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,14 @@ export class LearningTools {
107107
* so the caller can decide whether to prompt for initialization.
108108
*/
109109
async getState(): Promise<
110-
{ initialized: false } | ({ initialized: true } & StateSnapshot)
110+
| { initialized: false; error?: string }
111+
| ({ initialized: true } & StateSnapshot)
111112
> {
112113
if (!this.service.initialized) {
114+
const errorMsg = this.service.progressFileError;
115+
if (errorMsg) {
116+
return { initialized: false, error: errorMsg };
117+
}
113118
const detected = await detectLearningWorkspace();
114119
if (!detected) {
115120
return { initialized: false };

source/vscode/src/learning/service.ts

Lines changed: 94 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,19 @@ export class LearningService {
110110
private _progressFileWatcher: vscode.FileSystemWatcher | undefined;
111111
private _writingProgress = false;
112112
private _initPromise: Promise<boolean> | undefined;
113+
private _progressCorruptError: string | undefined;
113114

114115
constructor(private readonly extensionUri: vscode.Uri) {}
115116

116117
get initialized(): boolean {
117118
return this.workspace !== undefined;
118119
}
119120

121+
/** Non-null when the progress file is corrupt and blocks initialization. */
122+
get progressFileError(): string | undefined {
123+
return this._progressCorruptError;
124+
}
125+
120126
get learningContentRoot(): vscode.Uri {
121127
return this.requireWorkspace().learningContentRoot;
122128
}
@@ -160,7 +166,13 @@ export class LearningService {
160166

161167
dispose(): void {
162168
if (this.workspace) {
163-
this.saveProgress().catch(() => {});
169+
this.saveProgress().catch((err) => {
170+
vscode.window.showWarningMessage(
171+
`Could not save learning progress: ${
172+
err instanceof Error ? err.message : String(err)
173+
}`,
174+
);
175+
});
164176
}
165177
this._onDidChangeState.dispose();
166178
this._onDidChangeProgress.dispose();
@@ -655,10 +667,18 @@ export class LearningService {
655667
const detected = await detectLearningWorkspace();
656668

657669
if (detected) {
658-
await this.loadWorkspace(
659-
detected.workspaceRoot,
660-
detected.learningContentRoot,
661-
);
670+
try {
671+
await this.loadWorkspace(
672+
detected.workspaceRoot,
673+
detected.learningContentRoot,
674+
);
675+
} catch (err) {
676+
if (this._progressCorruptError) {
677+
vscode.window.showErrorMessage(this._progressCorruptError);
678+
return false;
679+
}
680+
throw err;
681+
}
662682
this.startWatcher();
663683
sendTelemetryEvent(
664684
EventType.LearningSessionStarted,
@@ -807,7 +827,10 @@ export class LearningService {
807827
{ key: "r", label: "Reset", action: "reset" },
808828
],
809829
];
810-
return [primaryGroup, ...extraGroups, navGroup].filter(
830+
const resetGroup: ActionGroup = [
831+
{ key: "x", label: "Reset", action: "reset" },
832+
];
833+
return [primaryGroup, ...extraGroups, resetGroup, navGroup].filter(
811834
(g) => g.length > 0,
812835
);
813836
}
@@ -981,52 +1004,69 @@ export class LearningService {
9811004
}
9821005

9831006
private async loadProgress(ws: WorkspaceState): Promise<void> {
1007+
let bytes: Uint8Array;
1008+
try {
1009+
bytes = await vscode.workspace.fs.readFile(ws.learningFile);
1010+
} catch {
1011+
// File doesn't exist yet — use defaults.
1012+
ws.progressData = {
1013+
version: 1,
1014+
position: {
1015+
courseId: ws.catalog.id,
1016+
unitId: ws.catalog.units[0]?.id ?? "",
1017+
activityId: ws.catalog.units[0]?.activities[0]?.id ?? "",
1018+
},
1019+
completions: {},
1020+
startedAt: new Date().toISOString(),
1021+
};
1022+
return;
1023+
}
1024+
1025+
// File exists — parse and validate.
1026+
let parsed: unknown;
9841027
try {
985-
const bytes = await vscode.workspace.fs.readFile(ws.learningFile);
986-
const parsed = JSON.parse(new TextDecoder().decode(bytes));
987-
if (
988-
parsed &&
989-
typeof parsed === "object" &&
990-
parsed.version === 1 &&
991-
typeof parsed.completions === "object" &&
992-
parsed.completions !== null &&
993-
typeof parsed.position === "object" &&
994-
parsed.position !== null
995-
) {
996-
ws.progressData = parsed as ProgressFileData;
997-
// Validate saved position references a known unit and activity
998-
if (ws.catalog.units.length > 0) {
999-
const unit = ws.catalog.units.find(
1000-
(k) => k.id === ws.progressData.position.unitId,
1028+
parsed = JSON.parse(new TextDecoder().decode(bytes));
1029+
} catch {
1030+
this._progressCorruptError =
1031+
"The qdk-learning.json file contains invalid JSON. Fix or delete the file to continue.";
1032+
throw new Error(this._progressCorruptError);
1033+
}
1034+
1035+
if (
1036+
parsed &&
1037+
typeof parsed === "object" &&
1038+
(parsed as any).version === 1 &&
1039+
typeof (parsed as any).completions === "object" &&
1040+
(parsed as any).completions !== null &&
1041+
typeof (parsed as any).position === "object" &&
1042+
(parsed as any).position !== null
1043+
) {
1044+
ws.progressData = parsed as ProgressFileData;
1045+
// Validate saved position references a known unit and activity
1046+
if (ws.catalog.units.length > 0) {
1047+
const unit = ws.catalog.units.find(
1048+
(k) => k.id === ws.progressData.position.unitId,
1049+
);
1050+
const activityValid =
1051+
unit &&
1052+
unit.activities.some(
1053+
(s) => s.id === ws.progressData.position.activityId,
10011054
);
1002-
const activityValid =
1003-
unit &&
1004-
unit.activities.some(
1005-
(s) => s.id === ws.progressData.position.activityId,
1006-
);
1007-
if (!activityValid) {
1008-
ws.progressData.position = {
1009-
courseId: ws.catalog.id,
1010-
unitId: ws.catalog.units[0].id,
1011-
activityId: ws.catalog.units[0].activities[0]?.id ?? "",
1012-
};
1013-
}
1055+
if (!activityValid) {
1056+
ws.progressData.position = {
1057+
courseId: ws.catalog.id,
1058+
unitId: ws.catalog.units[0].id,
1059+
activityId: ws.catalog.units[0].activities[0]?.id ?? "",
1060+
};
10141061
}
1015-
return;
10161062
}
1017-
} catch {
1018-
// expected when file is missing or corrupt
1063+
return;
10191064
}
1020-
ws.progressData = {
1021-
version: 1,
1022-
position: {
1023-
courseId: ws.catalog.id,
1024-
unitId: ws.catalog.units[0]?.id ?? "",
1025-
activityId: ws.catalog.units[0]?.activities[0]?.id ?? "",
1026-
},
1027-
completions: {},
1028-
startedAt: new Date().toISOString(),
1029-
};
1065+
1066+
// File exists but has unexpected structure.
1067+
this._progressCorruptError =
1068+
"The qdk-learning.json file is corrupt (unexpected structure). Fix or delete the file to continue.";
1069+
throw new Error(this._progressCorruptError);
10301070
}
10311071

10321072
private async saveProgress(): Promise<void> {
@@ -1038,6 +1078,13 @@ export class LearningService {
10381078
ws.learningFile,
10391079
new TextEncoder().encode(json),
10401080
);
1081+
} catch (err) {
1082+
throw new Error(
1083+
`Failed to save learning progress: ${
1084+
err instanceof Error ? err.message : String(err)
1085+
}`,
1086+
{ cause: err },
1087+
);
10411088
} finally {
10421089
this._writingProgress = false;
10431090
}

0 commit comments

Comments
 (0)