Skip to content
Closed
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
6 changes: 6 additions & 0 deletions .changeset/dark-sidebar-contrast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pymodel/pythinker-code": patch
"@pymodel/pythinker-desktop": patch
---

Make the workspace header, session timestamps, and the settings row legible in dark mode on the translucent desktop sidebar.
5 changes: 5 additions & 0 deletions .changeset/desktop-dedicated-update-channel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-desktop": patch
---

Publish desktop releases to a dedicated update channel so update checks resolve a desktop build instead of an unrelated release, and fail the release when a packaged build carries no update feed.
5 changes: 5 additions & 0 deletions .changeset/desktop-pin-host-port.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-desktop": patch
---

Pin the Host port so the desktop app reconnects to its own Host, and stop reporting builds that cannot self-update as update errors.
33 changes: 11 additions & 22 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,28 +195,17 @@ async function createMainWindow(): Promise<BrowserWindow> {
autoHideMenuBar: true,
frame: process.platform === 'win32',
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'hidden',
...(process.platform === 'darwin' ? {} : {
titleBarOverlay: {
color: '#00000000',
symbolColor: '#7f858f',
height: 44,
},
}),
...(process.platform === 'darwin' ? {
trafficLightPosition: { x: 16, y: 18 },
vibrancy: 'sidebar' as const,
visualEffectState: 'followWindow' as const,
} : {}),
// Windows uses an opaque window so theme colors do not blend with desktop wallpaper.
...(process.platform === 'win32' ? {
backgroundColor: '#0d1117',
hasShadow: true,
roundedCorners: true,
thickFrame: true,
} : {
transparent: true,
backgroundColor: '#00000000',
}),
titleBarOverlay: process.platform === 'darwin' ? undefined : {
color: '#00000000',
symbolColor: '#7f858f',
height: 44,
},
trafficLightPosition: process.platform === 'darwin' ? { x: 16, y: 18 } : undefined,
// Use an opaque window so theme colors do not blend with desktop wallpaper.
backgroundColor: '#0d1117',
hasShadow: process.platform === 'win32' ? true : undefined,
roundedCorners: process.platform === 'win32' ? true : undefined,
thickFrame: process.platform === 'win32' ? true : undefined,
title: APP_NAME,
webPreferences: {
contextIsolation: true,
Expand Down
7 changes: 4 additions & 3 deletions apps/desktop/tests/packaging-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ describe('desktop packaging configuration', () => {
expect(siteSource).toContain('releases/download/v${DESKTOP_VERSION}')
expect(siteSource).not.toContain('releases/download/v0.1.0')

const desktopShowcaseMatch = siteSource.match(/<section id="desktop"[\s\S]*?<\/section>/)
expect(desktopShowcaseMatch).not.toBeNull()
expect(desktopShowcaseMatch![0]).toContain('/brand/windows11.svg')
expect(siteSource).toContain('id="desktop"')
expect(siteSource).toMatch(
/\{[^}]*icon: '\/brand\/windows11\.svg', href: desktopDownloads\.windows[^}]*\}/,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

it('maps the staged Host node_modules directory as the copy root', () => {
Expand Down
54 changes: 33 additions & 21 deletions apps/desktop/tests/window-appearance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,48 @@ const desktopRoot = resolve(import.meta.dirname, '..')
const mainSource = readFileSync(resolve(desktopRoot, 'src', 'main.ts'), 'utf8')

describe('desktop window appearance configuration', () => {
it('keeps Windows opaque and non-Windows windows transparent', () => {
it('keeps the main window opaque on every platform', () => {
const backgroundMaterialMatches = [...mainSource.matchAll(/backgroundMaterial/gu)]
const win32BranchMatches = [...mainSource.matchAll(
/\.\.\.\(process\.platform === 'win32' \? \{([\s\S]*?)\} : \{\s*transparent: true,/gu,
)]
const nonWin32BranchMatches = [...mainSource.matchAll(
/\} : \{\s*transparent: true,[\s\S]*?\}\),\s*title:/gu,
const mainWindowOptionMatches = [...mainSource.matchAll(
/const window = new BrowserWindow\(\{([\s\S]*?)\n \}\)/gu,
)]

expect(backgroundMaterialMatches).toHaveLength(0)
expect(win32BranchMatches).toHaveLength(1)
expect(nonWin32BranchMatches).toHaveLength(1)
expect(mainWindowOptionMatches).toHaveLength(1)

const win32Branch = win32BranchMatches[0]![1]!
const opaqueColorMatches = [...win32Branch.matchAll(/backgroundColor:\s*'#[0-9a-fA-F]{6}'/gu)]
const alphaColorMatches = [...win32Branch.matchAll(/#[0-9a-fA-F]{8}/gu)]
const hasShadowMatches = [...win32Branch.matchAll(/hasShadow:\s*true/gu)]
const roundedCornersMatches = [...win32Branch.matchAll(/roundedCorners:\s*true/gu)]
const thickFrameMatches = [...win32Branch.matchAll(/thickFrame:\s*true/gu)]
const mainWindowOptions = mainWindowOptionMatches[0]![1]!
const vibrancyMatches = [...mainWindowOptions.matchAll(/vibrancy\s*:/gu)]
const visualEffectStateMatches = [...mainWindowOptions.matchAll(/visualEffectState\s*:/gu)]
const transparentMatches = [...mainWindowOptions.matchAll(/transparent:\s*true/gu)]
const backgroundColorMatches = [...mainWindowOptions.matchAll(
/backgroundColor:\s*'(#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?)'/gu,
)]
const transparentBackgroundMatches = backgroundColorMatches.filter(
(match) => match[1]!.length === 9 && match[1]!.endsWith('00'),
)
const hasShadowMatches = [...mainWindowOptions.matchAll(
/hasShadow:\s*process\.platform === 'win32' \? true : undefined/gu,
)]
const roundedCornersMatches = [...mainWindowOptions.matchAll(
/roundedCorners:\s*process\.platform === 'win32' \? true : undefined/gu,
)]
const thickFrameMatches = [...mainWindowOptions.matchAll(
/thickFrame:\s*process\.platform === 'win32' \? true : undefined/gu,
)]

expect(opaqueColorMatches).toHaveLength(1)
expect(alphaColorMatches).toHaveLength(0)
expect(vibrancyMatches).toHaveLength(0)
expect(visualEffectStateMatches).toHaveLength(0)
expect(transparentMatches).toHaveLength(0)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(backgroundColorMatches).toHaveLength(1)
expect(backgroundColorMatches[0]![1]).not.toBe('#00000000')
expect(transparentBackgroundMatches).toHaveLength(0)
expect(hasShadowMatches).toHaveLength(1)
expect(roundedCornersMatches).toHaveLength(1)
expect(thickFrameMatches).toHaveLength(1)

expect(win32Branch).toContain('backgroundColor')
expect(nonWin32BranchMatches[0]![0]).toContain('transparent: true')
expect(win32Branch).toContain('hasShadow')
expect(win32Branch).toContain('roundedCorners')
expect(win32Branch).toContain('thickFrame')
expect(mainWindowOptions).toContain('backgroundColor')
expect(mainWindowOptions).toContain('hasShadow')
expect(mainWindowOptions).toContain('roundedCorners')
expect(mainWindowOptions).toContain('thickFrame')
})
})
10 changes: 9 additions & 1 deletion apps/pythinker-web/src/components/Composer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -735,12 +735,18 @@ const hasUpload = computed(() => !!props.uploadImage);
// ---------------------------------------------------------------------------

const dropdownOpen = ref(false);
const modelPillRef = ref<HTMLElement | null>(null);
const modelDropdownStyle = ref<Record<string, string>>({});
const permDropdownOpen = ref(false);
const toolbarRef = ref<HTMLElement | null>(null);

function toggleDropdown(): void {
dropdownOpen.value = !dropdownOpen.value;
if (dropdownOpen.value) {
const rect = modelPillRef.value?.getBoundingClientRect();
modelDropdownStyle.value = rect
? { maxHeight: `${Math.max(160, rect.top - 4 - 12)}px` }
: {};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
permDropdownOpen.value = false;
document.addEventListener('click', onDocClick, true);
} else {
Expand Down Expand Up @@ -1179,6 +1185,7 @@ function selectModel(modelId: string): void {
<!-- Model pill — click to open quick-switch dropdown -->
<span
v-if="status"
ref="modelPillRef"
class="model-pill"
:class="{ open: dropdownOpen }"
role="button"
Expand All @@ -1195,7 +1202,7 @@ function selectModel(modelId: string): void {
</div>

<!-- Model dropdown — current provider models + controls + more -->
<div v-if="dropdownOpen && status" class="model-dropdown" role="menu" @click.stop>
<div v-if="dropdownOpen && status" class="model-dropdown" :style="modelDropdownStyle" role="menu" @click.stop>
<!-- Starred models from other providers -->
<div v-if="starredOtherModels.length > 0" class="md-section">{{ t('status.starredModels') }}</div>
<button
Expand Down Expand Up @@ -1719,6 +1726,7 @@ function selectModel(modelId: string): void {
display: flex;
flex-direction: column;
gap: 1px;
overflow-y: auto;
}

.md-section {
Expand Down
4 changes: 3 additions & 1 deletion apps/pythinker-web/src/components/NewSessionDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
border: 1px solid var(--line);
border-top: 2px solid var(--blue);
border-radius: 4px;
overflow: hidden;
width: 520px;
max-width: calc(100vw - 32px);
height: 360px;
Expand Down Expand Up @@ -210,6 +211,8 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
flex-direction: column;
gap: 12px;
flex: 1;
overflow-y: auto;
min-height: 0;
}

.form-row {
Expand Down Expand Up @@ -349,7 +352,6 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
max-height: calc(100dvh - 24px);
}
.form-body {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.form-row {
Expand Down
12 changes: 6 additions & 6 deletions apps/pythinker-web/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ html[data-color-scheme="dark"] {
color-scheme: dark;
--ink: #e8eaed;
--text: #c9cdd4;
--dim: #9aa0a8;
--muted: #727983;
--faint: #525960;
--dim: #c2c8d0;
--muted: #aab1ba;
--faint: #8b939c;
--line: #2d333b;
--line2: #22272e;
--panel: #1c2128;
Expand All @@ -99,9 +99,9 @@ html[data-color-scheme="dark"] {
html[data-color-scheme="system"] {
--ink: #e8eaed;
--text: #c9cdd4;
--dim: #9aa0a8;
--muted: #727983;
--faint: #525960;
--dim: #c2c8d0;
--muted: #aab1ba;
--faint: #8b939c;
--line: #2d333b;
--line2: #22272e;
--panel: #1c2128;
Expand Down
24 changes: 24 additions & 0 deletions apps/pythinker-web/test/composer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,30 @@ describe('Composer model dropdown', () => {

expect(wrapper.emitted('selectModel')).toEqual([['openai/gpt-5']]);
});

it('bounds the quick-switch dropdown to the measured space above its pill', async () => {
const wrapper = mountComposer({
status: { model: 'Model 0', modelId: 'pythinker/model-0', ctxUsed: 0, ctxMax: 128000, permission: 'manual' },
models: Array.from({ length: 17 }, (_, index) => ({
id: `pythinker/model-${index}`,
provider: 'pythinker',
model: `model-${index}`,
displayName: `Model ${index}`,
maxContextSize: 128000,
})),
});
const pill = wrapper.get('.model-pill');
const rect = vi.spyOn(pill.element, 'getBoundingClientRect');

rect.mockReturnValue({ top: 20 } as DOMRect);
await pill.trigger('click');
expect(wrapper.get('.model-dropdown').element.style.maxHeight).toBe('160px');

rect.mockReturnValue({ top: 300 } as DOMRect);
await pill.trigger('click');
await pill.trigger('click');
expect(wrapper.get('.model-dropdown').element.style.maxHeight).toBe('284px');
});
});

describe('Composer context indicator', () => {
Expand Down
Loading
Loading