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
Binary file added .maka-shots/pr-5562/settings-after-collapsed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .maka-shots/pr-5562/settings-after-enabled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .maka-shots/pr-5562/settings-before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"src/renderer/locales/settings-data-copy.ts",
"src/renderer/locales/settings-external-agents-copy.ts",
"src/renderer/locales/settings-health-copy.ts",
"src/renderer/locales/settings-jev-copy.ts",
"src/renderer/locales/settings-memory-copy.ts",
"src/renderer/locales/settings-navigation-copy.ts",
"src/renderer/locales/settings-preferences-copy.ts",
Expand Down Expand Up @@ -1486,6 +1487,15 @@
"actionFactories": [],
"dependencyPaths": {}
},
"src/renderer/locales/settings-jev-copy.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {}
},
"src/renderer/locales/settings-memory-copy.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
Expand Down Expand Up @@ -2408,6 +2418,7 @@
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"../features/jev-settings": 1,
"../features/network-proxy/index.js": 1,
"../locales/settings-preferences-copy.js": 1,
"../locales/settings-shared-copy.js": 1,
Expand Down
12 changes: 11 additions & 1 deletion apps/desktop/scripts/workhub-browser-presentation-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,17 @@ async function run() {
const point = await page.executeJavaScript(`(() => { const r = document.querySelector('button').getBoundingClientRect(); return {x:r.x+r.width/2,y:r.y+r.height/2}; })()`);
await page.debugger.sendCommand('Input.dispatchMouseEvent', { type: 'mousePressed', ...point, button: 'left', buttons: 1, clickCount: 1 });
await page.debugger.sendCommand('Input.dispatchMouseEvent', { type: 'mouseReleased', ...point, button: 'left', buttons: 0, clickCount: 1 });
assert.equal(await page.executeJavaScript("document.querySelector('button').textContent"), 'Clicked');
// CDP input acknowledgement can precede the renderer's click handler on
// Linux. Observe its effect without dispatching another click, so a lost
// event or broken background-page attachment still fails this smoke.
const clickDeadline = Date.now() + 3_000;
let buttonText;
do {
buttonText = await page.executeJavaScript("document.querySelector('button').textContent");
if (buttonText === 'Clicked') break;
await wait(20);
} while (Date.now() < clickDeadline);
assert.equal(buttonText, 'Clicked', 'background page must handle the native click after Main closes');
await closedMainLease.release();
console.log('PASS closing Main preserves the background page and native clicks');
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,3 +522,34 @@ test("compound config operations share the lane without re-entering it", async (
"config:end",
]);
});

test('Jev settings write Host policy and vault, project only a mask, and never persist the key locally', async () => {
let policy = createDefaultRuntimePolicy();
let secret: string | undefined;
const local = createDefaultSettings();
const module = createRuntimeHostSettingsModule({
client: {
queryRuntimePolicy: async () => ({ revision: 1, policy }),
queryCredential: async (locator: { scope: string }) => locator.scope === 'jev' && secret
? { configured: true, credentialId: 'jev-key', revision: 1, updatedAt: 1, locator } : null,
setCredential: async (input: { locator: { scope: string }; secret: string }) => {
assert.equal(input.locator.scope, 'jev'); secret = input.secret; return { kind: 'committed' };
},
deleteCredential: async () => { secret = undefined; return { kind: 'committed' }; },
updateRuntimePolicy: async (mutation: () => { kind: string; value: { enabled: boolean } }) => {
const operation = mutation(); assert.equal(operation.kind, 'set_jev');
policy = { ...policy, jev: operation.value }; return { revision: 2, policy };
},
} as never,
settingsStore: { get: async () => local, update: async () => { assert.fail('Host settings must not write Desktop settings'); } } as never,
applyClientSettings: async () => {},
});
const saved = await module.update({ jev: { apiKey: 'jev-secret', enabled: true } });
assert.deepEqual(saved.jev, { enabled: true, apiKey: '••••••••' });
assert.equal(JSON.stringify(local).includes('jev-secret'), false);
await module.update({ jev: { apiKey: '••••••••', enabled: false } });
assert.equal(secret, 'jev-secret');
const removed = await module.update({ jev: { apiKey: '', enabled: true } });
assert.deepEqual(removed.jev, { enabled: false, apiKey: '' });
assert.equal(secret, undefined);
});
18 changes: 17 additions & 1 deletion apps/desktop/src/main/runtime-host-settings-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type RuntimeHostSettingsClient = Pick<
| "updateRuntimePolicyIf"
>;

