diff --git a/.chronus/changes/copilot-fix-html-program-viewer-crash-2026-5-8-19-28-50.md b/.chronus/changes/copilot-fix-html-program-viewer-crash-2026-5-8-19-28-50.md
new file mode 100644
index 00000000000..4197ab2f74f
--- /dev/null
+++ b/.chronus/changes/copilot-fix-html-program-viewer-crash-2026-5-8-19-28-50.md
@@ -0,0 +1,7 @@
+---
+changeKind: fix
+packages:
+ - "@typespec/html-program-viewer"
+---
+
+Fix html program viewer crash when rendering search results with missing type kind.
\ No newline at end of file
diff --git a/packages/compiler/src/core/diagnostics.ts b/packages/compiler/src/core/diagnostics.ts
index 180661c70d7..e7a2c441f87 100644
--- a/packages/compiler/src/core/diagnostics.ts
+++ b/packages/compiler/src/core/diagnostics.ts
@@ -16,6 +16,7 @@ import {
SyntaxKind,
Type,
TypeSpecDiagnosticTarget,
+ Value,
} from "./types.js";
export type WriteLine = (text?: string) => void;
@@ -51,12 +52,12 @@ export function getRelatedLocations(diagnostic: Diagnostic): RelatedSourceLocati
* - For template instance targets: returns the node of the template declaration
* - For symbols: returns the first declaration node (or symbol source for using symbols)
* - For AST nodes: returns the node itself
- * - For types: returns the node associated with the type
+ * - For entities: returns the most relevant node associated with the entity
*
* @param target The diagnostic target to extract a node from. Can be a template instance,
* symbol, AST node, or type.
- * @returns The AST node associated with the target, or undefined if the target is a type
- * or symbol that doesn't have an associated node.
+ * @returns The AST node associated with the target, or undefined if the target
+ * doesn't have an associated node.
*/
export function getNodeForTarget(target: TypeSpecDiagnosticTarget): Node | undefined {
if (!("kind" in target) && !("entityKind" in target)) {
@@ -71,6 +72,23 @@ export function getNodeForTarget(target: TypeSpecDiagnosticTarget): Node | undef
}
return target.declarations[0];
+ } else if ("entityKind" in target) {
+ switch (target.entityKind) {
+ case "Type":
+ return target.node;
+ case "Value":
+ return getValueNode(target) ?? target.type.node;
+ case "MixedParameterConstraint":
+ // Prefer the explicit union expression node when present, otherwise fall back
+ // to a side of the constraint that has a source node. Type is preferred
+ // over valueType to keep location behavior stable for mixed constraints
+ // that include both branches.
+ return target.node ?? target.type?.node ?? target.valueType?.node;
+ case "Indeterminate":
+ return target.type.node;
+ default:
+ return undefined;
+ }
} else if ("kind" in target && typeof target.kind === "number") {
// node
return target as Node;
@@ -80,6 +98,20 @@ export function getNodeForTarget(target: TypeSpecDiagnosticTarget): Node | undef
}
}
+function getValueNode(value: Value): Node | undefined {
+ // Only compound values and function values carry their own syntax node.
+ // Primitive values (string/number/boolean/null/enum/scalar literal values)
+ // are represented by their resolved value/type and don't retain a direct node.
+ switch (value.valueKind) {
+ case "ObjectValue":
+ case "ArrayValue":
+ case "Function":
+ return value.node;
+ default:
+ return undefined;
+ }
+}
+
export interface SourceLocationOptions {
/**
* If trying to resolve the location of a type with an ID, show the location of the ID node instead of the entire type.
diff --git a/packages/compiler/test/core/diagnostics.test.ts b/packages/compiler/test/core/diagnostics.test.ts
index 179b04b8f3d..c55b96d6e29 100644
--- a/packages/compiler/test/core/diagnostics.test.ts
+++ b/packages/compiler/test/core/diagnostics.test.ts
@@ -3,6 +3,8 @@ import { describe, it } from "vitest";
import { SourceLocationOptions, getSourceLocation } from "../../src/index.js";
import { extractSquiggles } from "../../src/testing/source-utils.js";
import { Tester } from "../tester.js";
+import { getNodeForTarget } from "../../src/core/diagnostics.js";
+import { SyntaxKind } from "../../src/core/types.js";
describe("compiler: diagnostics", () => {
async function expectLocationMatch(code: string, options: SourceLocationOptions = {}) {
@@ -34,4 +36,82 @@ describe("compiler: diagnostics", () => {
{ locateId: true },
));
});
+
+ describe("getNodeForTarget", () => {
+ const mockSyntaxKindA = SyntaxKind.ModelStatement;
+ const mockSyntaxKindB = SyntaxKind.ScalarStatement;
+ const mockSyntaxKindC = SyntaxKind.NamespaceStatement;
+
+ it("returns function value node when available", () => {
+ const valueNode = { kind: mockSyntaxKindA } as any;
+ const typeNode = { kind: mockSyntaxKindB } as any;
+
+ const target = {
+ entityKind: "Value",
+ valueKind: "Function",
+ node: valueNode,
+ type: { kind: "FunctionType", node: typeNode },
+ } as any;
+
+ strictEqual(getNodeForTarget(target), valueNode);
+ });
+
+ it("falls back to value type node when value has no node", () => {
+ const typeNode = { kind: mockSyntaxKindC } as any;
+
+ const target = {
+ entityKind: "Value",
+ valueKind: "StringValue",
+ type: { kind: "String", node: typeNode },
+ } as any;
+
+ strictEqual(getNodeForTarget(target), typeNode);
+ });
+
+ it("returns object value node when available", () => {
+ const valueNode = { kind: mockSyntaxKindB } as any;
+
+ const target = {
+ entityKind: "Value",
+ valueKind: "ObjectValue",
+ node: valueNode,
+ type: { kind: "Model", node: undefined },
+ } as any;
+
+ strictEqual(getNodeForTarget(target), valueNode);
+ });
+
+ it("resolves mixed parameter constraint target in priority order", () => {
+ const explicitNode = { kind: mockSyntaxKindA } as any;
+ const typeNode = { kind: mockSyntaxKindB } as any;
+
+ strictEqual(
+ getNodeForTarget({
+ entityKind: "MixedParameterConstraint",
+ node: explicitNode,
+ type: { kind: "Model", node: typeNode },
+ } as any),
+ explicitNode,
+ );
+
+ strictEqual(
+ getNodeForTarget({
+ entityKind: "MixedParameterConstraint",
+ type: { kind: "Model", node: typeNode },
+ } as any),
+ typeNode,
+ );
+ });
+
+ it("resolves indeterminate target to underlying type node", () => {
+ const typeNode = { kind: mockSyntaxKindC } as any;
+
+ const target = {
+ entityKind: "Indeterminate",
+ type: { kind: "String", node: typeNode },
+ } as any;
+
+ strictEqual(getNodeForTarget(target), typeNode);
+ });
+ });
});
diff --git a/packages/html-program-viewer/src/react/tree-navigation.test.tsx b/packages/html-program-viewer/src/react/tree-navigation.test.tsx
new file mode 100644
index 00000000000..afdd402c088
--- /dev/null
+++ b/packages/html-program-viewer/src/react/tree-navigation.test.tsx
@@ -0,0 +1,22 @@
+import { render, screen } from "@testing-library/react";
+import type { Type } from "@typespec/compiler";
+import { describe, expect, it } from "vitest";
+import { NodeIcon } from "./tree-navigation.js";
+
+describe("NodeIcon", () => {
+ it("falls back when type kind is missing", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("?")).toBeDefined();
+ });
+});
diff --git a/packages/html-program-viewer/src/react/tree-navigation.tsx b/packages/html-program-viewer/src/react/tree-navigation.tsx
index e0c0eefa9f7..2934d86b60a 100644
--- a/packages/html-program-viewer/src/react/tree-navigation.tsx
+++ b/packages/html-program-viewer/src/react/tree-navigation.tsx
@@ -21,8 +21,10 @@ export const TreeNavigation = (_: TreeNavigationProps) => {
export const NodeIcon = ({ node }: { node: TypeGraphNode }) => {
switch (node.kind) {
- case "type":
- return {node.type.kind[0]};
+ case "type": {
+ const kindPrefix = node.type?.kind?.[0] ?? "?";
+ return {kindPrefix};
+ }
case "list":
return ;
}