Skip to content
Open
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
53 changes: 10 additions & 43 deletions apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@
"src/renderer/settings/projects-settings-page.tsx",
"src/renderer/settings/provider-add-form.tsx",
"src/renderer/settings/provider-add-submission.ts",
"src/renderer/settings/provider-brand-marks.tsx",
"src/renderer/settings/provider-catalog-page.tsx",
"src/renderer/settings/provider-connection-detail.tsx",
"src/renderer/settings/provider-connection-status.ts",
Expand Down Expand Up @@ -332,8 +331,12 @@
]
},
"src/renderer/features/connection-settings/index.ts": {
"appShell": [
"ProviderBrandMark"
],
"composition": [
"ConnectionSettingsServicesProvider"
"ConnectionSettingsServicesProvider",
"ProviderBrandMark"
]
},
"src/renderer/features/conversation/index.ts": {
Expand Down Expand Up @@ -650,7 +653,7 @@
"nonTriviaTokens": 780
},
"src/renderer/app-shell.tsx": {
"importDeclarations": 33,
"importDeclarations": 32,
"bridgePaths": {},
"environmentCapabilities": {
"window.requestAnimationFrame": 3
Expand Down Expand Up @@ -696,6 +699,7 @@
"./desktop-execution-boundary-surface": 1,
"./error-boundary": 1,
"./features/app-update/index.js": 1,
"./features/connection-settings/index.js": 1,
"./features/conversation": 1,
"./features/conversation/index.js": 1,
"./features/diagnostics/index.js": 1,
Expand All @@ -718,7 +722,6 @@
"./pending-session-view": 1,
"./platform/desktop/default-runtime-host-operation.js": 1,
"./scroll-motion-policy": 1,
"./settings/provider-brand-marks": 1,
"./settings/runtime-host-ssh-terminal-dialog.js": 1,
"./shell/detail-panel": 1,
"./shell/frame-style": 1,
Expand All @@ -738,7 +741,7 @@
"@maka/ui": 1,
"react": 1
},
"importSpecifiers": 42,
"importSpecifiers": 41,
"nonTriviaTokens": 7271
},
"src/renderer/use-app-shell-session-list.ts": {
Expand Down Expand Up @@ -1964,6 +1967,7 @@
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"../features/connection-settings/index.js": 1,
"../features/conversation/index.js": 1,
"../features/jev-settings": 1,
"../features/network-proxy/index.js": 1,
Expand All @@ -1974,7 +1978,6 @@
"../locales/shell-copy.js": 1,
"./password-input": 1,
"./personalization-settings-section": 1,
"./provider-brand-marks": 1,
"./runtime-host-settings-target.js": 1,
"./settings-error-copy": 1,
"./settings-section": 1,
Expand Down Expand Up @@ -2340,42 +2343,6 @@
"@maka/core/llm-connections": 1
}
},
"src/renderer/settings/provider-brand-marks.tsx": {
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"../assets/provider-brands/alibabacloud.svg": 1,
"../assets/provider-brands/cerebras.svg": 1,
"../assets/provider-brands/cloudflare.svg": 1,
"../assets/provider-brands/cohere.svg": 1,
"../assets/provider-brands/deepinfra.svg": 1,
"../assets/provider-brands/fireworks.svg": 1,
"../assets/provider-brands/groq.svg": 1,
"../assets/provider-brands/huggingface.svg": 1,
"../assets/provider-brands/hunyuan.svg": 1,
"../assets/provider-brands/lmstudio.svg": 1,
"../assets/provider-brands/localai.svg": 1,
"../assets/provider-brands/mistral.svg": 1,
"../assets/provider-brands/nvidia.svg": 1,
"../assets/provider-brands/opencode.svg": 1,
"../assets/provider-brands/openrouter.svg": 1,
"../assets/provider-brands/stepfun.svg": 1,
"../assets/provider-brands/tencentcloud.svg": 1,
"../assets/provider-brands/together.svg": 1,
"../assets/provider-brands/vercel.svg": 1,
"../assets/provider-brands/volcengine.svg": 1,
"../assets/provider-brands/xai.svg": 1,
"../assets/provider-brands/xiaomimimo.svg": 1,
"../assets/provider-brands/zai.svg": 1,
"../assets/provider-brands/zenmux.svg": 1,
"../features/connection-settings": 1,
"simple-icons": 1
}
},
"src/renderer/settings/provider-catalog-page.tsx": {
"bridgePaths": {},
"environmentCapabilities": {},
Expand Down Expand Up @@ -2462,7 +2429,7 @@
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"./provider-brand-marks": 1,
"../features/connection-settings/index.js": 1,
"./provider-display-copy": 1
}
},
Expand Down
138 changes: 138 additions & 0 deletions apps/desktop/src/main/__tests__/workhub-composer-provider-mark.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { afterEach, test } from 'node:test';
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { parseHTML } from 'linkedom';
import type { ChatModelChoice } from '@maka/ui';
import { AstryxLocaleProvider, LocaleProvider, ToastProvider } from '@maka/ui';
import type { SessionSummary } from '@maka/core/session';
import { WorkHubServicesProvider, type WorkHubServices } from '../../renderer/features/workhub/index.js';
import { WorkHubComposer } from '../../renderer/features/workhub/testing.js';

