@@ -9,18 +9,22 @@ import * as vscode from 'vscode';
99import * as fs from 'fs' ;
1010import * as path from 'path' ;
1111import * as cp from 'child_process' ;
12+ import * as crypto from 'crypto' ;
1213import type * as positron from '@posit-dev/positron' ;
1314import type { JupyterKernelSpec , JupyterSession , JupyterKernel , PositronSupervisorApi } from './types' ;
1415import { log } from './extension' ;
1516
17+ /** Where a kernel candidate was discovered */
18+ type KernelSource = 'Setting' | 'Jupyter' | 'System' | 'Path' ;
19+
1620/**
1721 * A discovered ggsql-jupyter kernel candidate
1822 */
1923interface KernelCandidate {
2024 /** Absolute path to the ggsql-jupyter binary (or bare name for PATH fallback) */
2125 kernelPath : string ;
2226 /** Human-readable label for where this was found */
23- source : string ;
27+ source : KernelSource ;
2428}
2529
2630/**
@@ -42,7 +46,7 @@ function discoverKernelPaths(): KernelCandidate[] {
4246 const config = vscode . workspace . getConfiguration ( 'ggsql' ) ;
4347 const configuredPath = config . get < string > ( 'kernelPath' , '' ) ;
4448 if ( configuredPath && configuredPath . trim ( ) !== '' ) {
45- candidates . push ( { kernelPath : configuredPath , source : 'setting ' } ) ;
49+ candidates . push ( { kernelPath : configuredPath , source : 'Setting ' } ) ;
4650 }
4751
4852 // 2. Jupyter kernelspec locations
@@ -52,6 +56,11 @@ function discoverKernelPaths(): KernelCandidate[] {
5256 path . join ( homeDir , 'Library' , 'Jupyter' , 'kernels' , 'ggsql' , binaryName ) ,
5357 // User kernelspec (Linux)
5458 path . join ( homeDir , '.local' , 'share' , 'jupyter' , 'kernels' , 'ggsql' , binaryName ) ,
59+ // User kernelspec (Windows)
60+ path . join (
61+ process . env . APPDATA || path . join ( homeDir , 'AppData' , 'Roaming' ) ,
62+ 'jupyter' , 'kernels' , 'ggsql' , binaryName
63+ ) ,
5564 // System kernelspec (macOS)
5665 path . join ( '/usr' , 'local' , 'share' , 'jupyter' , 'kernels' , 'ggsql' , binaryName ) ,
5766 // System kernelspec (Linux)
@@ -137,15 +146,15 @@ async function isKernelAccessible(kernelPath: string): Promise<boolean> {
137146function generateMetadata (
138147 context : vscode . ExtensionContext ,
139148 candidate : KernelCandidate ,
140- index : number
141149) : positron . LanguageRuntimeMetadata {
142150 const version = context . extension . packageJSON . version as string ;
143151
144152 const iconPath = path . join ( context . extensionPath , 'resources' , 'ggsql-icon.svg' ) ;
145153 const base64Icon = fs . readFileSync ( iconPath ) . toString ( 'base64' ) ;
146154
155+ const pathHash = crypto . createHash ( 'sha256' ) . update ( candidate . kernelPath ) . digest ( 'hex' ) . substring ( 0 , 12 ) ;
147156 return {
148- runtimeId : index === 0 ? 'ggsql-jupyter' : `ggsql-jupyter- ${ index } ` ,
157+ runtimeId : `ggsql-${ pathHash } ` ,
149158 runtimePath : candidate . kernelPath ,
150159 runtimeName : `ggsql (${ candidate . source } )` ,
151160 runtimeShortName : 'ggsql' ,
@@ -284,7 +293,7 @@ function writeKernelJson(kernelDir: string, kernelPath: string): void {
284293 argv : [ kernelPath , '-f' , '{connection_file}' ] ,
285294 display_name : 'ggsql' ,
286295 language : 'ggsql' ,
287- interrupt_mode : 'message ' ,
296+ interrupt_mode : 'signal ' ,
288297 env : { RUST_LOG : 'error' } ,
289298 metadata : { debugger : false }
290299 } ;
@@ -308,16 +317,49 @@ function writeKernelJson(kernelDir: string, kernelPath: string): void {
308317}
309318
310319/**
311- * Install a Jupyter kernel spec for ggsql so that external tools like Quarto
312- * can discover it via `jupyter kernelspec list`.
313- *
314- * Writes kernel.json to the appropriate Jupyter kernelspec directory: the
315- * active virtualenv/conda env if detected, otherwise the user-level dir.
320+ * Try to resolve a binary name to its absolute path via the system PATH.
321+ * Returns the original value if resolution fails.
322+ */
323+ function resolveKernelPath ( kernelPath : string ) : string {
324+ if ( path . isAbsolute ( kernelPath ) ) {
325+ return kernelPath ;
326+ }
327+ try {
328+ const cmd = process . platform === 'win32' ? 'where' : 'which' ;
329+ const resolved = cp . execFileSync ( cmd , [ kernelPath ] , {
330+ encoding : 'utf8' ,
331+ timeout : 5000 ,
332+ } ) . trim ( ) . split ( / \r ? \n / ) [ 0 ] ;
333+ if ( resolved && path . isAbsolute ( resolved ) ) {
334+ log ( `Resolved '${ kernelPath } ' to '${ resolved } '` ) ;
335+ return resolved ;
336+ }
337+ } catch {
338+ log ( `Could not resolve '${ kernelPath } ' to absolute path, using as-is` ) ;
339+ }
340+ return kernelPath ;
341+ }
342+
343+ /**
344+ * Ensure a Jupyter kernel spec is installed so that external tools like
345+ * Quarto can discover ggsql. Called from session creation/restoration.
316346 *
317- * @param kernelPath Absolute path to the ggsql-jupyter binary
347+ * Writes to the active virtualenv/conda env if detected, otherwise the
348+ * user-level kernelspec directory.
349+ */
350+ function ensureKernelSpecInstalled ( kernelPath : string ) : void {
351+ writeKernelJson ( getJupyterKernelDir ( ) , resolveKernelPath ( kernelPath ) ) ;
352+ }
353+
354+ /**
355+ * Create the dynamic state for a ggsql runtime session.
318356 */
319- function installJupyterKernelSpec ( kernelPath : string ) : void {
320- writeKernelJson ( getJupyterKernelDir ( ) , kernelPath ) ;
357+ function createDynState ( ) : positron . LanguageRuntimeDynState {
358+ return {
359+ inputPrompt : 'ggsql> ' ,
360+ continuationPrompt : '... ' ,
361+ sessionName : 'ggsql' ,
362+ } ;
321363}
322364
323365/**
@@ -347,21 +389,19 @@ export class GgsqlRuntimeManager implements positron.LanguageRuntimeManager {
347389 const candidates = discoverKernelPaths ( ) ;
348390 log ( `Found ${ candidates . length } kernel candidate(s)` ) ;
349391
350- let index = 0 ;
351392 for ( const candidate of candidates ) {
352393 const accessible = await isKernelAccessible ( candidate . kernelPath ) ;
353394 if ( accessible ) {
354395 // When a system install is found, write the kernel spec to
355396 // the user kernelspec dir immediately so that Quarto/Jupyter
356397 // can discover ggsql even if no session is ever started.
357398 if ( candidate . source === 'System' ) {
358- writeKernelJson ( getUserJupyterKernelDir ( ) , candidate . kernelPath ) ;
399+ writeKernelJson ( getUserJupyterKernelDir ( ) , resolveKernelPath ( candidate . kernelPath ) ) ;
359400 }
360401
361- const metadata = generateMetadata ( context , candidate , index ) ;
402+ const metadata = generateMetadata ( context , candidate ) ;
362403 log ( `Yielding runtime: ${ metadata . runtimeName } (${ metadata . runtimeId } ) at ${ candidate . kernelPath } ` ) ;
363404 yield metadata ;
364- index ++ ;
365405 } else {
366406 log ( `Skipping inaccessible kernel: ${ candidate . kernelPath } ` ) ;
367407 }
@@ -408,15 +448,10 @@ export class GgsqlRuntimeManager implements positron.LanguageRuntimeManager {
408448 // Create the kernel spec using the runtime's kernel path
409449 const kernelSpec = createKernelSpec ( runtimeMetadata . runtimePath , workspacePath ) ;
410450
411- // Create the dynamic state
412- const dynState : positron . LanguageRuntimeDynState = {
413- inputPrompt : 'ggsql> ' ,
414- continuationPrompt : '... ' ,
415- sessionName : 'ggsql'
416- } ;
451+ const dynState = createDynState ( ) ;
417452
418453 // Advertise this kernel to external tools (Quarto, Jupyter)
419- installJupyterKernelSpec ( runtimeMetadata . runtimePath ) ;
454+ ensureKernelSpecInstalled ( runtimeMetadata . runtimePath ) ;
420455
421456 // Create the session using the supervisor
422457 const session = await supervisorApi . createSession (
@@ -455,14 +490,10 @@ export class GgsqlRuntimeManager implements positron.LanguageRuntimeManager {
455490
456491 const supervisorApi = await supervisorExt . activate ( ) ;
457492
458- const dynState : positron . LanguageRuntimeDynState = {
459- inputPrompt : 'ggsql> ' ,
460- continuationPrompt : '... ' ,
461- sessionName : 'ggsql'
462- } ;
493+ const dynState = createDynState ( ) ;
463494
464495 // Re-advertise this kernel on restore
465- installJupyterKernelSpec ( runtimeMetadata . runtimePath ) ;
496+ ensureKernelSpecInstalled ( runtimeMetadata . runtimePath ) ;
466497
467498 const session = await supervisorApi . restoreSession (
468499 runtimeMetadata ,
0 commit comments