From 1f739a521afc95a70bc489ce604bae9fab514b6b Mon Sep 17 00:00:00 2001 From: BlitzOS Upstream Prep Date: Wed, 2 Sep 2026 00:28:35 +0000 Subject: [PATCH] feat(components): let a host without a language service drop the two LSP actions `Go to Definition` and `Find References` are registered on every Monaco viewer unconditionally, so they sit in the context menu and on F12 / Shift+F12 whatever the host can answer. A machine that runs no language service answers "Host language service does not support this file" for every identifier. Add one optional prop per level, each defaulting to today's behaviour: `SessionDetail.hideLanguageServiceActions`, `SessionFileContentView.lspAvailable` and `SessionMonacoTextViewer.lspActions`, down to `lspActions` on `SessionMonacoEditorControllerOptions`, which gates the two `addAction` calls. Gating the ACTIONS rather than the callbacks is the point: an action whose callback is `undefined` still sits in the context menu and does nothing at all, which is worse than the message it replaces. `renderViewerTabContent` is shared by the desktop and mobile branches, so one line covers both. No existing call site passes any of them. Model: claude-opus-5[1m] --- .../components/sessions/session-detail.tsx | 12 +++ .../sessions/session-file-content-view.tsx | 16 ++++ .../sessions/session-monaco-text-viewer.tsx | 9 ++ .../lib/session-monaco-editor-controller.ts | 89 ++++++++++--------- 4 files changed, 86 insertions(+), 40 deletions(-) diff --git a/packages/components/src/components/sessions/session-detail.tsx b/packages/components/src/components/sessions/session-detail.tsx index cf08bfa3a..3ed0794bf 100644 --- a/packages/components/src/components/sessions/session-detail.tsx +++ b/packages/components/src/components/sessions/session-detail.tsx @@ -664,12 +664,23 @@ const SessionDetail = ({ urlPrNumber, urlBrowser, onMobileBack, + hideLanguageServiceActions = false, }: { sessionId: SessionId; urlTab?: string; urlPrNumber?: number; urlBrowser?: boolean; onMobileBack?: () => void; + /** + * Whether the host serves a language service at all. + * + * Go to Definition and Find References are Machine RPC round trips. A host + * whose machine answers "unsupported" for every file draws two editor + * entries whose only outcome is that message, so it can take them off the + * menu instead. Passed to every file viewer this page mounts; see + * `SessionFileContentViewProps.lspAvailable`. + */ + hideLanguageServiceActions?: boolean; }) => { const { t } = useTranslation(); const router = useRouter(); @@ -4522,6 +4533,7 @@ const SessionDetail = ({ saveRequestSeq={viewerTabSaveStates[tab.id]?.saveRequestSeq ?? 0} copyMarkdownRequestSeq={viewerTabSaveStates[tab.id]?.copyMarkdownRequestSeq ?? 0} preferNativeMarkdownSelection={isMobile} + lspAvailable={!hideLanguageServiceActions} fileProvider={activeSessionFileProvider} fileProviderPending={activeSessionFileProviderPending} fileProviderMessage={activeSessionFileProviderMessage} diff --git a/packages/components/src/components/sessions/session-file-content-view.tsx b/packages/components/src/components/sessions/session-file-content-view.tsx index 559b2ad24..6bedacd74 100644 --- a/packages/components/src/components/sessions/session-file-content-view.tsx +++ b/packages/components/src/components/sessions/session-file-content-view.tsx @@ -159,6 +159,14 @@ export type SessionFileContentViewProps = { * context menu. Rendered Markdown opts into native selection as well. */ preferNativeMarkdownSelection?: boolean; + /** + * Whether the host's machine serves a language service. With it off the two + * LSP entry points are not registered at all: Go to Definition and Find + * References leave the editor's context menu and stop answering F12 / + * Shift+F12, instead of answering every identifier with "Host language + * service does not support this file". Defaults to on. + */ + lspAvailable?: boolean; className?: string; active?: boolean; fileProvider?: SessionFileProvider | null; @@ -211,6 +219,7 @@ function SessionFileContentViewImpl({ saveRequestSeq, copyMarkdownRequestSeq, preferNativeMarkdownSelection = false, + lspAvailable = true, className, active = true, fileProvider, @@ -987,6 +996,7 @@ function SessionFileContentViewImpl({ // trip and exposes a small state machine the inline panel renders. const lspFileId = providerEntry?.fileId ?? fileId ?? null; const isLspEnabled = + lspAvailable && isActiveSurface && shouldUseProviderFileContent && providerEntry?.kind === 'text' && @@ -1222,6 +1232,11 @@ function SessionFileContentViewImpl({ onSelectionChange={ liveFileId !== null ? handleProviderEditorSelectionChange : undefined } + // `lspActions` and the two callbacks answer different + // questions. The callbacks are what an action DOES; this is + // whether the action exists. An action with no callback still + // sits in the context menu and does nothing at all. + lspActions={lspAvailable} onGoToDefinition={isLspEnabled ? handleGoToDefinition : undefined} onFindReferences={isLspEnabled ? handleFindReferences : undefined} externalTextUpdate={externalTextUpdate} @@ -2117,6 +2132,7 @@ type SessionTextMonacoViewerProps = { readonly line: number; readonly character: number; }) => void; + readonly lspActions?: boolean; readonly externalTextUpdate?: SessionMonacoExternalTextUpdate; readonly onExternalTextUpdateApplied?: (result: 'applied' | 'no-op') => void; readonly findRequestSeq?: number; diff --git a/packages/components/src/components/sessions/session-monaco-text-viewer.tsx b/packages/components/src/components/sessions/session-monaco-text-viewer.tsx index 880adf678..11dfbb5f9 100644 --- a/packages/components/src/components/sessions/session-monaco-text-viewer.tsx +++ b/packages/components/src/components/sessions/session-monaco-text-viewer.tsx @@ -45,6 +45,7 @@ export function SessionMonacoTextViewer({ onSelectionChange, onGoToDefinition, onFindReferences, + lspActions = true, onScrollChange, externalTextUpdate, onExternalTextUpdateApplied, @@ -99,6 +100,12 @@ export function SessionMonacoTextViewer({ readonly line: number; readonly character: number; }) => void; + // Whether those two actions exist at all. Separate from the callbacks + // above, and read once at mount: a host whose machine serves no language + // service wants the entries OFF the context menu, and an action wired to + // an absent callback is still an entry that does nothing. Defaults to on, + // so a caller that passes neither keeps today's behaviour. + readonly lspActions?: boolean; readonly onScrollChange?: (state: { readonly scrollTop: number }) => void; // Optional Monaco model URI. When provided the viewer creates the // model under this URI, which lets Monaco's globally-registered @@ -139,6 +146,7 @@ export function SessionMonacoTextViewer({ readOnly, wordWrap, modelUri, + lspActions, }); useEffect(() => { @@ -153,6 +161,7 @@ export function SessionMonacoTextViewer({ initialReadOnly: initial.readOnly, initialWordWrap: initial.wordWrap, initialModelUri: initial.modelUri, + lspActions: initial.lspActions, callbacks: { onContentChange, onSelectionChange, diff --git a/packages/components/src/lib/session-monaco-editor-controller.ts b/packages/components/src/lib/session-monaco-editor-controller.ts index ef92c7edb..24e7ad775 100644 --- a/packages/components/src/lib/session-monaco-editor-controller.ts +++ b/packages/components/src/lib/session-monaco-editor-controller.ts @@ -52,6 +52,13 @@ export type SessionMonacoEditorControllerOptions = { readonly initialReadOnly: boolean; readonly initialWordWrap: boolean; readonly initialModelUri?: monaco.Uri; + /** + * Whether to register the two LSP entry-point actions at all. Off takes them + * out of the editor's context menu and unbinds F12 / Shift+F12, which is what + * a host with no language service behind the provider wants: an action whose + * callback is absent still sits in the menu and does nothing. Defaults to on. + */ + readonly lspActions?: boolean; readonly callbacks: SessionMonacoEditorCallbacks; }; @@ -179,46 +186,48 @@ export class SessionMonacoEditorController { }) ); - // LSP entry-point editor actions. Cmd-F12 / F12 fires definition; - // Shift-F12 fires references. Both pass an LSP-shape `{line, character}` - // (0-indexed) so the consumer can hand off to provider RPC directly. - // No `!editorReadonly` precondition: read roles are allowed to - // request LSP RPC by spec, so read-only viewers also surface the - // actions in the context menu. - this.disposables.push( - this.editor.addAction({ - id: 'lody.codeCollab.goToDefinition', - label: 'Go to Definition (Code Collab)', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F12, monaco.KeyCode.F12], - contextMenuGroupId: 'navigation', - contextMenuOrder: 1.1, - run: (ed) => { - const position = ed.getPosition(); - if (!position) return; - this.callbacks.onGoToDefinition?.({ - line: position.lineNumber - 1, - character: position.column - 1, - }); - }, - }) - ); - this.disposables.push( - this.editor.addAction({ - id: 'lody.codeCollab.findReferences', - label: 'Find References (Code Collab)', - keybindings: [monaco.KeyMod.Shift | monaco.KeyCode.F12], - contextMenuGroupId: 'navigation', - contextMenuOrder: 1.2, - run: (ed) => { - const position = ed.getPosition(); - if (!position) return; - this.callbacks.onFindReferences?.({ - line: position.lineNumber - 1, - character: position.column - 1, - }); - }, - }) - ); + if (options.lspActions !== false) { + // LSP entry-point editor actions. Cmd-F12 / F12 fires definition; + // Shift-F12 fires references. Both pass an LSP-shape `{line, character}` + // (0-indexed) so the consumer can hand off to provider RPC directly. + // No `!editorReadonly` precondition: read roles are allowed to + // request LSP RPC by spec, so read-only viewers also surface the + // actions in the context menu. + this.disposables.push( + this.editor.addAction({ + id: 'lody.codeCollab.goToDefinition', + label: 'Go to Definition (Code Collab)', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F12, monaco.KeyCode.F12], + contextMenuGroupId: 'navigation', + contextMenuOrder: 1.1, + run: (ed) => { + const position = ed.getPosition(); + if (!position) return; + this.callbacks.onGoToDefinition?.({ + line: position.lineNumber - 1, + character: position.column - 1, + }); + }, + }) + ); + this.disposables.push( + this.editor.addAction({ + id: 'lody.codeCollab.findReferences', + label: 'Find References (Code Collab)', + keybindings: [monaco.KeyMod.Shift | monaco.KeyCode.F12], + contextMenuGroupId: 'navigation', + contextMenuOrder: 1.2, + run: (ed) => { + const position = ed.getPosition(); + if (!position) return; + this.callbacks.onFindReferences?.({ + line: position.lineNumber - 1, + character: position.column - 1, + }); + }, + }) + ); + } } // Swap the callback bundle. Listeners read `this.callbacks` at fire