const JEV_CREDENTIAL: CredentialLocator = { scope: 'jev', kind: 'api_key' };
const PROXY_CREDENTIAL: CredentialLocator = {
scope: "network_proxy",
kind: "password",
Expand Down Expand Up @@ -262,16 +263,18 @@ async function testNetworkProxyWithoutLane(
async function loadRuntimeHostSettingsWithoutLane(
deps: RuntimeHostSettingsModuleDeps,
): Promise<RuntimeHostAppSettings> {
const [local, runtimePolicy, proxyCredential, webSearchCredential] =
const [local, runtimePolicy, proxyCredential, webSearchCredential, jevCredential] =
await Promise.all([
deps.settingsStore.get(),
deps.client.queryRuntimePolicy(),
deps.client.queryCredential(PROXY_CREDENTIAL),
deps.client.queryCredential(WEB_SEARCH_CREDENTIAL),
deps.client.queryCredential(JEV_CREDENTIAL),
]);
const policy = runtimePolicy.policy;
return {
...local,
jev: { enabled: policy.jev?.enabled === true, apiKey: jevCredential?.configured ? SENSITIVE_PLACEHOLDER : '' },
network: {
proxy: {
...policy.networkProxy,
Expand Down Expand Up @@ -347,6 +350,19 @@ async function applyHostPatchWithoutLane(
guard?: RuntimeHostSettingsUpdateGuard,
): Promise<number> {
let skippedCredentials = 0;
if (patch.jev) {
const apiKey = patch.jev.apiKey;
const removingKey = typeof apiKey === "string" && apiKey !== SENSITIVE_PLACEHOLDER && !apiKey.trim();
if (apiKey !== undefined && apiKey !== SENSITIVE_PLACEHOLDER) {
if (apiKey.trim()) await setCredential(client, JEV_CREDENTIAL, apiKey.trim());
else await deleteCredential(client, JEV_CREDENTIAL);
}
if (patch.jev.enabled !== undefined || removingKey) {
await client.updateRuntimePolicy(() => ({
kind: 'set_jev', value: { enabled: removingKey ? false : patch.jev!.enabled === true },
}));
}
}
if (patch.network?.proxy) {
skippedCredentials += await updateNetworkProxy(client, patch.network.proxy);
}
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/settings-ipc-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export function maskAppSettings(
): AppSettings {
return {
...settings,
jev: { ...settings.jev, apiKey: maskSensitive(settings.jev.apiKey) ?? "" },
botChat: {
...settings.botChat,
channels: Object.fromEntries(
Expand Down Expand Up @@ -135,6 +136,8 @@ export function maskAppSettings(
export function stripSettingsSecretsForExport(
settings: AppSettings,
): Record<string, unknown> {
const jev = { ...settings.jev } as Record<string, unknown>;
delete jev.apiKey;
const proxy = { ...settings.network.proxy } as Record<string, unknown>;
delete proxy.password;
delete proxy.passwordConfigured;
Expand All @@ -155,6 +158,7 @@ export function stripSettingsSecretsForExport(

return {
...settings,
jev,
network: { ...settings.network, proxy },
botChat: { ...settings.botChat, channels },
webSearch: {
Expand Down
60 changes: 60 additions & 0 deletions apps/desktop/src/renderer/features/jev-settings/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 { useState, type ReactNode } from 'react';
import { JEV_COPY } from '../../locales/settings-jev-copy.js';
import { useActionGuard } from '../../application/contracts/settings-presentation/index.js';
import type { AppSettings, UpdateAppSettingsInput, UpdateAppSettingsResult } from '@maka/core/settings';
import { useMountedRef, useToast, useUiLocale } from '@maka/ui';


export function JevSettingsController(props: {
isInteractive: boolean;
onUpdate(patch: UpdateAppSettingsInput): Promise<UpdateAppSettingsResult>;
children(state: {
copy: typeof JEV_COPY.en;
key: string;
setKey(value: string): void;
saving: boolean;
save(patch: Partial<AppSettings['jev']>): Promise<void>;
}): ReactNode;
}) {

const locale = useUiLocale();
const copy = JEV_COPY[locale];
const [key, setKey] = useState('');
const [saving, setSaving] = useState(false);
const guard = useActionGuard<'save'>();
const mounted = useMountedRef();
const toast = useToast();
async function save(patch: Partial<AppSettings['jev']>) {
if (!props.isInteractive || !guard.begin('save')) return;
setSaving(true);
try {
await props.onUpdate({ jev: patch });
if (mounted.current && patch.apiKey !== undefined) setKey('');
} catch {
if (mounted.current) toast.error(copy.failure);
} finally {
guard.finish();
if (mounted.current) setSaving(false);
}
}
return props.children({ copy, key, setKey, saving, save });
}
47 changes: 47 additions & 0 deletions apps/desktop/src/renderer/locales/settings-jev-copy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 type { UiCatalog } from '@maka/core/ui-locale';

export const JEV_COPY = {
'zh-CN': {
advanced: '高级设置', title: 'Jev 辅助决策',
help: '使用 TypeSafe Jev 辅助 WorkHub 的意图分类和工作路由。会发送当前消息、近期对话与候选工作摘要;对话、执行和标题仍使用原模型。',
key: 'TypeSafe API Key', saved: '已保存密钥;输入新值以替换',
save: '保存密钥', saving: '正在保存…', clear: '移除密钥',
behavior: '判断不确定时请求澄清;服务不可用时使用原有路由。',
failure: 'Jev 设置保存失败,请重试。',
},
'zh-TW': {
advanced: '進階設定', title: 'Jev 輔助決策',
help: '使用 TypeSafe Jev 輔助 WorkHub 的意圖分類和工作路由。會傳送目前訊息、近期對話與候選工作摘要;對話、執行和標題仍使用原模型。',
key: 'TypeSafe API Key', saved: '已儲存金鑰;輸入新值以替換',
save: '儲存金鑰', saving: '正在儲存…', clear: '移除金鑰',
behavior: '判斷不確定時請求釐清;服務無法使用時採用原有路由。',
failure: 'Jev 設定儲存失敗,請重試。',
},
en: {
advanced: 'Advanced settings', title: 'Jev assisted decisions',
help: 'Use TypeSafe Jev for WorkHub intent classification and work routing. Sends the current message, recent conversation, and candidate work summaries. Conversation, execution, and titles keep their original models.',
key: 'TypeSafe API Key', saved: 'Key saved; enter a new value to replace it',
save: 'Save key', saving: 'Saving…', clear: 'Remove key',
behavior: 'Uncertain decisions ask for clarification; service failures use the existing router.',
failure: 'Could not save Jev settings. Please try again.',
},
} satisfies UiCatalog<Record<'advanced' | 'title' | 'help' | 'key' | 'saved' | 'save' | 'saving' | 'clear' | 'behavior' | 'failure', string>>;
40 changes: 40 additions & 0 deletions apps/desktop/src/renderer/settings/general-settings-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/

import { JevSettingsController } from '../features/jev-settings';
import { useEffect, useMemo, useState } from "react";
import { PersonalizationSettingsSection } from "./personalization-settings-section";
import {
Expand All @@ -33,6 +34,7 @@ import type {
NetworkProxySettings,
RuntimeHostNetworkProxySettings,
UpdateAppSettingsResult,
UpdateAppSettingsInput,
} from '@maka/core/settings';
import type {
IdentifiedLlmConnection,
Expand Down Expand Up @@ -73,6 +75,7 @@ import { getShellCopy } from "../locales/shell-copy.js";
import type { RuntimeHostSettingsConnectionsBridge } from '../features/connection-settings';
import { getSettingsSharedCopy } from '../locales/settings-shared-copy.js';
import {
RuntimeHostSettingsGenerationBoundary,
useOptionalRuntimeHostSettingsTarget,
useRuntimeHostSettingsTarget,
} from './runtime-host-settings-target.js';
Expand Down Expand Up @@ -309,6 +312,9 @@ export function GeneralSettingsPage(props: {
) : null}
{runtimeHostSettingsAvailable ? (
<>
{host && <RuntimeHostSettingsGenerationBoundary>
<JevSettingsSection settings={props.settings.jev} isInteractive={runtimeHostSettingsInteractive} onUpdate={props.onUpdate} />
</RuntimeHostSettingsGenerationBoundary>}
<ShellSettingsSection
settings={props.settings}
isInteractive={runtimeHostSettingsInteractive}
Expand Down Expand Up @@ -976,3 +982,37 @@ function csvList(value: string): string[] {
.map((part) => part.trim())
.filter(Boolean);
}

function JevSettingsSection({ settings, isInteractive, onUpdate }: {
settings: AppSettings['jev'];
isInteractive: boolean;
onUpdate(patch: UpdateAppSettingsInput): Promise<UpdateAppSettingsResult>;
}) {
return <JevSettingsController isInteractive={isInteractive} onUpdate={onUpdate}>
{({copy, key, setKey, saving, save}) => (

<details className="jevAdvancedSettings">
<summary>{copy.advanced}</summary>
<SettingsSection title={copy.title}>
<SettingsRow label={copy.title} description={copy.help} align="start" end={(
<Switch label={copy.title} isLabelHidden value={settings.enabled}
isDisabled={!isInteractive || saving || !settings.apiKey}
onChange={(enabled) => void save({ enabled })} />
)} />
<SettingsField><FormLayout>
<TextInput label={copy.key} type="password" value={key}
placeholder={settings.apiKey ? copy.saved : 'TypeSafe API Key'}
description={copy.behavior} isDisabled={!isInteractive || saving}
onChange={setKey} />
<SettingsActions>
<Button label={saving ? copy.saving : copy.save} variant="primary"
isDisabled={!isInteractive || saving || !key.trim()} onClick={() => void save({ apiKey: key.trim() })} />
{settings.apiKey && <Button label={copy.clear} variant="secondary"
isDisabled={!isInteractive || saving} onClick={() => void save({ apiKey: '', enabled: false })} />}
</SettingsActions>
</FormLayout></SettingsField>
</SettingsSection>
</details>
)}
</JevSettingsController>;
}
4 changes: 4 additions & 0 deletions apps/desktop/src/renderer/styles/settings/rows.css
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,7 @@
.maka-import-selection-spacer {
flex: 1 1 auto;
}

.jevAdvancedSettings { min-width: 0; }
.jevAdvancedSettings > summary { padding-block: var(--space-3); cursor: default; }
.jevAdvancedSettings > summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
1 change: 1 addition & 0 deletions apps/desktop/src/shared/settings-ownership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function hasRuntimeHostSettingsPatch(
patch: UpdateAppSettingsInput,
): boolean {
return Boolean(
patch.jev ||
patch.externalAgents ||
patch.shell ||
patch.network ||
Expand Down
Loading