const originalGlobals = {
document: globalThis.document,
window: globalThis.window,
HTMLElement: globalThis.HTMLElement,
Element: globalThis.Element,
Event: globalThis.Event,
Node: globalThis.Node,
matchMedia: globalThis.matchMedia,
requestAnimationFrame: globalThis.requestAnimationFrame,
cancelAnimationFrame: globalThis.cancelAnimationFrame,
IS_REACT_ACT_ENVIRONMENT: (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
.IS_REACT_ACT_ENVIRONMENT,
};
let mountedRoot: Root | undefined;
afterEach(async () => {
if (mountedRoot) await act(() => mountedRoot?.unmount());
mountedRoot = undefined;
Object.assign(globalThis, originalGlobals);
});

test('the WorkHub composer renders provider marks like regular chat', async () => {
const { document, window } = parseHTML('<div id="root"></div>');
const getSelection = () => null;
Object.assign(document, { getSelection });
Object.assign(window, {
getSelection,
getComputedStyle: () =>
({ direction: 'ltr', writingMode: 'horizontal-tb', getPropertyValue: () => '' }) as unknown as CSSStyleDeclaration,
matchMedia: () =>
({ matches: false, addEventListener() {}, removeEventListener() {} }) as unknown as MediaQueryList,
});
Object.assign(globalThis, {
document,
window,
HTMLElement: window.HTMLElement,
Element: window.Element,
Event: window.Event,
Node: window.Node,
matchMedia: window.matchMedia,
requestAnimationFrame: window.requestAnimationFrame,
cancelAnimationFrame: window.cancelAnimationFrame,
IS_REACT_ACT_ENVIRONMENT: true,
});
const activeSession = {
id: 'workhub-session', name: 'WorkHub', status: 'active', labels: [], isFlagged: false,
isArchived: false, hasUnread: false, backend: 'ai-sdk', llmConnectionSlug: 'native',
connectionLocked: false, model: 'native-model', permissionMode: 'ask',
} as unknown as SessionSummary;
const choices: ChatModelChoice[] = [{
connectionId: 'native', connectionSlug: 'native', connectionName: 'My account',
providerType: 'openai', providerLabel: 'OpenAI', model: 'native-model', label: 'Native model',
description: 'Native model description', isDefault: true, thinkingLevels: [],
}];
const services = {} as unknown as WorkHubServices;
mountedRoot = createRoot(document.getElementById('root')!);
await act(async () => {
mountedRoot!.render(createElement(LocaleProvider, {
locale: 'en',
children: createElement(AstryxLocaleProvider, {
children: createElement(ToastProvider, {
children: createElement(WorkHubServicesProvider, { services },
createElement(WorkHubComposer, {
sessionId: 'workhub-session',
onStop: () => undefined,
onSend: async () => true,
activeSession,
modelChoices: choices,
activeModel: 'native-model',
activeModelConnectionId: 'native',
activeModelConnectionSlug: 'native',
activeProviderType: 'openai',
onModelChange: () => undefined,
renderProviderMark: (type: string) => createElement('span', { 'data-provider-mark': type }),
})),
}),
}),
}));
});
const mark = document.querySelector('[data-provider-mark]');
assert.ok(mark, 'the provider mark must render beside the model switcher');
assert.equal(mark.getAttribute('data-provider-mark'), 'openai');
});

// The render test above pins the composer seam by feeding the prop itself, so
// it stays green if the passthrough is narrowed away. This pins the wiring:
// the composition root must hand its mark to the WorkHub root, and the root
// must keep forwarding it to the composer (#6100 review).
test('the WorkHub wiring keeps the injected mark flowing to its composer', () => {
const rootSource = readFileSync(
new URL('../../../src/renderer/features/workhub/ui/workhub-root.tsx', import.meta.url),
'utf8',
);
assert.match(
rootSource,
/renderProviderMark=\{renderProviderMark\}/,
'WorkHubRoot must forward the injected mark to its composer',
);
const adapterSource = readFileSync(
new URL('../../../src/renderer/composition/legacy-desktop-region.tsx', import.meta.url),
'utf8',
);
assert.match(
adapterSource,
/renderProviderMark=\{renderProviderMark\}/,
'the composition adapter must pass its mark supplier into WorkHubRoot',
);
});
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ import {
type OnboardingShellProjection,
} from './application/contracts/onboarding/onboarding-authority.js';
import { ShellLifecycleSubscriptions } from './application/contracts/shell-lifecycle.js';
import { ProviderBrandMark } from './settings/provider-brand-marks';
import { ProviderBrandMark } from './features/connection-settings/index.js';
import { RuntimeHostSshTerminalDialog } from './settings/runtime-host-ssh-terminal-dialog.js';
import {
getShellCopy,
Expand Down
12 changes: 11 additions & 1 deletion apps/desktop/src/renderer/composition/legacy-desktop-region.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,17 @@
*/

import { AppShell as LegacyAppShell } from '../app-shell';
import { ProviderBrandMark } from '../features/connection-settings';
import { WorkHubRoot, WorkHubSurfaceSwitch } from '../features/workhub';

type RenderProviderMark = NonNullable<import('@maka/ui').ComposerProps['renderProviderMark']>;

// The feature owns the marks; the composition root is the one place both the
// legacy shell and the WorkHub surface can be handed the same renderer.
const renderProviderMark: RenderProviderMark = (type) => <ProviderBrandMark type={type} />;

export function AppShell() {
return <WorkHubSurfaceSwitch main={<LegacyAppShell />} workhub={<WorkHubRoot />} />;
return <WorkHubSurfaceSwitch main={<LegacyAppShell />} workhub={
<WorkHubRoot renderProviderMark={renderProviderMark} />
} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type {
CredentialPresenceStatus,
} from './provider-panel-shared.js';

export { GenericProviderMark } from './generic-provider-mark.js';
export { ProviderBrandMark } from './provider-brand-marks.js';
export { parseContextWindowInput } from './context-window-input.js';
export { CapabilityEditor } from './provider-capability-editor.js';
export { AddModelDialog, ModelParametersDialog } from './provider-add-model-dialog.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,33 +33,33 @@
// legible in both light and dark themes; viewBox is the upstream 24×24.

import type { ProviderType } from '@maka/core/llm-connections';
import { GenericProviderMark } from '../features/connection-settings';
import { GenericProviderMark } from './generic-provider-mark.js';
import type { ReactElement } from 'react';
import { siMinimax } from 'simple-icons';
import alibabaBrandMark from '../assets/provider-brands/alibabacloud.svg';
import cerebrasBrandMark from '../assets/provider-brands/cerebras.svg';
import cohereBrandMark from '../assets/provider-brands/cohere.svg';
import cloudflareMarkUrl from '../assets/provider-brands/cloudflare.svg';
import deepinfraBrandMark from '../assets/provider-brands/deepinfra.svg';
import fireworksMarkUrl from '../assets/provider-brands/fireworks.svg';
import groqBrandMark from '../assets/provider-brands/groq.svg';
import huggingfaceBrandMark from '../assets/provider-brands/huggingface.svg';
import hunyuanBrandMark from '../assets/provider-brands/hunyuan.svg';
import tencentCloudBrandMark from '../assets/provider-brands/tencentcloud.svg';
import lmStudioBrandMark from '../assets/provider-brands/lmstudio.svg';
import localAiBrandMark from '../assets/provider-brands/localai.svg';
import mistralBrandMark from '../assets/provider-brands/mistral.svg';
import togetherBrandMark from '../assets/provider-brands/together.svg';
import nvidiaMarkUrl from '../assets/provider-brands/nvidia.svg';
import opencodeBrandMark from '../assets/provider-brands/opencode.svg';
import openrouterBrandMark from '../assets/provider-brands/openrouter.svg';
import stepfunBrandMark from '../assets/provider-brands/stepfun.svg';
import vercelBrandMark from '../assets/provider-brands/vercel.svg';
import volcengineBrandMark from '../assets/provider-brands/volcengine.svg';
import xaiMarkUrl from '../assets/provider-brands/xai.svg';
import xiaomiMiMoMarkUrl from '../assets/provider-brands/xiaomimimo.svg';
import zaiMarkUrl from '../assets/provider-brands/zai.svg';
import zenmuxBrandMark from '../assets/provider-brands/zenmux.svg';
const alibabaBrandMark = new URL('../../assets/provider-brands/alibabacloud.svg', import.meta.url).href;
const cerebrasBrandMark = new URL('../../assets/provider-brands/cerebras.svg', import.meta.url).href;
const cohereBrandMark = new URL('../../assets/provider-brands/cohere.svg', import.meta.url).href;
const cloudflareMarkUrl = new URL('../../assets/provider-brands/cloudflare.svg', import.meta.url).href;
const deepinfraBrandMark = new URL('../../assets/provider-brands/deepinfra.svg', import.meta.url).href;
const fireworksMarkUrl = new URL('../../assets/provider-brands/fireworks.svg', import.meta.url).href;
const groqBrandMark = new URL('../../assets/provider-brands/groq.svg', import.meta.url).href;
const huggingfaceBrandMark = new URL('../../assets/provider-brands/huggingface.svg', import.meta.url).href;
const hunyuanBrandMark = new URL('../../assets/provider-brands/hunyuan.svg', import.meta.url).href;
const tencentCloudBrandMark = new URL('../../assets/provider-brands/tencentcloud.svg', import.meta.url).href;
const lmStudioBrandMark = new URL('../../assets/provider-brands/lmstudio.svg', import.meta.url).href;
const localAiBrandMark = new URL('../../assets/provider-brands/localai.svg', import.meta.url).href;
const mistralBrandMark = new URL('../../assets/provider-brands/mistral.svg', import.meta.url).href;
const togetherBrandMark = new URL('../../assets/provider-brands/together.svg', import.meta.url).href;
const nvidiaMarkUrl = new URL('../../assets/provider-brands/nvidia.svg', import.meta.url).href;
const opencodeBrandMark = new URL('../../assets/provider-brands/opencode.svg', import.meta.url).href;
const openrouterBrandMark = new URL('../../assets/provider-brands/openrouter.svg', import.meta.url).href;
const stepfunBrandMark = new URL('../../assets/provider-brands/stepfun.svg', import.meta.url).href;
const vercelBrandMark = new URL('../../assets/provider-brands/vercel.svg', import.meta.url).href;
const volcengineBrandMark = new URL('../../assets/provider-brands/volcengine.svg', import.meta.url).href;
const xaiMarkUrl = new URL('../../assets/provider-brands/xai.svg', import.meta.url).href;
const xiaomiMiMoMarkUrl = new URL('../../assets/provider-brands/xiaomimimo.svg', import.meta.url).href;
const zaiMarkUrl = new URL('../../assets/provider-brands/zai.svg', import.meta.url).href;
const zenmuxBrandMark = new URL('../../assets/provider-brands/zenmux.svg', import.meta.url).href;

// ZenMux mark vendored byte-for-byte from Lobe Icons:
// - repository: https://github.com/lobehub/lobe-icons
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ function revealWordmark(element: HTMLDivElement | null, content: HTMLDivElement
}
}

export function WorkHubRoot() {
export function WorkHubRoot({ renderProviderMark }: {
/** Injected by the composition root so the provider SVG library stays out of the main-process test build. */
renderProviderMark: NonNullable<import('@maka/ui').ComposerProps['renderProviderMark']>;
}) {
const highlight = useWorkHubHighlightState();
const composer = useRef<ComposerHandle>(null);
const draftRestore = useRef<((sessionId: string, draft: RestoredDraftContent) => void) | undefined>(undefined);
Expand Down Expand Up @@ -371,6 +374,7 @@ export function WorkHubRoot() {
activeModel={newWorkNativeModel?.model}
activeModelLabel={newWorkModelChoice?.label}
activeProviderType={newWorkModelChoice?.providerType}
renderProviderMark={renderProviderMark}
activeModelConnectionId={newWorkNativeModel?.llmConnectionId}
activeModelConnectionSlug={newWorkNativeModel?.llmConnectionSlug}
modelChoices={controller.choices}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import {
useUiLocale,
Banner,
} from "@maka/ui";
import { ProviderBrandMark } from "./provider-brand-marks";
import { ProviderBrandMark } from "../features/connection-settings/index.js";
import { PasswordInput } from "./password-input";
import { settingsActionErrorMessage } from "./settings-error-copy";
import { useActionGuard, useKeyedActionGuard } from "./use-action-guard";
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/settings/provider-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import type { ProviderType } from '@maka/core/llm-connections';
import { ProviderBrandMark } from './provider-brand-marks';
import { ProviderBrandMark } from '../features/connection-settings/index.js';
export { providerDisplay } from './provider-display-copy';

// Kept as a thin wrapper so the many `ProviderLogo` call sites stay put.
Expand Down
Loading