answer: Fix types, lint, and dead code

Amolith created

- Align EditorTheme.selectList with current SelectListTheme API
(selectedPrefix, selectedText, description, scrollInfo, noMatch)
- Replace non-null assertions on ctx.model and lastAssistantText with
captured locals after the early-return guards so closures see narrowed
types
- Add explicit fallback parameter to resolveExtractionModel instead
of asserting ctx.model inside the function
- Convert remaining string concatenations to template literals
- Remove unused private allQuestionsAnswered method

Change summary

packages/answer/src/index.ts | 47 +++++++++++++++++++------------------
1 file changed, 24 insertions(+), 23 deletions(-)

Detailed changes

packages/answer/src/index.ts 🔗

@@ -188,10 +188,10 @@ const PREFERRED_EXTRACTION_MODEL_ID = "nemotron-3-super-120b-a12b";
  * Resolve the extraction model: prefer a lightweight model from the registry,
  * fall back to the current session model.
  */
-function resolveExtractionModel(ctx: ExtensionContext) {
+function resolveExtractionModel(ctx: ExtensionContext, fallback: NonNullable<ExtensionContext["model"]>) {
 	const available = ctx.modelRegistry.getAvailable();
 	const preferred = available.find((m) => m.id === PREFERRED_EXTRACTION_MODEL_ID);
-	return preferred ?? ctx.model!;
+	return preferred ?? fallback;
 }
 
 /**
@@ -272,9 +272,11 @@ class QnAComponent implements Component {
 		const editorTheme: EditorTheme = {
 			borderColor: this.dim,
 			selectList: {
-				selectedBg: (s: string) => `\x1b[44m${s}\x1b[0m`,
-				matchHighlight: this.cyan,
-				itemSecondary: this.gray,
+				selectedPrefix: this.cyan,
+				selectedText: (s: string) => `\x1b[44m${s}\x1b[0m`,
+				description: this.gray,
+				scrollInfo: this.dim,
+				noMatch: this.dim,
 			},
 		};
 
@@ -296,11 +298,6 @@ class QnAComponent implements Component {
 		};
 	}
 
-	private allQuestionsAnswered(): boolean {
-		this.saveCurrentAnswer();
-		return this.answers.every((a) => (a?.trim() || "").length > 0);
-	}
-
 	private saveCurrentAnswer(): void {
 		this.answers[this.currentIndex] = this.editor.getText();
 	}
@@ -473,10 +470,10 @@ class QnAComponent implements Component {
 		};
 
 		// Title
-		lines.push(padToWidth(this.dim("╭" + horizontalLine(boxWidth - 2) + "╮")));
+		lines.push(padToWidth(this.dim(`╭${horizontalLine(boxWidth - 2)}╮`)));
 		const title = `${this.bold(this.cyan("Questions"))} ${this.dim(`(${this.currentIndex + 1}/${this.questions.length})`)}`;
 		lines.push(padToWidth(boxLine(title)));
-		lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
+		lines.push(padToWidth(this.dim(`├${horizontalLine(boxWidth - 2)}┤`)));
 
 		// Progress indicator
 		const progressParts: string[] = [];
@@ -525,7 +522,7 @@ class QnAComponent implements Component {
 				lines.push(padToWidth(boxLine(answerPrefix + editorLines[i])));
 			} else {
 				// Subsequent lines get padding to align with the first line
-				lines.push(padToWidth(boxLine("   " + editorLines[i])));
+				lines.push(padToWidth(boxLine(`   ${editorLines[i]}`)));
 			}
 		}
 
@@ -537,7 +534,7 @@ class QnAComponent implements Component {
 			const notesEditorWidth = contentWidth - 4;
 			const notesEditorLines = this.notesEditor.render(notesEditorWidth);
 			for (let i = 1; i < notesEditorLines.length - 1; i++) {
-				lines.push(padToWidth(boxLine("  " + notesEditorLines[i])));
+				lines.push(padToWidth(boxLine(`  ${notesEditorLines[i]}`)));
 			}
 		} else if (this.notesText) {
 			lines.push(padToWidth(emptyBoxLine()));
@@ -556,15 +553,15 @@ class QnAComponent implements Component {
 
 		// Confirmation dialog or footer with controls
 		if (this.showingConfirmation) {
-			lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
+			lines.push(padToWidth(this.dim(`├${horizontalLine(boxWidth - 2)}┤`)));
 			const confirmMsg = `${this.yellow("Submit all answers?")} ${this.dim("(Enter/y to confirm, Esc/n to cancel)")}`;
 			lines.push(padToWidth(boxLine(truncateToWidth(confirmMsg, contentWidth))));
 		} else {
-			lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
+			lines.push(padToWidth(this.dim(`├${horizontalLine(boxWidth - 2)}┤`)));
 			const controls = `${this.dim("Tab/Enter")} next · ${this.dim("Shift+Tab")} prev · ${this.dim("Shift+Enter")} newline · ${this.dim("Alt+N")} note · ${this.dim("Esc")} cancel`;
 			lines.push(padToWidth(boxLine(truncateToWidth(controls, contentWidth))));
 		}
-		lines.push(padToWidth(this.dim("╰" + horizontalLine(boxWidth - 2) + "╯")));
+		lines.push(padToWidth(this.dim(`╰${horizontalLine(boxWidth - 2)}╯`)));
 
 		this.cachedWidth = width;
 		this.cachedLines = lines;
@@ -613,6 +610,10 @@ export default function (pi: ExtensionAPI) {
 			return;
 		}
 
+		// Capture narrowed values so closures can use them without non-null assertions.
+		const sessionModel = ctx.model;
+		const assistantText = lastAssistantText;
+
 		// Run extraction with loader UI.
 		// The result distinguishes user cancellation from errors so we can
 		// show the right message after the custom UI closes.
@@ -622,7 +623,7 @@ export default function (pi: ExtensionAPI) {
 			| { kind: "error"; message: string };
 
 		const outcome = await ctx.ui.custom<ExtractionOutcome>((tui, theme, _kb, done) => {
-			const extractionModel = resolveExtractionModel(ctx);
+			const extractionModel = resolveExtractionModel(ctx, sessionModel);
 			const loader = new BorderedLoader(tui, theme, `Extracting questions using ${extractionModel.name}...`);
 			loader.onAbort = () => done({ kind: "cancelled" });
 
@@ -640,7 +641,7 @@ export default function (pi: ExtensionAPI) {
 					content: [
 						{
 							type: "text",
-							text: `<last_assistant_message>\n${lastAssistantText!}\n</last_assistant_message>\n\n${instructionBlock}`,
+							text: `<last_assistant_message>\n${assistantText}\n</last_assistant_message>\n\n${instructionBlock}`,
 						},
 					],
 					timestamp: Date.now(),
@@ -679,9 +680,9 @@ export default function (pi: ExtensionAPI) {
 
 				// If the preferred model errored and it's not already the
 				// session model, fall back to the session model.
-				if (result.kind === "model_error" && extractionModel.id !== ctx.model!.id) {
-					ctx.ui.notify(`${extractionModel.name} unavailable, falling back to ${ctx.model!.name}...`, "warning");
-					result = await tryExtract(ctx.model!);
+				if (result.kind === "model_error" && extractionModel.id !== sessionModel.id) {
+					ctx.ui.notify(`${extractionModel.name} unavailable, falling back to ${sessionModel.name}...`, "warning");
+					result = await tryExtract(sessionModel);
 				}
 
 				switch (result.kind) {
@@ -739,7 +740,7 @@ export default function (pi: ExtensionAPI) {
 		pi.sendMessage(
 			{
 				customType: "answers",
-				content: "I answered your questions in the following way:\n\n" + answersResult,
+				content: `I answered your questions in the following way:\n\n${answersResult}`,
 				display: true,
 			},
 			{ triggerTurn: true },