Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/desktop-dock-tile.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-desktop": patch
---

Stop a second, unnamed Pythinker icon appearing in the macOS Dock while the app runs.
2 changes: 1 addition & 1 deletion .changeset/remove-managed-kimi-endpoints.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
---
"@pymodel/pythinker-code": minor
"@pymodel/pythinker-code": major
---

Remove the hosted self-update checks, default plugin marketplace catalog, official plugin badges, tips banner, and sign-up links; Kimi now serves only as a model provider through OAuth or an API key. Set PYTHINKER_CODE_PLUGIN_MARKETPLACE_URL to keep using a plugin catalog.
2 changes: 1 addition & 1 deletion .changeset/subagent-execution-inspector.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,4 +2,4 @@
"@pymodel/pythinker-code": patch
---

Allow sub agent activity cards to open their live execution transcript.
Allow subagent activity cards to open their live execution transcript.
26 changes: 26 additions & 0 deletions apps/desktop/src/host-supervisor.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
/** Supervise the loopback Web Host used by the first desktop application. */

import { spawn, spawnSync, type ChildProcessByStdio } from 'node:child_process'
import { basename, join } from 'node:path'
import type { Readable } from 'node:stream'

const READINESS_PREFIX = 'Pythinker server: '
Expand DownExpand Up@@ -312,6 +313,31 @@ export function createHostSupervisor(options: HostSupervisorOptions): HostSuperv
return { start, shutdown }
}

/**
* Resolve the executable that runs the Host on a packaged macOS app.
*
* The app must not re-exec its own main binary. LaunchServices registers that
* child as a second `Foreground` application under the same bundle id, so it
* takes a Dock tile of its own — drawn with the generic Unix-executable icon,
* since a bare executable has no icon to show. `ELECTRON_RUN_AS_NODE` stops the
* child from becoming a browser process but does not stop that registration.
* The bundled Electron helper declares `LSUIElement`, so it runs the very same
* Node runtime with no Dock tile and no second app.
* @param options - Platform, the app's own executable, its `Frameworks` directory, and an existence probe.
* @returns The helper executable when the bundle ships one, otherwise `execPath` unchanged.
*/
export function resolveHostExecutable(options: {
readonly platform: string
readonly execPath: string
readonly frameworksPath: string
readonly exists: (path: string) => boolean
}): string {
if (options.platform !== 'darwin') return options.execPath
const name = basename(options.execPath)
const helper = join(options.frameworksPath, `${name} Helper.app`, 'Contents', 'MacOS', `${name} Helper`)
return options.exists(helper) ? helper : options.execPath
}

/** Options for the real Pythinker server child. */
export interface SpawnPythinkerServerOptions {
/** Node-compatible executable selected by the desktop app. */
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,7 @@ import {
isPortInUseError,
parseRunningServerConflict,
resolveDesktopPort,
resolveHostExecutable,
spawnPythinkerServer,
type HostSupervisor,
} from './host-supervisor'
Expand DownExpand Up@@ -112,7 +113,12 @@ function hostPaths(): { nodeExecutable: string; cliEntry: string; cwd: string; e
}
}
return {
nodeExecutable: process.execPath,
nodeExecutable: resolveHostExecutable({
platform: process.platform,
execPath: process.execPath,
frameworksPath: join(process.resourcesPath, '..', 'Frameworks'),
exists: existsSync,
}),
cliEntry: join(process.resourcesPath, 'host/node_modules/@pymodel/pythinker-code/dist/main.mjs'),
cwd: app.getPath('home'),
electronRunAsNode: true,
Expand Down
49 changes: 49 additions & 0 deletions apps/desktop/tests/host-supervisor.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import {
createHostSupervisor,
createReadinessParser,
resolveHostExecutable,
type HostChild,
} from '../src/host-supervisor'
import * as hostSupervisor from '../src/host-supervisor'
Expand DownExpand Up@@ -509,3 +510,51 @@ describe('desktop Host process', () => {
expect(spawnSync).not.toHaveBeenCalled()
})
})

describe('resolveHostExecutable', () => {
const APP = '/Applications/Pythinker.app/Contents/MacOS/Pythinker'
const FRAMEWORKS = '/Applications/Pythinker.app/Contents/Frameworks'
const HELPER = `${FRAMEWORKS}/Pythinker Helper.app/Contents/MacOS/Pythinker Helper`
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('runs the Host from the LSUIElement helper so it takes no Dock tile of its own', () => {
// Re-execing the app's own binary registers a second Foreground app under
// the same bundle id, which shows up as a stray generic-executable icon in
// the Dock next to the real app.
const seen: string[] = []
const resolved = resolveHostExecutable({
platform: 'darwin',
execPath: APP,
frameworksPath: FRAMEWORKS,
exists: path => {
seen.push(path)
return path === HELPER
},
})
expect(resolved).toBe(HELPER)
expect(seen).toEqual([HELPER])
})

it('keeps the app executable when the bundle ships no matching helper', () => {
expect(
resolveHostExecutable({
platform: 'darwin',
execPath: APP,
frameworksPath: FRAMEWORKS,
exists: () => false,
}),
).toBe(APP)
})

it('leaves non-macOS platforms alone', () => {
for (const platform of ['win32', 'linux']) {
expect(
resolveHostExecutable({
platform,
execPath: 'C:\\Program Files\\Pythinker\\Pythinker.exe',
frameworksPath: 'C:\\Program Files\\Pythinker\\Frameworks',
exists: () => true,
}),
).toBe('C:\\Program Files\\Pythinker\\Pythinker.exe')
}
})
})
Loading