@@ -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