Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6885faa
fix(cli): Preserve mid-turn image messages (#5183)
DragonnZhang Jul 13, 2026
94fe082
fix(desktop): address git branch badge review (#5247)
DragonnZhang Jul 13, 2026
cf4a78d
feat(extensions): add i18n support for extension displayName and desc…
DragonnZhang Jul 13, 2026
09d73a3
feat(desktop): compile macOS 26+ Liquid Glass Assets.car in brand-cre…
DragonnZhang Jul 13, 2026
19e2ef5
fix: Expand Windows ~\\ home paths and hide phantom (session) entries…
DragonnZhang Jul 13, 2026
737d4ef
fix(desktop): detect WebP and AVI in RIFF magic-byte sniffing (#5336)
DragonnZhang Jul 13, 2026
22f410a
fix(desktop): accept uppercase icon URL schemes (#5470)
DragonnZhang Jul 13, 2026
b803618
fix: accept uppercase endpoint URL schemes (#5443)
DragonnZhang Jul 13, 2026
946447a
fix(desktop): preserve uppercase favicon URLs (#5463)
DragonnZhang Jul 13, 2026
70e468c
fix(desktop): parse NO_PROXY ports strictly (#5498)
DragonnZhang Jul 13, 2026
fc6ac77
test(desktop): update blocked scheme open-url assertion (#5529)
DragonnZhang Jul 13, 2026
78b6348
fix(desktop): restore locale parity (#5537)
DragonnZhang Jul 13, 2026
9da8d0c
fix(desktop): parse server ports strictly (#5509)
DragonnZhang Jul 13, 2026
1b23e7d
fix(desktop): validate generic oauth token responses (#5511)
DragonnZhang Jul 13, 2026
287f48d
fix(desktop): allow double dots in bundle filenames (#5515)
DragonnZhang Jul 13, 2026
11c032e
test(desktop): align interceptor packaging contract (#5531)
DragonnZhang Jul 13, 2026
4085fcc
fix(desktop): keep sibling paths absolute (#5517)
DragonnZhang Jul 13, 2026
ef039a2
test(desktop): enable feedback flag in permission tests (#5533)
DragonnZhang Jul 13, 2026
7dbff4b
fix(desktop): separate transform data output lines (#5525)
DragonnZhang Jul 13, 2026
0b94813
fix(desktop): handle Windows file mentions (#5523)
DragonnZhang Jul 13, 2026
f1f666f
fix(desktop): consolidate path boundary checks (#5545)
DragonnZhang Jul 13, 2026
e516910
fix(desktop): reject fractional transfer sizes (#5527)
DragonnZhang Jul 13, 2026
416c1db
feat(desktop): show file preview in a resizable side panel instead of…
DragonnZhang Jul 13, 2026
d241dab
feat(memory): confirm auto-generated skills before persisting (#5616)
DragonnZhang Jul 13, 2026
bee2b93
fix(desktop): reject unsafe source slugs before deletion (#5829)
DragonnZhang Jul 13, 2026
8047f89
fix(desktop): harden remaining source path validation (#5914)
DragonnZhang Jul 13, 2026
ef26ef0
feat(memory): add a git-shared team memory tier (#5886)
DragonnZhang Jul 13, 2026
dfff8da
fix(desktop): normalize source slug validation errors (#5911)
DragonnZhang Jul 13, 2026
d1b2864
feat(desktop): voice dictation in the desktop app (#5856)
DragonnZhang Jul 13, 2026
76fb3ad
fix(cli): switch TUI prefix ✦→◆ to fix glyph overflow on some termina…
DragonnZhang Jul 13, 2026
8089c9d
fix(desktop): enforce transform_data isolation (#6285)
DragonnZhang Jul 13, 2026
304128e
fix(desktop): preserve glued automation history records (#6344)
DragonnZhang Jul 13, 2026
71625bb
fix(desktop): preserve MCP URL query suffixes (#6587)
DragonnZhang Jul 13, 2026
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
114 changes: 106 additions & 8 deletions .agents/skills/desktop-brand-builder/scripts/brand-create.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ import {
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { extname, join, resolve } from 'node:path';

interface BrandInput {
Expand DownExpand Up@@ -130,10 +132,15 @@ async function run(cmd: string[], cwd: string): Promise<void> {
}
}

interface BrandAssetsResult {
macIcon: string;
hasAssetsCar: boolean;
}

async function writeBrandAssets(
config: BrandConfig,
desktopRoot: string,
): Promise<string> {
): Promise<BrandAssetsResult> {
const requireFromDesktop = createRequire(join(desktopRoot, 'package.json'));
const sharp = requireFromDesktop('sharp') as typeof import('sharp');
const electronDir = join(desktopRoot, 'apps', 'electron');
Expand All@@ -157,7 +164,7 @@ async function writeBrandAssets(
await writePng(join(brandDir, 'dock.png'), 512);
await writePng(join(brandDir, 'symbol.png'), 512);

if (process.platform !== 'darwin') return 'icon.png';
if (process.platform !== 'darwin') return { macIcon: 'icon.png', hasAssetsCar: false };

const iconset = join(brandDir, 'icon.iconset');
rmSync(iconset, { recursive: true, force: true });
Expand All@@ -184,7 +191,91 @@ async function writeBrandAssets(
['iconutil', '-c', 'icns', iconset, '-o', join(brandDir, 'icon.icns')],
brandDir,
);
return 'icon.icns';

const hasAssetsCar = await compileAssetsCar(config, brandDir, writePng);
return { macIcon: 'icon.icns', hasAssetsCar };
}

async function compileAssetsCar(
config: BrandConfig,
brandDir: string,
writePng: (output: string, size: number) => Promise<void>,
): Promise<boolean> {
const xcassets = join(brandDir, 'Assets.xcassets');
const appiconset = join(xcassets, 'AppIcon.appiconset');
rmSync(xcassets, { recursive: true, force: true });
mkdirSync(appiconset, { recursive: true });

writeFileSync(
join(xcassets, 'Contents.json'),
JSON.stringify({ info: { author: 'xcode', version: 1 } }),
);

const entries = [
{ file: 'icon_16.png', size: 16, scale: '1x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '2x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '1x', dims: '32x32' },
{ file: 'icon_64.png', size: 64, scale: '2x', dims: '32x32' },
{ file: 'icon_128.png', size: 128, scale: '1x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '2x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '1x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '2x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '1x', dims: '512x512' },
{ file: 'icon_1024.png', size: 1024, scale: '2x', dims: '512x512' },
];

const uniqueSizes = new Set(entries.map((e) => e.size));
for (const size of uniqueSizes) {
await writePng(join(appiconset, `icon_${size}.png`), size);
}

writeFileSync(
join(appiconset, 'Contents.json'),
JSON.stringify({
images: entries.map((e) => ({
filename: e.file,
idiom: 'mac',
scale: e.scale,
size: e.dims,
})),
info: { author: 'xcode', version: 1 },
}),
);

const outDir = mkdtempSync(join(tmpdir(), 'assets-car-'));
const partialPlist = join(outDir, 'partial-info.plist');
const proc = Bun.spawn({
cmd: [
'xcrun', 'actool', xcassets,
'--compile', outDir,
'--app-icon', 'AppIcon',
'--platform', 'macosx',
'--minimum-deployment-target', '14.0',
'--output-partial-info-plist', partialPlist,
],
cwd: brandDir,
stdout: 'pipe',
stderr: 'pipe',
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
console.log('Warning: actool compilation failed, skipping Assets.car');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

const compiledCar = join(outDir, 'Assets.car');
if (!existsSync(compiledCar)) {
console.log('Warning: actool produced no Assets.car, skipping');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

copyFileSync(compiledCar, join(brandDir, 'Assets.car'));
rmSync(xcassets, { recursive: true, force: true });
rmSync(outDir, { recursive: true, force: true });
console.log('Assets.car compiled successfully');
return true;
}

function tsString(value: string): string {
Expand All@@ -203,8 +294,11 @@ function helpMenuLinks(config: BrandConfig): string {
]`;
}

function brandBlock(config: BrandConfig, macIcon: string): string {
function brandBlock(config: BrandConfig, macIcon: string, hasAssetsCar: boolean): string {
const resourceDir = `resources/brands/${config.brandId}`;
const liquidGlassLine = hasAssetsCar
? `\n liquidGlassAssetsCar: ${tsString(`${resourceDir}/Assets.car`)},`
: '';

return ` ${tsString(config.brandId)}: {
id: ${tsString(config.brandId)},
Expand All@@ -223,7 +317,7 @@ function brandBlock(config: BrandConfig, macIcon: string): string {
macIcon: ${tsString(`${resourceDir}/${macIcon}`)},
winIcon: ${tsString(`${resourceDir}/icon.png`)},
linuxIcon: ${tsString(`${resourceDir}/icon.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},${liquidGlassLine}
},
credits: '',
creditsShort: '',
Expand All@@ -236,6 +330,7 @@ function registerBrand(
config: BrandConfig,
desktopRoot: string,
macIcon: string,
hasAssetsCar: boolean,
): void {
const brandingPath = join(
desktopRoot,
Expand All@@ -259,22 +354,25 @@ function registerBrand(

writeFileSync(
brandingPath,
source.replace(marker, `\n${brandBlock(config, macIcon)}${marker}`),
source.replace(marker, `\n${brandBlock(config, macIcon, hasAssetsCar)}${marker}`),
);
}

async function main(): Promise<void> {
const desktopRoot = desktopRootFromArgs();
const config = loadConfig(configPathFromArgs());
const macIcon = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon);
const { macIcon, hasAssetsCar } = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon, hasAssetsCar);

console.log(`Created brand ${config.brandId}`);
console.log(`App name: ${config.appName}`);
console.log(`App ID: ${config.appId}`);
console.log(
`Assets: ${join(desktopRoot, 'apps', 'electron', 'resources', 'brands', config.brandId)}`,
);
if (hasAssetsCar) {
console.log('Assets.car: generated (macOS 26+ Liquid Glass icon)');
}
}

main().catch((error: unknown) => {
Expand Down
3 changes: 3 additions & 0 deletions apps/electron/build/entitlements.mac.plist
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,5 +10,8 @@
<!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Voice dictation: microphone access under the hardened runtime. -->
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
2 changes: 2 additions & 0 deletions apps/electron/electron-builder.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,6 +72,8 @@ mac:
# The value must match --app-icon used in actool (see afterPack.js)
extendInfo:
CFBundleIconName: AppIcon
# Voice dictation: shown in the macOS microphone permission prompt.
NSMicrophoneUsageDescription: Qwen Code uses the microphone for voice dictation in the prompt composer.
target:
- target: dmg
arch:
Expand Down
42 changes: 39 additions & 3 deletions apps/electron/src/main/__tests__/network-proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,18 @@ describe('parseNoProxyRules', () => {

it('parses host:port', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(rules).toEqual([{ host: 'example.com', port: 8080, wildcard: false }]);
expect(rules).toEqual([
{ host: 'example.com', port: 8080, wildcard: false },
]);
});

it('does not parse malformed port suffixes', () => {
expect(parseNoProxyRules('example.com:443abc')).toEqual([
{ host: 'example.com:443abc', wildcard: false },
]);
expect(parseNoProxyRules('[::1]:abc')).toEqual([
{ host: '[::1]:abc', wildcard: false },
]);
});
});

Expand All@@ -55,7 +66,9 @@ describe('shouldBypassProxy', () => {
it('respects port-scoped rules', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('http://example.com:8080/path', rules)).toBe(true);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(false);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(
false,
);
});

it('matches implicit default ports', () => {
Expand All@@ -71,7 +84,9 @@ describe('shouldBypassProxy', () => {

// Explicit port that differs from rule should not match
const rules8080 = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(false);
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(
false,
);
});

it('wildcard bypasses everything', () => {
Expand All@@ -84,6 +99,27 @@ describe('shouldBypassProxy', () => {
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
});

it('respects IPv6 port-scoped rules', () => {
const rules = parseNoProxyRules('[::1]:3000');
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
expect(shouldBypassProxy('http://[::1]:3001/path', rules)).toBe(false);
});

it('does not bypass for malformed port suffixes', () => {
expect(
shouldBypassProxy(
'https://example.com/path',
parseNoProxyRules('example.com:443abc'),
),
).toBe(false);
expect(
shouldBypassProxy(
'http://[::1]:3000/path',
parseNoProxyRules('[::1]:abc'),
),
).toBe(false);
});

it('matches exact IP literal', () => {
const rules = parseNoProxyRules('192.168.1.1');
expect(shouldBypassProxy('http://192.168.1.1/', rules)).toBe(true);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ const requestContext = {

const getDefaultThinkingLevelMock = mock(() => 'think');
const setDefaultThinkingLevelMock = mock((_level: string) => true);
const setVoiceModelMock = mock((_model: string) => {});
let mockedWorkspace: Record<string, unknown> | null = null;
let mockedWorkspaceConfig: Record<string, unknown> | null = null;
const getWorkspaceByNameOrIdMock = mock(
Expand DownExpand Up@@ -64,9 +65,14 @@ mock.module('@craft-agent/shared/config', () => ({
getWorkspaceByNameOrId: getWorkspaceByNameOrIdMock,
getDefaultThinkingLevel: getDefaultThinkingLevelMock,
setDefaultThinkingLevel: setDefaultThinkingLevelMock,
setVoiceModel: setVoiceModelMock,
isProtectedWorkspace: () => false,
}));

mock.module('@craft-agent/shared/config/storage', () => ({
setVoiceModel: setVoiceModelMock,
}));

mock.module('@craft-agent/shared/workspaces', () => ({
loadWorkspaceConfig: loadWorkspaceConfigMock,
}));
Expand DownExpand Up@@ -94,6 +100,7 @@ describe('settings default thinking RPC handlers', () => {
handlers.clear();
getDefaultThinkingLevelMock.mockClear();
setDefaultThinkingLevelMock.mockClear();
setVoiceModelMock.mockClear();
mockedWorkspace = null;
mockedWorkspaceConfig = null;
getWorkspaceByNameOrIdMock.mockClear();
Expand DownExpand Up@@ -189,6 +196,17 @@ describe('settings default thinking RPC handlers', () => {
expect(setDefaultThinkingLevelMock).not.toHaveBeenCalled();
});

it('accepts dated voice model variants supported by the transport resolver', async () => {
const setHandler = handlers.get(RPC_CHANNELS.input.SET_VOICE_MODEL);
expect(setHandler).toBeTruthy();

await setHandler!(requestContext, 'qwen3-asr-flash-2025-06-01');

expect(setVoiceModelMock).toHaveBeenCalledWith(
'qwen3-asr-flash-2025-06-01',
);
});

it('returns global permission mode through Qwen ACP', async () => {
const getHandler = handlers.get(
RPC_CHANNELS.settings.GET_GLOBAL_PERMISSION_MODE,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
chore(sync): sync qwen-code desktop updates by DragonnZhang · Pull Request #74 · modelstudioai/openwork · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6885faa
fix(cli): Preserve mid-turn image messages (#5183)
DragonnZhang Jul 13, 2026
94fe082
fix(desktop): address git branch badge review (#5247)
DragonnZhang Jul 13, 2026
cf4a78d
feat(extensions): add i18n support for extension displayName and desc…
DragonnZhang Jul 13, 2026
09d73a3
feat(desktop): compile macOS 26+ Liquid Glass Assets.car in brand-cre…
DragonnZhang Jul 13, 2026
19e2ef5
fix: Expand Windows ~\\ home paths and hide phantom (session) entries…
DragonnZhang Jul 13, 2026
737d4ef
fix(desktop): detect WebP and AVI in RIFF magic-byte sniffing (#5336)
DragonnZhang Jul 13, 2026
22f410a
fix(desktop): accept uppercase icon URL schemes (#5470)
DragonnZhang Jul 13, 2026
b803618
fix: accept uppercase endpoint URL schemes (#5443)
DragonnZhang Jul 13, 2026
946447a
fix(desktop): preserve uppercase favicon URLs (#5463)
DragonnZhang Jul 13, 2026
70e468c
fix(desktop): parse NO_PROXY ports strictly (#5498)
DragonnZhang Jul 13, 2026
fc6ac77
test(desktop): update blocked scheme open-url assertion (#5529)
DragonnZhang Jul 13, 2026
78b6348
fix(desktop): restore locale parity (#5537)
DragonnZhang Jul 13, 2026
9da8d0c
fix(desktop): parse server ports strictly (#5509)
DragonnZhang Jul 13, 2026
1b23e7d
fix(desktop): validate generic oauth token responses (#5511)
DragonnZhang Jul 13, 2026
287f48d
fix(desktop): allow double dots in bundle filenames (#5515)
DragonnZhang Jul 13, 2026
11c032e
test(desktop): align interceptor packaging contract (#5531)
DragonnZhang Jul 13, 2026
4085fcc
fix(desktop): keep sibling paths absolute (#5517)
DragonnZhang Jul 13, 2026
ef039a2
test(desktop): enable feedback flag in permission tests (#5533)
DragonnZhang Jul 13, 2026
7dbff4b
fix(desktop): separate transform data output lines (#5525)
DragonnZhang Jul 13, 2026
0b94813
fix(desktop): handle Windows file mentions (#5523)
DragonnZhang Jul 13, 2026
f1f666f
fix(desktop): consolidate path boundary checks (#5545)
DragonnZhang Jul 13, 2026
e516910
fix(desktop): reject fractional transfer sizes (#5527)
DragonnZhang Jul 13, 2026
416c1db
feat(desktop): show file preview in a resizable side panel instead of…
DragonnZhang Jul 13, 2026
d241dab
feat(memory): confirm auto-generated skills before persisting (#5616)
DragonnZhang Jul 13, 2026
bee2b93
fix(desktop): reject unsafe source slugs before deletion (#5829)
DragonnZhang Jul 13, 2026
8047f89
fix(desktop): harden remaining source path validation (#5914)
DragonnZhang Jul 13, 2026
ef26ef0
feat(memory): add a git-shared team memory tier (#5886)
DragonnZhang Jul 13, 2026
dfff8da
fix(desktop): normalize source slug validation errors (#5911)
DragonnZhang Jul 13, 2026
d1b2864
feat(desktop): voice dictation in the desktop app (#5856)
DragonnZhang Jul 13, 2026
76fb3ad
fix(cli): switch TUI prefix ✦→◆ to fix glyph overflow on some termina…
DragonnZhang Jul 13, 2026
8089c9d
fix(desktop): enforce transform_data isolation (#6285)
DragonnZhang Jul 13, 2026
304128e
fix(desktop): preserve glued automation history records (#6344)
DragonnZhang Jul 13, 2026
71625bb
fix(desktop): preserve MCP URL query suffixes (#6587)
DragonnZhang Jul 13, 2026
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
114 changes: 106 additions & 8 deletions .agents/skills/desktop-brand-builder/scripts/brand-create.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ import {
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { extname, join, resolve } from 'node:path';

interface BrandInput {
Expand DownExpand Up@@ -130,10 +132,15 @@ async function run(cmd: string[], cwd: string): Promise<void> {
}
}

interface BrandAssetsResult {
macIcon: string;
hasAssetsCar: boolean;
}

async function writeBrandAssets(
config: BrandConfig,
desktopRoot: string,
): Promise<string> {
): Promise<BrandAssetsResult> {
const requireFromDesktop = createRequire(join(desktopRoot, 'package.json'));
const sharp = requireFromDesktop('sharp') as typeof import('sharp');
const electronDir = join(desktopRoot, 'apps', 'electron');
Expand All@@ -157,7 +164,7 @@ async function writeBrandAssets(
await writePng(join(brandDir, 'dock.png'), 512);
await writePng(join(brandDir, 'symbol.png'), 512);

if (process.platform !== 'darwin') return 'icon.png';
if (process.platform !== 'darwin') return { macIcon: 'icon.png', hasAssetsCar: false };

const iconset = join(brandDir, 'icon.iconset');
rmSync(iconset, { recursive: true, force: true });
Expand All@@ -184,7 +191,91 @@ async function writeBrandAssets(
['iconutil', '-c', 'icns', iconset, '-o', join(brandDir, 'icon.icns')],
brandDir,
);
return 'icon.icns';

const hasAssetsCar = await compileAssetsCar(config, brandDir, writePng);
return { macIcon: 'icon.icns', hasAssetsCar };
}

async function compileAssetsCar(
config: BrandConfig,
brandDir: string,
writePng: (output: string, size: number) => Promise<void>,
): Promise<boolean> {
const xcassets = join(brandDir, 'Assets.xcassets');
const appiconset = join(xcassets, 'AppIcon.appiconset');
rmSync(xcassets, { recursive: true, force: true });
mkdirSync(appiconset, { recursive: true });

writeFileSync(
join(xcassets, 'Contents.json'),
JSON.stringify({ info: { author: 'xcode', version: 1 } }),
);

const entries = [
{ file: 'icon_16.png', size: 16, scale: '1x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '2x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '1x', dims: '32x32' },
{ file: 'icon_64.png', size: 64, scale: '2x', dims: '32x32' },
{ file: 'icon_128.png', size: 128, scale: '1x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '2x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '1x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '2x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '1x', dims: '512x512' },
{ file: 'icon_1024.png', size: 1024, scale: '2x', dims: '512x512' },
];

const uniqueSizes = new Set(entries.map((e) => e.size));
for (const size of uniqueSizes) {
await writePng(join(appiconset, `icon_${size}.png`), size);
}

writeFileSync(
join(appiconset, 'Contents.json'),
JSON.stringify({
images: entries.map((e) => ({
filename: e.file,
idiom: 'mac',
scale: e.scale,
size: e.dims,
})),
info: { author: 'xcode', version: 1 },
}),
);

const outDir = mkdtempSync(join(tmpdir(), 'assets-car-'));
const partialPlist = join(outDir, 'partial-info.plist');
const proc = Bun.spawn({
cmd: [
'xcrun', 'actool', xcassets,
'--compile', outDir,
'--app-icon', 'AppIcon',
'--platform', 'macosx',
'--minimum-deployment-target', '14.0',
'--output-partial-info-plist', partialPlist,
],
cwd: brandDir,
stdout: 'pipe',
stderr: 'pipe',
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
console.log('Warning: actool compilation failed, skipping Assets.car');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

const compiledCar = join(outDir, 'Assets.car');
if (!existsSync(compiledCar)) {
console.log('Warning: actool produced no Assets.car, skipping');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

copyFileSync(compiledCar, join(brandDir, 'Assets.car'));
rmSync(xcassets, { recursive: true, force: true });
rmSync(outDir, { recursive: true, force: true });
console.log('Assets.car compiled successfully');
return true;
}

function tsString(value: string): string {
Expand All@@ -203,8 +294,11 @@ function helpMenuLinks(config: BrandConfig): string {
]`;
}

function brandBlock(config: BrandConfig, macIcon: string): string {
function brandBlock(config: BrandConfig, macIcon: string, hasAssetsCar: boolean): string {
const resourceDir = `resources/brands/${config.brandId}`;
const liquidGlassLine = hasAssetsCar
? `\n liquidGlassAssetsCar: ${tsString(`${resourceDir}/Assets.car`)},`
: '';

return ` ${tsString(config.brandId)}: {
id: ${tsString(config.brandId)},
Expand All@@ -223,7 +317,7 @@ function brandBlock(config: BrandConfig, macIcon: string): string {
macIcon: ${tsString(`${resourceDir}/${macIcon}`)},
winIcon: ${tsString(`${resourceDir}/icon.png`)},
linuxIcon: ${tsString(`${resourceDir}/icon.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},${liquidGlassLine}
},
credits: '',
creditsShort: '',
Expand All@@ -236,6 +330,7 @@ function registerBrand(
config: BrandConfig,
desktopRoot: string,
macIcon: string,
hasAssetsCar: boolean,
): void {
const brandingPath = join(
desktopRoot,
Expand All@@ -259,22 +354,25 @@ function registerBrand(

writeFileSync(
brandingPath,
source.replace(marker, `\n${brandBlock(config, macIcon)}${marker}`),
source.replace(marker, `\n${brandBlock(config, macIcon, hasAssetsCar)}${marker}`),
);
}

async function main(): Promise<void> {
const desktopRoot = desktopRootFromArgs();
const config = loadConfig(configPathFromArgs());
const macIcon = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon);
const { macIcon, hasAssetsCar } = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon, hasAssetsCar);

console.log(`Created brand ${config.brandId}`);
console.log(`App name: ${config.appName}`);
console.log(`App ID: ${config.appId}`);
console.log(
`Assets: ${join(desktopRoot, 'apps', 'electron', 'resources', 'brands', config.brandId)}`,
);
if (hasAssetsCar) {
console.log('Assets.car: generated (macOS 26+ Liquid Glass icon)');
}
}

main().catch((error: unknown) => {
Expand Down
3 changes: 3 additions & 0 deletions apps/electron/build/entitlements.mac.plist
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,5 +10,8 @@
<!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Voice dictation: microphone access under the hardened runtime. -->
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
2 changes: 2 additions & 0 deletions apps/electron/electron-builder.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,6 +72,8 @@ mac:
# The value must match --app-icon used in actool (see afterPack.js)
extendInfo:
CFBundleIconName: AppIcon
# Voice dictation: shown in the macOS microphone permission prompt.
NSMicrophoneUsageDescription: Qwen Code uses the microphone for voice dictation in the prompt composer.
target:
- target: dmg
arch:
Expand Down
42 changes: 39 additions & 3 deletions apps/electron/src/main/__tests__/network-proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,18 @@ describe('parseNoProxyRules', () => {

it('parses host:port', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(rules).toEqual([{ host: 'example.com', port: 8080, wildcard: false }]);
expect(rules).toEqual([
{ host: 'example.com', port: 8080, wildcard: false },
]);
});

it('does not parse malformed port suffixes', () => {
expect(parseNoProxyRules('example.com:443abc')).toEqual([
{ host: 'example.com:443abc', wildcard: false },
]);
expect(parseNoProxyRules('[::1]:abc')).toEqual([
{ host: '[::1]:abc', wildcard: false },
]);
});
});

Expand All@@ -55,7 +66,9 @@ describe('shouldBypassProxy', () => {
it('respects port-scoped rules', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('http://example.com:8080/path', rules)).toBe(true);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(false);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(
false,
);
});

it('matches implicit default ports', () => {
Expand All@@ -71,7 +84,9 @@ describe('shouldBypassProxy', () => {

// Explicit port that differs from rule should not match
const rules8080 = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(false);
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(
false,
);
});

it('wildcard bypasses everything', () => {
Expand All@@ -84,6 +99,27 @@ describe('shouldBypassProxy', () => {
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
});

it('respects IPv6 port-scoped rules', () => {
const rules = parseNoProxyRules('[::1]:3000');
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
expect(shouldBypassProxy('http://[::1]:3001/path', rules)).toBe(false);
});

it('does not bypass for malformed port suffixes', () => {
expect(
shouldBypassProxy(
'https://example.com/path',
parseNoProxyRules('example.com:443abc'),
),
).toBe(false);
expect(
shouldBypassProxy(
'http://[::1]:3000/path',
parseNoProxyRules('[::1]:abc'),
),
).toBe(false);
});

it('matches exact IP literal', () => {
const rules = parseNoProxyRules('192.168.1.1');
expect(shouldBypassProxy('http://192.168.1.1/', rules)).toBe(true);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ const requestContext = {

const getDefaultThinkingLevelMock = mock(() => 'think');
const setDefaultThinkingLevelMock = mock((_level: string) => true);
const setVoiceModelMock = mock((_model: string) => {});
let mockedWorkspace: Record<string, unknown> | null = null;
let mockedWorkspaceConfig: Record<string, unknown> | null = null;
const getWorkspaceByNameOrIdMock = mock(
Expand DownExpand Up@@ -64,9 +65,14 @@ mock.module('@craft-agent/shared/config', () => ({
getWorkspaceByNameOrId: getWorkspaceByNameOrIdMock,
getDefaultThinkingLevel: getDefaultThinkingLevelMock,
setDefaultThinkingLevel: setDefaultThinkingLevelMock,
setVoiceModel: setVoiceModelMock,
isProtectedWorkspace: () => false,
}));

mock.module('@craft-agent/shared/config/storage', () => ({
setVoiceModel: setVoiceModelMock,
}));

mock.module('@craft-agent/shared/workspaces', () => ({
loadWorkspaceConfig: loadWorkspaceConfigMock,
}));
Expand DownExpand Up@@ -94,6 +100,7 @@ describe('settings default thinking RPC handlers', () => {
handlers.clear();
getDefaultThinkingLevelMock.mockClear();
setDefaultThinkingLevelMock.mockClear();
setVoiceModelMock.mockClear();
mockedWorkspace = null;
mockedWorkspaceConfig = null;
getWorkspaceByNameOrIdMock.mockClear();
Expand DownExpand Up@@ -189,6 +196,17 @@ describe('settings default thinking RPC handlers', () => {
expect(setDefaultThinkingLevelMock).not.toHaveBeenCalled();
});

it('accepts dated voice model variants supported by the transport resolver', async () => {
const setHandler = handlers.get(RPC_CHANNELS.input.SET_VOICE_MODEL);
expect(setHandler).toBeTruthy();

await setHandler!(requestContext, 'qwen3-asr-flash-2025-06-01');

expect(setVoiceModelMock).toHaveBeenCalledWith(
'qwen3-asr-flash-2025-06-01',
);
});

it('returns global permission mode through Qwen ACP', async () => {
const getHandler = handlers.get(
RPC_CHANNELS.settings.GET_GLOBAL_PERMISSION_MODE,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' chore(sync): sync qwen-code desktop updates by DragonnZhang · Pull Request #74 · modelstudioai/openwork · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6885faa
fix(cli): Preserve mid-turn image messages (#5183)
DragonnZhang Jul 13, 2026
94fe082
fix(desktop): address git branch badge review (#5247)
DragonnZhang Jul 13, 2026
cf4a78d
feat(extensions): add i18n support for extension displayName and desc…
DragonnZhang Jul 13, 2026
09d73a3
feat(desktop): compile macOS 26+ Liquid Glass Assets.car in brand-cre…
DragonnZhang Jul 13, 2026
19e2ef5
fix: Expand Windows ~\\ home paths and hide phantom (session) entries…
DragonnZhang Jul 13, 2026
737d4ef
fix(desktop): detect WebP and AVI in RIFF magic-byte sniffing (#5336)
DragonnZhang Jul 13, 2026
22f410a
fix(desktop): accept uppercase icon URL schemes (#5470)
DragonnZhang Jul 13, 2026
b803618
fix: accept uppercase endpoint URL schemes (#5443)
DragonnZhang Jul 13, 2026
946447a
fix(desktop): preserve uppercase favicon URLs (#5463)
DragonnZhang Jul 13, 2026
70e468c
fix(desktop): parse NO_PROXY ports strictly (#5498)
DragonnZhang Jul 13, 2026
fc6ac77
test(desktop): update blocked scheme open-url assertion (#5529)
DragonnZhang Jul 13, 2026
78b6348
fix(desktop): restore locale parity (#5537)
DragonnZhang Jul 13, 2026
9da8d0c
fix(desktop): parse server ports strictly (#5509)
DragonnZhang Jul 13, 2026
1b23e7d
fix(desktop): validate generic oauth token responses (#5511)
DragonnZhang Jul 13, 2026
287f48d
fix(desktop): allow double dots in bundle filenames (#5515)
DragonnZhang Jul 13, 2026
11c032e
test(desktop): align interceptor packaging contract (#5531)
DragonnZhang Jul 13, 2026
4085fcc
fix(desktop): keep sibling paths absolute (#5517)
DragonnZhang Jul 13, 2026
ef039a2
test(desktop): enable feedback flag in permission tests (#5533)
DragonnZhang Jul 13, 2026
7dbff4b
fix(desktop): separate transform data output lines (#5525)
DragonnZhang Jul 13, 2026
0b94813
fix(desktop): handle Windows file mentions (#5523)
DragonnZhang Jul 13, 2026
f1f666f
fix(desktop): consolidate path boundary checks (#5545)
DragonnZhang Jul 13, 2026
e516910
fix(desktop): reject fractional transfer sizes (#5527)
DragonnZhang Jul 13, 2026
416c1db
feat(desktop): show file preview in a resizable side panel instead of…
DragonnZhang Jul 13, 2026
d241dab
feat(memory): confirm auto-generated skills before persisting (#5616)
DragonnZhang Jul 13, 2026
bee2b93
fix(desktop): reject unsafe source slugs before deletion (#5829)
DragonnZhang Jul 13, 2026
8047f89
fix(desktop): harden remaining source path validation (#5914)
DragonnZhang Jul 13, 2026
ef26ef0
feat(memory): add a git-shared team memory tier (#5886)
DragonnZhang Jul 13, 2026
dfff8da
fix(desktop): normalize source slug validation errors (#5911)
DragonnZhang Jul 13, 2026
d1b2864
feat(desktop): voice dictation in the desktop app (#5856)
DragonnZhang Jul 13, 2026
76fb3ad
fix(cli): switch TUI prefix ✦→◆ to fix glyph overflow on some termina…
DragonnZhang Jul 13, 2026
8089c9d
fix(desktop): enforce transform_data isolation (#6285)
DragonnZhang Jul 13, 2026
304128e
fix(desktop): preserve glued automation history records (#6344)
DragonnZhang Jul 13, 2026
71625bb
fix(desktop): preserve MCP URL query suffixes (#6587)
DragonnZhang Jul 13, 2026
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
114 changes: 106 additions & 8 deletions .agents/skills/desktop-brand-builder/scripts/brand-create.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ import {
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { extname, join, resolve } from 'node:path';

interface BrandInput {
Expand DownExpand Up@@ -130,10 +132,15 @@ async function run(cmd: string[], cwd: string): Promise<void> {
}
}

interface BrandAssetsResult {
macIcon: string;
hasAssetsCar: boolean;
}

async function writeBrandAssets(
config: BrandConfig,
desktopRoot: string,
): Promise<string> {
): Promise<BrandAssetsResult> {
const requireFromDesktop = createRequire(join(desktopRoot, 'package.json'));
const sharp = requireFromDesktop('sharp') as typeof import('sharp');
const electronDir = join(desktopRoot, 'apps', 'electron');
Expand All@@ -157,7 +164,7 @@ async function writeBrandAssets(
await writePng(join(brandDir, 'dock.png'), 512);
await writePng(join(brandDir, 'symbol.png'), 512);

if (process.platform !== 'darwin') return 'icon.png';
if (process.platform !== 'darwin') return { macIcon: 'icon.png', hasAssetsCar: false };

const iconset = join(brandDir, 'icon.iconset');
rmSync(iconset, { recursive: true, force: true });
Expand All@@ -184,7 +191,91 @@ async function writeBrandAssets(
['iconutil', '-c', 'icns', iconset, '-o', join(brandDir, 'icon.icns')],
brandDir,
);
return 'icon.icns';

const hasAssetsCar = await compileAssetsCar(config, brandDir, writePng);
return { macIcon: 'icon.icns', hasAssetsCar };
}

async function compileAssetsCar(
config: BrandConfig,
brandDir: string,
writePng: (output: string, size: number) => Promise<void>,
): Promise<boolean> {
const xcassets = join(brandDir, 'Assets.xcassets');
const appiconset = join(xcassets, 'AppIcon.appiconset');
rmSync(xcassets, { recursive: true, force: true });
mkdirSync(appiconset, { recursive: true });

writeFileSync(
join(xcassets, 'Contents.json'),
JSON.stringify({ info: { author: 'xcode', version: 1 } }),
);

const entries = [
{ file: 'icon_16.png', size: 16, scale: '1x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '2x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '1x', dims: '32x32' },
{ file: 'icon_64.png', size: 64, scale: '2x', dims: '32x32' },
{ file: 'icon_128.png', size: 128, scale: '1x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '2x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '1x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '2x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '1x', dims: '512x512' },
{ file: 'icon_1024.png', size: 1024, scale: '2x', dims: '512x512' },
];

const uniqueSizes = new Set(entries.map((e) => e.size));
for (const size of uniqueSizes) {
await writePng(join(appiconset, `icon_${size}.png`), size);
}

writeFileSync(
join(appiconset, 'Contents.json'),
JSON.stringify({
images: entries.map((e) => ({
filename: e.file,
idiom: 'mac',
scale: e.scale,
size: e.dims,
})),
info: { author: 'xcode', version: 1 },
}),
);

const outDir = mkdtempSync(join(tmpdir(), 'assets-car-'));
const partialPlist = join(outDir, 'partial-info.plist');
const proc = Bun.spawn({
cmd: [
'xcrun', 'actool', xcassets,
'--compile', outDir,
'--app-icon', 'AppIcon',
'--platform', 'macosx',
'--minimum-deployment-target', '14.0',
'--output-partial-info-plist', partialPlist,
],
cwd: brandDir,
stdout: 'pipe',
stderr: 'pipe',
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
console.log('Warning: actool compilation failed, skipping Assets.car');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

const compiledCar = join(outDir, 'Assets.car');
if (!existsSync(compiledCar)) {
console.log('Warning: actool produced no Assets.car, skipping');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

copyFileSync(compiledCar, join(brandDir, 'Assets.car'));
rmSync(xcassets, { recursive: true, force: true });
rmSync(outDir, { recursive: true, force: true });
console.log('Assets.car compiled successfully');
return true;
}

function tsString(value: string): string {
Expand All@@ -203,8 +294,11 @@ function helpMenuLinks(config: BrandConfig): string {
]`;
}

function brandBlock(config: BrandConfig, macIcon: string): string {
function brandBlock(config: BrandConfig, macIcon: string, hasAssetsCar: boolean): string {
const resourceDir = `resources/brands/${config.brandId}`;
const liquidGlassLine = hasAssetsCar
? `\n liquidGlassAssetsCar: ${tsString(`${resourceDir}/Assets.car`)},`
: '';

return ` ${tsString(config.brandId)}: {
id: ${tsString(config.brandId)},
Expand All@@ -223,7 +317,7 @@ function brandBlock(config: BrandConfig, macIcon: string): string {
macIcon: ${tsString(`${resourceDir}/${macIcon}`)},
winIcon: ${tsString(`${resourceDir}/icon.png`)},
linuxIcon: ${tsString(`${resourceDir}/icon.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},${liquidGlassLine}
},
credits: '',
creditsShort: '',
Expand All@@ -236,6 +330,7 @@ function registerBrand(
config: BrandConfig,
desktopRoot: string,
macIcon: string,
hasAssetsCar: boolean,
): void {
const brandingPath = join(
desktopRoot,
Expand All@@ -259,22 +354,25 @@ function registerBrand(

writeFileSync(
brandingPath,
source.replace(marker, `\n${brandBlock(config, macIcon)}${marker}`),
source.replace(marker, `\n${brandBlock(config, macIcon, hasAssetsCar)}${marker}`),
);
}

async function main(): Promise<void> {
const desktopRoot = desktopRootFromArgs();
const config = loadConfig(configPathFromArgs());
const macIcon = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon);
const { macIcon, hasAssetsCar } = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon, hasAssetsCar);

console.log(`Created brand ${config.brandId}`);
console.log(`App name: ${config.appName}`);
console.log(`App ID: ${config.appId}`);
console.log(
`Assets: ${join(desktopRoot, 'apps', 'electron', 'resources', 'brands', config.brandId)}`,
);
if (hasAssetsCar) {
console.log('Assets.car: generated (macOS 26+ Liquid Glass icon)');
}
}

main().catch((error: unknown) => {
Expand Down
3 changes: 3 additions & 0 deletions apps/electron/build/entitlements.mac.plist
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,5 +10,8 @@
<!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Voice dictation: microphone access under the hardened runtime. -->
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
2 changes: 2 additions & 0 deletions apps/electron/electron-builder.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,6 +72,8 @@ mac:
# The value must match --app-icon used in actool (see afterPack.js)
extendInfo:
CFBundleIconName: AppIcon
# Voice dictation: shown in the macOS microphone permission prompt.
NSMicrophoneUsageDescription: Qwen Code uses the microphone for voice dictation in the prompt composer.
target:
- target: dmg
arch:
Expand Down
42 changes: 39 additions & 3 deletions apps/electron/src/main/__tests__/network-proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,18 @@ describe('parseNoProxyRules', () => {

it('parses host:port', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(rules).toEqual([{ host: 'example.com', port: 8080, wildcard: false }]);
expect(rules).toEqual([
{ host: 'example.com', port: 8080, wildcard: false },
]);
});

it('does not parse malformed port suffixes', () => {
expect(parseNoProxyRules('example.com:443abc')).toEqual([
{ host: 'example.com:443abc', wildcard: false },
]);
expect(parseNoProxyRules('[::1]:abc')).toEqual([
{ host: '[::1]:abc', wildcard: false },
]);
});
});

Expand All@@ -55,7 +66,9 @@ describe('shouldBypassProxy', () => {
it('respects port-scoped rules', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('http://example.com:8080/path', rules)).toBe(true);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(false);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(
false,
);
});

it('matches implicit default ports', () => {
Expand All@@ -71,7 +84,9 @@ describe('shouldBypassProxy', () => {

// Explicit port that differs from rule should not match
const rules8080 = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(false);
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(
false,
);
});

it('wildcard bypasses everything', () => {
Expand All@@ -84,6 +99,27 @@ describe('shouldBypassProxy', () => {
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
});

it('respects IPv6 port-scoped rules', () => {
const rules = parseNoProxyRules('[::1]:3000');
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
expect(shouldBypassProxy('http://[::1]:3001/path', rules)).toBe(false);
});

it('does not bypass for malformed port suffixes', () => {
expect(
shouldBypassProxy(
'https://example.com/path',
parseNoProxyRules('example.com:443abc'),
),
).toBe(false);
expect(
shouldBypassProxy(
'http://[::1]:3000/path',
parseNoProxyRules('[::1]:abc'),
),
).toBe(false);
});

it('matches exact IP literal', () => {
const rules = parseNoProxyRules('192.168.1.1');
expect(shouldBypassProxy('http://192.168.1.1/', rules)).toBe(true);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ const requestContext = {

const getDefaultThinkingLevelMock = mock(() => 'think');
const setDefaultThinkingLevelMock = mock((_level: string) => true);
const setVoiceModelMock = mock((_model: string) => {});
let mockedWorkspace: Record<string, unknown> | null = null;
let mockedWorkspaceConfig: Record<string, unknown> | null = null;
const getWorkspaceByNameOrIdMock = mock(
Expand DownExpand Up@@ -64,9 +65,14 @@ mock.module('@craft-agent/shared/config', () => ({
getWorkspaceByNameOrId: getWorkspaceByNameOrIdMock,
getDefaultThinkingLevel: getDefaultThinkingLevelMock,
setDefaultThinkingLevel: setDefaultThinkingLevelMock,
setVoiceModel: setVoiceModelMock,
isProtectedWorkspace: () => false,
}));

mock.module('@craft-agent/shared/config/storage', () => ({
setVoiceModel: setVoiceModelMock,
}));

mock.module('@craft-agent/shared/workspaces', () => ({
loadWorkspaceConfig: loadWorkspaceConfigMock,
}));
Expand DownExpand Up@@ -94,6 +100,7 @@ describe('settings default thinking RPC handlers', () => {
handlers.clear();
getDefaultThinkingLevelMock.mockClear();
setDefaultThinkingLevelMock.mockClear();
setVoiceModelMock.mockClear();
mockedWorkspace = null;
mockedWorkspaceConfig = null;
getWorkspaceByNameOrIdMock.mockClear();
Expand DownExpand Up@@ -189,6 +196,17 @@ describe('settings default thinking RPC handlers', () => {
expect(setDefaultThinkingLevelMock).not.toHaveBeenCalled();
});

it('accepts dated voice model variants supported by the transport resolver', async () => {
const setHandler = handlers.get(RPC_CHANNELS.input.SET_VOICE_MODEL);
expect(setHandler).toBeTruthy();

await setHandler!(requestContext, 'qwen3-asr-flash-2025-06-01');

expect(setVoiceModelMock).toHaveBeenCalledWith(
'qwen3-asr-flash-2025-06-01',
);
});

it('returns global permission mode through Qwen ACP', async () => {
const getHandler = handlers.get(
RPC_CHANNELS.settings.GET_GLOBAL_PERMISSION_MODE,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' chore(sync): sync qwen-code desktop updates by DragonnZhang · Pull Request #74 · modelstudioai/openwork · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6885faa
fix(cli): Preserve mid-turn image messages (#5183)
DragonnZhang Jul 13, 2026
94fe082
fix(desktop): address git branch badge review (#5247)
DragonnZhang Jul 13, 2026
cf4a78d
feat(extensions): add i18n support for extension displayName and desc…
DragonnZhang Jul 13, 2026
09d73a3
feat(desktop): compile macOS 26+ Liquid Glass Assets.car in brand-cre…
DragonnZhang Jul 13, 2026
19e2ef5
fix: Expand Windows ~\\ home paths and hide phantom (session) entries…
DragonnZhang Jul 13, 2026
737d4ef
fix(desktop): detect WebP and AVI in RIFF magic-byte sniffing (#5336)
DragonnZhang Jul 13, 2026
22f410a
fix(desktop): accept uppercase icon URL schemes (#5470)
DragonnZhang Jul 13, 2026
b803618
fix: accept uppercase endpoint URL schemes (#5443)
DragonnZhang Jul 13, 2026
946447a
fix(desktop): preserve uppercase favicon URLs (#5463)
DragonnZhang Jul 13, 2026
70e468c
fix(desktop): parse NO_PROXY ports strictly (#5498)
DragonnZhang Jul 13, 2026
fc6ac77
test(desktop): update blocked scheme open-url assertion (#5529)
DragonnZhang Jul 13, 2026
78b6348
fix(desktop): restore locale parity (#5537)
DragonnZhang Jul 13, 2026
9da8d0c
fix(desktop): parse server ports strictly (#5509)
DragonnZhang Jul 13, 2026
1b23e7d
fix(desktop): validate generic oauth token responses (#5511)
DragonnZhang Jul 13, 2026
287f48d
fix(desktop): allow double dots in bundle filenames (#5515)
DragonnZhang Jul 13, 2026
11c032e
test(desktop): align interceptor packaging contract (#5531)
DragonnZhang Jul 13, 2026
4085fcc
fix(desktop): keep sibling paths absolute (#5517)
DragonnZhang Jul 13, 2026
ef039a2
test(desktop): enable feedback flag in permission tests (#5533)
DragonnZhang Jul 13, 2026
7dbff4b
fix(desktop): separate transform data output lines (#5525)
DragonnZhang Jul 13, 2026
0b94813
fix(desktop): handle Windows file mentions (#5523)
DragonnZhang Jul 13, 2026
f1f666f
fix(desktop): consolidate path boundary checks (#5545)
DragonnZhang Jul 13, 2026
e516910
fix(desktop): reject fractional transfer sizes (#5527)
DragonnZhang Jul 13, 2026
416c1db
feat(desktop): show file preview in a resizable side panel instead of…
DragonnZhang Jul 13, 2026
d241dab
feat(memory): confirm auto-generated skills before persisting (#5616)
DragonnZhang Jul 13, 2026
bee2b93
fix(desktop): reject unsafe source slugs before deletion (#5829)
DragonnZhang Jul 13, 2026
8047f89
fix(desktop): harden remaining source path validation (#5914)
DragonnZhang Jul 13, 2026
ef26ef0
feat(memory): add a git-shared team memory tier (#5886)
DragonnZhang Jul 13, 2026
dfff8da
fix(desktop): normalize source slug validation errors (#5911)
DragonnZhang Jul 13, 2026
d1b2864
feat(desktop): voice dictation in the desktop app (#5856)
DragonnZhang Jul 13, 2026
76fb3ad
fix(cli): switch TUI prefix ✦→◆ to fix glyph overflow on some termina…
DragonnZhang Jul 13, 2026
8089c9d
fix(desktop): enforce transform_data isolation (#6285)
DragonnZhang Jul 13, 2026
304128e
fix(desktop): preserve glued automation history records (#6344)
DragonnZhang Jul 13, 2026
71625bb
fix(desktop): preserve MCP URL query suffixes (#6587)
DragonnZhang Jul 13, 2026
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
114 changes: 106 additions & 8 deletions .agents/skills/desktop-brand-builder/scripts/brand-create.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ import {
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { extname, join, resolve } from 'node:path';

interface BrandInput {
Expand DownExpand Up@@ -130,10 +132,15 @@ async function run(cmd: string[], cwd: string): Promise<void> {
}
}

interface BrandAssetsResult {
macIcon: string;
hasAssetsCar: boolean;
}

async function writeBrandAssets(
config: BrandConfig,
desktopRoot: string,
): Promise<string> {
): Promise<BrandAssetsResult> {
const requireFromDesktop = createRequire(join(desktopRoot, 'package.json'));
const sharp = requireFromDesktop('sharp') as typeof import('sharp');
const electronDir = join(desktopRoot, 'apps', 'electron');
Expand All@@ -157,7 +164,7 @@ async function writeBrandAssets(
await writePng(join(brandDir, 'dock.png'), 512);
await writePng(join(brandDir, 'symbol.png'), 512);

if (process.platform !== 'darwin') return 'icon.png';
if (process.platform !== 'darwin') return { macIcon: 'icon.png', hasAssetsCar: false };

const iconset = join(brandDir, 'icon.iconset');
rmSync(iconset, { recursive: true, force: true });
Expand All@@ -184,7 +191,91 @@ async function writeBrandAssets(
['iconutil', '-c', 'icns', iconset, '-o', join(brandDir, 'icon.icns')],
brandDir,
);
return 'icon.icns';

const hasAssetsCar = await compileAssetsCar(config, brandDir, writePng);
return { macIcon: 'icon.icns', hasAssetsCar };
}

async function compileAssetsCar(
config: BrandConfig,
brandDir: string,
writePng: (output: string, size: number) => Promise<void>,
): Promise<boolean> {
const xcassets = join(brandDir, 'Assets.xcassets');
const appiconset = join(xcassets, 'AppIcon.appiconset');
rmSync(xcassets, { recursive: true, force: true });
mkdirSync(appiconset, { recursive: true });

writeFileSync(
join(xcassets, 'Contents.json'),
JSON.stringify({ info: { author: 'xcode', version: 1 } }),
);

const entries = [
{ file: 'icon_16.png', size: 16, scale: '1x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '2x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '1x', dims: '32x32' },
{ file: 'icon_64.png', size: 64, scale: '2x', dims: '32x32' },
{ file: 'icon_128.png', size: 128, scale: '1x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '2x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '1x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '2x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '1x', dims: '512x512' },
{ file: 'icon_1024.png', size: 1024, scale: '2x', dims: '512x512' },
];

const uniqueSizes = new Set(entries.map((e) => e.size));
for (const size of uniqueSizes) {
await writePng(join(appiconset, `icon_${size}.png`), size);
}

writeFileSync(
join(appiconset, 'Contents.json'),
JSON.stringify({
images: entries.map((e) => ({
filename: e.file,
idiom: 'mac',
scale: e.scale,
size: e.dims,
})),
info: { author: 'xcode', version: 1 },
}),
);

const outDir = mkdtempSync(join(tmpdir(), 'assets-car-'));
const partialPlist = join(outDir, 'partial-info.plist');
const proc = Bun.spawn({
cmd: [
'xcrun', 'actool', xcassets,
'--compile', outDir,
'--app-icon', 'AppIcon',
'--platform', 'macosx',
'--minimum-deployment-target', '14.0',
'--output-partial-info-plist', partialPlist,
],
cwd: brandDir,
stdout: 'pipe',
stderr: 'pipe',
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
console.log('Warning: actool compilation failed, skipping Assets.car');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

const compiledCar = join(outDir, 'Assets.car');
if (!existsSync(compiledCar)) {
console.log('Warning: actool produced no Assets.car, skipping');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

copyFileSync(compiledCar, join(brandDir, 'Assets.car'));
rmSync(xcassets, { recursive: true, force: true });
rmSync(outDir, { recursive: true, force: true });
console.log('Assets.car compiled successfully');
return true;
}

function tsString(value: string): string {
Expand All@@ -203,8 +294,11 @@ function helpMenuLinks(config: BrandConfig): string {
]`;
}

function brandBlock(config: BrandConfig, macIcon: string): string {
function brandBlock(config: BrandConfig, macIcon: string, hasAssetsCar: boolean): string {
const resourceDir = `resources/brands/${config.brandId}`;
const liquidGlassLine = hasAssetsCar
? `\n liquidGlassAssetsCar: ${tsString(`${resourceDir}/Assets.car`)},`
: '';

return ` ${tsString(config.brandId)}: {
id: ${tsString(config.brandId)},
Expand All@@ -223,7 +317,7 @@ function brandBlock(config: BrandConfig, macIcon: string): string {
macIcon: ${tsString(`${resourceDir}/${macIcon}`)},
winIcon: ${tsString(`${resourceDir}/icon.png`)},
linuxIcon: ${tsString(`${resourceDir}/icon.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},${liquidGlassLine}
},
credits: '',
creditsShort: '',
Expand All@@ -236,6 +330,7 @@ function registerBrand(
config: BrandConfig,
desktopRoot: string,
macIcon: string,
hasAssetsCar: boolean,
): void {
const brandingPath = join(
desktopRoot,
Expand All@@ -259,22 +354,25 @@ function registerBrand(

writeFileSync(
brandingPath,
source.replace(marker, `\n${brandBlock(config, macIcon)}${marker}`),
source.replace(marker, `\n${brandBlock(config, macIcon, hasAssetsCar)}${marker}`),
);
}

async function main(): Promise<void> {
const desktopRoot = desktopRootFromArgs();
const config = loadConfig(configPathFromArgs());
const macIcon = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon);
const { macIcon, hasAssetsCar } = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon, hasAssetsCar);

console.log(`Created brand ${config.brandId}`);
console.log(`App name: ${config.appName}`);
console.log(`App ID: ${config.appId}`);
console.log(
`Assets: ${join(desktopRoot, 'apps', 'electron', 'resources', 'brands', config.brandId)}`,
);
if (hasAssetsCar) {
console.log('Assets.car: generated (macOS 26+ Liquid Glass icon)');
}
}

main().catch((error: unknown) => {
Expand Down
3 changes: 3 additions & 0 deletions apps/electron/build/entitlements.mac.plist
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,5 +10,8 @@
<!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Voice dictation: microphone access under the hardened runtime. -->
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
2 changes: 2 additions & 0 deletions apps/electron/electron-builder.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,6 +72,8 @@ mac:
# The value must match --app-icon used in actool (see afterPack.js)
extendInfo:
CFBundleIconName: AppIcon
# Voice dictation: shown in the macOS microphone permission prompt.
NSMicrophoneUsageDescription: Qwen Code uses the microphone for voice dictation in the prompt composer.
target:
- target: dmg
arch:
Expand Down
42 changes: 39 additions & 3 deletions apps/electron/src/main/__tests__/network-proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,18 @@ describe('parseNoProxyRules', () => {

it('parses host:port', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(rules).toEqual([{ host: 'example.com', port: 8080, wildcard: false }]);
expect(rules).toEqual([
{ host: 'example.com', port: 8080, wildcard: false },
]);
});

it('does not parse malformed port suffixes', () => {
expect(parseNoProxyRules('example.com:443abc')).toEqual([
{ host: 'example.com:443abc', wildcard: false },
]);
expect(parseNoProxyRules('[::1]:abc')).toEqual([
{ host: '[::1]:abc', wildcard: false },
]);
});
});

Expand All@@ -55,7 +66,9 @@ describe('shouldBypassProxy', () => {
it('respects port-scoped rules', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('http://example.com:8080/path', rules)).toBe(true);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(false);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(
false,
);
});

it('matches implicit default ports', () => {
Expand All@@ -71,7 +84,9 @@ describe('shouldBypassProxy', () => {

// Explicit port that differs from rule should not match
const rules8080 = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(false);
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(
false,
);
});

it('wildcard bypasses everything', () => {
Expand All@@ -84,6 +99,27 @@ describe('shouldBypassProxy', () => {
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
});

it('respects IPv6 port-scoped rules', () => {
const rules = parseNoProxyRules('[::1]:3000');
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
expect(shouldBypassProxy('http://[::1]:3001/path', rules)).toBe(false);
});

it('does not bypass for malformed port suffixes', () => {
expect(
shouldBypassProxy(
'https://example.com/path',
parseNoProxyRules('example.com:443abc'),
),
).toBe(false);
expect(
shouldBypassProxy(
'http://[::1]:3000/path',
parseNoProxyRules('[::1]:abc'),
),
).toBe(false);
});

it('matches exact IP literal', () => {
const rules = parseNoProxyRules('192.168.1.1');
expect(shouldBypassProxy('http://192.168.1.1/', rules)).toBe(true);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ const requestContext = {

const getDefaultThinkingLevelMock = mock(() => 'think');
const setDefaultThinkingLevelMock = mock((_level: string) => true);
const setVoiceModelMock = mock((_model: string) => {});
let mockedWorkspace: Record<string, unknown> | null = null;
let mockedWorkspaceConfig: Record<string, unknown> | null = null;
const getWorkspaceByNameOrIdMock = mock(
Expand DownExpand Up@@ -64,9 +65,14 @@ mock.module('@craft-agent/shared/config', () => ({
getWorkspaceByNameOrId: getWorkspaceByNameOrIdMock,
getDefaultThinkingLevel: getDefaultThinkingLevelMock,
setDefaultThinkingLevel: setDefaultThinkingLevelMock,
setVoiceModel: setVoiceModelMock,
isProtectedWorkspace: () => false,
}));

mock.module('@craft-agent/shared/config/storage', () => ({
setVoiceModel: setVoiceModelMock,
}));

mock.module('@craft-agent/shared/workspaces', () => ({
loadWorkspaceConfig: loadWorkspaceConfigMock,
}));
Expand DownExpand Up@@ -94,6 +100,7 @@ describe('settings default thinking RPC handlers', () => {
handlers.clear();
getDefaultThinkingLevelMock.mockClear();
setDefaultThinkingLevelMock.mockClear();
setVoiceModelMock.mockClear();
mockedWorkspace = null;
mockedWorkspaceConfig = null;
getWorkspaceByNameOrIdMock.mockClear();
Expand DownExpand Up@@ -189,6 +196,17 @@ describe('settings default thinking RPC handlers', () => {
expect(setDefaultThinkingLevelMock).not.toHaveBeenCalled();
});

it('accepts dated voice model variants supported by the transport resolver', async () => {
const setHandler = handlers.get(RPC_CHANNELS.input.SET_VOICE_MODEL);
expect(setHandler).toBeTruthy();

await setHandler!(requestContext, 'qwen3-asr-flash-2025-06-01');

expect(setVoiceModelMock).toHaveBeenCalledWith(
'qwen3-asr-flash-2025-06-01',
);
});

it('returns global permission mode through Qwen ACP', async () => {
const getHandler = handlers.get(
RPC_CHANNELS.settings.GET_GLOBAL_PERMISSION_MODE,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' chore(sync): sync qwen-code desktop updates by DragonnZhang · Pull Request #74 · modelstudioai/openwork · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6885faa
fix(cli): Preserve mid-turn image messages (#5183)
DragonnZhang Jul 13, 2026
94fe082
fix(desktop): address git branch badge review (#5247)
DragonnZhang Jul 13, 2026
cf4a78d
feat(extensions): add i18n support for extension displayName and desc…
DragonnZhang Jul 13, 2026
09d73a3
feat(desktop): compile macOS 26+ Liquid Glass Assets.car in brand-cre…
DragonnZhang Jul 13, 2026
19e2ef5
fix: Expand Windows ~\\ home paths and hide phantom (session) entries…
DragonnZhang Jul 13, 2026
737d4ef
fix(desktop): detect WebP and AVI in RIFF magic-byte sniffing (#5336)
DragonnZhang Jul 13, 2026
22f410a
fix(desktop): accept uppercase icon URL schemes (#5470)
DragonnZhang Jul 13, 2026
b803618
fix: accept uppercase endpoint URL schemes (#5443)
DragonnZhang Jul 13, 2026
946447a
fix(desktop): preserve uppercase favicon URLs (#5463)
DragonnZhang Jul 13, 2026
70e468c
fix(desktop): parse NO_PROXY ports strictly (#5498)
DragonnZhang Jul 13, 2026
fc6ac77
test(desktop): update blocked scheme open-url assertion (#5529)
DragonnZhang Jul 13, 2026
78b6348
fix(desktop): restore locale parity (#5537)
DragonnZhang Jul 13, 2026
9da8d0c
fix(desktop): parse server ports strictly (#5509)
DragonnZhang Jul 13, 2026
1b23e7d
fix(desktop): validate generic oauth token responses (#5511)
DragonnZhang Jul 13, 2026
287f48d
fix(desktop): allow double dots in bundle filenames (#5515)
DragonnZhang Jul 13, 2026
11c032e
test(desktop): align interceptor packaging contract (#5531)
DragonnZhang Jul 13, 2026
4085fcc
fix(desktop): keep sibling paths absolute (#5517)
DragonnZhang Jul 13, 2026
ef039a2
test(desktop): enable feedback flag in permission tests (#5533)
DragonnZhang Jul 13, 2026
7dbff4b
fix(desktop): separate transform data output lines (#5525)
DragonnZhang Jul 13, 2026
0b94813
fix(desktop): handle Windows file mentions (#5523)
DragonnZhang Jul 13, 2026
f1f666f
fix(desktop): consolidate path boundary checks (#5545)
DragonnZhang Jul 13, 2026
e516910
fix(desktop): reject fractional transfer sizes (#5527)
DragonnZhang Jul 13, 2026
416c1db
feat(desktop): show file preview in a resizable side panel instead of…
DragonnZhang Jul 13, 2026
d241dab
feat(memory): confirm auto-generated skills before persisting (#5616)
DragonnZhang Jul 13, 2026
bee2b93
fix(desktop): reject unsafe source slugs before deletion (#5829)
DragonnZhang Jul 13, 2026
8047f89
fix(desktop): harden remaining source path validation (#5914)
DragonnZhang Jul 13, 2026
ef26ef0
feat(memory): add a git-shared team memory tier (#5886)
DragonnZhang Jul 13, 2026
dfff8da
fix(desktop): normalize source slug validation errors (#5911)
DragonnZhang Jul 13, 2026
d1b2864
feat(desktop): voice dictation in the desktop app (#5856)
DragonnZhang Jul 13, 2026
76fb3ad
fix(cli): switch TUI prefix ✦→◆ to fix glyph overflow on some termina…
DragonnZhang Jul 13, 2026
8089c9d
fix(desktop): enforce transform_data isolation (#6285)
DragonnZhang Jul 13, 2026
304128e
fix(desktop): preserve glued automation history records (#6344)
DragonnZhang Jul 13, 2026
71625bb
fix(desktop): preserve MCP URL query suffixes (#6587)
DragonnZhang Jul 13, 2026
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
114 changes: 106 additions & 8 deletions .agents/skills/desktop-brand-builder/scripts/brand-create.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ import {
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { extname, join, resolve } from 'node:path';

interface BrandInput {
Expand DownExpand Up@@ -130,10 +132,15 @@ async function run(cmd: string[], cwd: string): Promise<void> {
}
}

interface BrandAssetsResult {
macIcon: string;
hasAssetsCar: boolean;
}

async function writeBrandAssets(
config: BrandConfig,
desktopRoot: string,
): Promise<string> {
): Promise<BrandAssetsResult> {
const requireFromDesktop = createRequire(join(desktopRoot, 'package.json'));
const sharp = requireFromDesktop('sharp') as typeof import('sharp');
const electronDir = join(desktopRoot, 'apps', 'electron');
Expand All@@ -157,7 +164,7 @@ async function writeBrandAssets(
await writePng(join(brandDir, 'dock.png'), 512);
await writePng(join(brandDir, 'symbol.png'), 512);

if (process.platform !== 'darwin') return 'icon.png';
if (process.platform !== 'darwin') return { macIcon: 'icon.png', hasAssetsCar: false };

const iconset = join(brandDir, 'icon.iconset');
rmSync(iconset, { recursive: true, force: true });
Expand All@@ -184,7 +191,91 @@ async function writeBrandAssets(
['iconutil', '-c', 'icns', iconset, '-o', join(brandDir, 'icon.icns')],
brandDir,
);
return 'icon.icns';

const hasAssetsCar = await compileAssetsCar(config, brandDir, writePng);
return { macIcon: 'icon.icns', hasAssetsCar };
}

async function compileAssetsCar(
config: BrandConfig,
brandDir: string,
writePng: (output: string, size: number) => Promise<void>,
): Promise<boolean> {
const xcassets = join(brandDir, 'Assets.xcassets');
const appiconset = join(xcassets, 'AppIcon.appiconset');
rmSync(xcassets, { recursive: true, force: true });
mkdirSync(appiconset, { recursive: true });

writeFileSync(
join(xcassets, 'Contents.json'),
JSON.stringify({ info: { author: 'xcode', version: 1 } }),
);

const entries = [
{ file: 'icon_16.png', size: 16, scale: '1x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '2x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '1x', dims: '32x32' },
{ file: 'icon_64.png', size: 64, scale: '2x', dims: '32x32' },
{ file: 'icon_128.png', size: 128, scale: '1x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '2x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '1x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '2x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '1x', dims: '512x512' },
{ file: 'icon_1024.png', size: 1024, scale: '2x', dims: '512x512' },
];

const uniqueSizes = new Set(entries.map((e) => e.size));
for (const size of uniqueSizes) {
await writePng(join(appiconset, `icon_${size}.png`), size);
}

writeFileSync(
join(appiconset, 'Contents.json'),
JSON.stringify({
images: entries.map((e) => ({
filename: e.file,
idiom: 'mac',
scale: e.scale,
size: e.dims,
})),
info: { author: 'xcode', version: 1 },
}),
);

const outDir = mkdtempSync(join(tmpdir(), 'assets-car-'));
const partialPlist = join(outDir, 'partial-info.plist');
const proc = Bun.spawn({
cmd: [
'xcrun', 'actool', xcassets,
'--compile', outDir,
'--app-icon', 'AppIcon',
'--platform', 'macosx',
'--minimum-deployment-target', '14.0',
'--output-partial-info-plist', partialPlist,
],
cwd: brandDir,
stdout: 'pipe',
stderr: 'pipe',
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
console.log('Warning: actool compilation failed, skipping Assets.car');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

const compiledCar = join(outDir, 'Assets.car');
if (!existsSync(compiledCar)) {
console.log('Warning: actool produced no Assets.car, skipping');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

copyFileSync(compiledCar, join(brandDir, 'Assets.car'));
rmSync(xcassets, { recursive: true, force: true });
rmSync(outDir, { recursive: true, force: true });
console.log('Assets.car compiled successfully');
return true;
}

function tsString(value: string): string {
Expand All@@ -203,8 +294,11 @@ function helpMenuLinks(config: BrandConfig): string {
]`;
}

function brandBlock(config: BrandConfig, macIcon: string): string {
function brandBlock(config: BrandConfig, macIcon: string, hasAssetsCar: boolean): string {
const resourceDir = `resources/brands/${config.brandId}`;
const liquidGlassLine = hasAssetsCar
? `\n liquidGlassAssetsCar: ${tsString(`${resourceDir}/Assets.car`)},`
: '';

return ` ${tsString(config.brandId)}: {
id: ${tsString(config.brandId)},
Expand All@@ -223,7 +317,7 @@ function brandBlock(config: BrandConfig, macIcon: string): string {
macIcon: ${tsString(`${resourceDir}/${macIcon}`)},
winIcon: ${tsString(`${resourceDir}/icon.png`)},
linuxIcon: ${tsString(`${resourceDir}/icon.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},${liquidGlassLine}
},
credits: '',
creditsShort: '',
Expand All@@ -236,6 +330,7 @@ function registerBrand(
config: BrandConfig,
desktopRoot: string,
macIcon: string,
hasAssetsCar: boolean,
): void {
const brandingPath = join(
desktopRoot,
Expand All@@ -259,22 +354,25 @@ function registerBrand(

writeFileSync(
brandingPath,
source.replace(marker, `\n${brandBlock(config, macIcon)}${marker}`),
source.replace(marker, `\n${brandBlock(config, macIcon, hasAssetsCar)}${marker}`),
);
}

async function main(): Promise<void> {
const desktopRoot = desktopRootFromArgs();
const config = loadConfig(configPathFromArgs());
const macIcon = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon);
const { macIcon, hasAssetsCar } = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon, hasAssetsCar);

console.log(`Created brand ${config.brandId}`);
console.log(`App name: ${config.appName}`);
console.log(`App ID: ${config.appId}`);
console.log(
`Assets: ${join(desktopRoot, 'apps', 'electron', 'resources', 'brands', config.brandId)}`,
);
if (hasAssetsCar) {
console.log('Assets.car: generated (macOS 26+ Liquid Glass icon)');
}
}

main().catch((error: unknown) => {
Expand Down
3 changes: 3 additions & 0 deletions apps/electron/build/entitlements.mac.plist
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,5 +10,8 @@
<!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Voice dictation: microphone access under the hardened runtime. -->
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
2 changes: 2 additions & 0 deletions apps/electron/electron-builder.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,6 +72,8 @@ mac:
# The value must match --app-icon used in actool (see afterPack.js)
extendInfo:
CFBundleIconName: AppIcon
# Voice dictation: shown in the macOS microphone permission prompt.
NSMicrophoneUsageDescription: Qwen Code uses the microphone for voice dictation in the prompt composer.
target:
- target: dmg
arch:
Expand Down
42 changes: 39 additions & 3 deletions apps/electron/src/main/__tests__/network-proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,18 @@ describe('parseNoProxyRules', () => {

it('parses host:port', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(rules).toEqual([{ host: 'example.com', port: 8080, wildcard: false }]);
expect(rules).toEqual([
{ host: 'example.com', port: 8080, wildcard: false },
]);
});

it('does not parse malformed port suffixes', () => {
expect(parseNoProxyRules('example.com:443abc')).toEqual([
{ host: 'example.com:443abc', wildcard: false },
]);
expect(parseNoProxyRules('[::1]:abc')).toEqual([
{ host: '[::1]:abc', wildcard: false },
]);
});
});

Expand All@@ -55,7 +66,9 @@ describe('shouldBypassProxy', () => {
it('respects port-scoped rules', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('http://example.com:8080/path', rules)).toBe(true);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(false);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(
false,
);
});

it('matches implicit default ports', () => {
Expand All@@ -71,7 +84,9 @@ describe('shouldBypassProxy', () => {

// Explicit port that differs from rule should not match
const rules8080 = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(false);
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(
false,
);
});

it('wildcard bypasses everything', () => {
Expand All@@ -84,6 +99,27 @@ describe('shouldBypassProxy', () => {
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
});

it('respects IPv6 port-scoped rules', () => {
const rules = parseNoProxyRules('[::1]:3000');
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
expect(shouldBypassProxy('http://[::1]:3001/path', rules)).toBe(false);
});

it('does not bypass for malformed port suffixes', () => {
expect(
shouldBypassProxy(
'https://example.com/path',
parseNoProxyRules('example.com:443abc'),
),
).toBe(false);
expect(
shouldBypassProxy(
'http://[::1]:3000/path',
parseNoProxyRules('[::1]:abc'),
),
).toBe(false);
});

it('matches exact IP literal', () => {
const rules = parseNoProxyRules('192.168.1.1');
expect(shouldBypassProxy('http://192.168.1.1/', rules)).toBe(true);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ const requestContext = {

const getDefaultThinkingLevelMock = mock(() => 'think');
const setDefaultThinkingLevelMock = mock((_level: string) => true);
const setVoiceModelMock = mock((_model: string) => {});
let mockedWorkspace: Record<string, unknown> | null = null;
let mockedWorkspaceConfig: Record<string, unknown> | null = null;
const getWorkspaceByNameOrIdMock = mock(
Expand DownExpand Up@@ -64,9 +65,14 @@ mock.module('@craft-agent/shared/config', () => ({
getWorkspaceByNameOrId: getWorkspaceByNameOrIdMock,
getDefaultThinkingLevel: getDefaultThinkingLevelMock,
setDefaultThinkingLevel: setDefaultThinkingLevelMock,
setVoiceModel: setVoiceModelMock,
isProtectedWorkspace: () => false,
}));

mock.module('@craft-agent/shared/config/storage', () => ({
setVoiceModel: setVoiceModelMock,
}));

mock.module('@craft-agent/shared/workspaces', () => ({
loadWorkspaceConfig: loadWorkspaceConfigMock,
}));
Expand DownExpand Up@@ -94,6 +100,7 @@ describe('settings default thinking RPC handlers', () => {
handlers.clear();
getDefaultThinkingLevelMock.mockClear();
setDefaultThinkingLevelMock.mockClear();
setVoiceModelMock.mockClear();
mockedWorkspace = null;
mockedWorkspaceConfig = null;
getWorkspaceByNameOrIdMock.mockClear();
Expand DownExpand Up@@ -189,6 +196,17 @@ describe('settings default thinking RPC handlers', () => {
expect(setDefaultThinkingLevelMock).not.toHaveBeenCalled();
});

it('accepts dated voice model variants supported by the transport resolver', async () => {
const setHandler = handlers.get(RPC_CHANNELS.input.SET_VOICE_MODEL);
expect(setHandler).toBeTruthy();

await setHandler!(requestContext, 'qwen3-asr-flash-2025-06-01');

expect(setVoiceModelMock).toHaveBeenCalledWith(
'qwen3-asr-flash-2025-06-01',
);
});

it('returns global permission mode through Qwen ACP', async () => {
const getHandler = handlers.get(
RPC_CHANNELS.settings.GET_GLOBAL_PERMISSION_MODE,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' chore(sync): sync qwen-code desktop updates by DragonnZhang · Pull Request #74 · modelstudioai/openwork · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6885faa
fix(cli): Preserve mid-turn image messages (#5183)
DragonnZhang Jul 13, 2026
94fe082
fix(desktop): address git branch badge review (#5247)
DragonnZhang Jul 13, 2026
cf4a78d
feat(extensions): add i18n support for extension displayName and desc…
DragonnZhang Jul 13, 2026
09d73a3
feat(desktop): compile macOS 26+ Liquid Glass Assets.car in brand-cre…
DragonnZhang Jul 13, 2026
19e2ef5
fix: Expand Windows ~\\ home paths and hide phantom (session) entries…
DragonnZhang Jul 13, 2026
737d4ef
fix(desktop): detect WebP and AVI in RIFF magic-byte sniffing (#5336)
DragonnZhang Jul 13, 2026
22f410a
fix(desktop): accept uppercase icon URL schemes (#5470)
DragonnZhang Jul 13, 2026
b803618
fix: accept uppercase endpoint URL schemes (#5443)
DragonnZhang Jul 13, 2026
946447a
fix(desktop): preserve uppercase favicon URLs (#5463)
DragonnZhang Jul 13, 2026
70e468c
fix(desktop): parse NO_PROXY ports strictly (#5498)
DragonnZhang Jul 13, 2026
fc6ac77
test(desktop): update blocked scheme open-url assertion (#5529)
DragonnZhang Jul 13, 2026
78b6348
fix(desktop): restore locale parity (#5537)
DragonnZhang Jul 13, 2026
9da8d0c
fix(desktop): parse server ports strictly (#5509)
DragonnZhang Jul 13, 2026
1b23e7d
fix(desktop): validate generic oauth token responses (#5511)
DragonnZhang Jul 13, 2026
287f48d
fix(desktop): allow double dots in bundle filenames (#5515)
DragonnZhang Jul 13, 2026
11c032e
test(desktop): align interceptor packaging contract (#5531)
DragonnZhang Jul 13, 2026
4085fcc
fix(desktop): keep sibling paths absolute (#5517)
DragonnZhang Jul 13, 2026
ef039a2
test(desktop): enable feedback flag in permission tests (#5533)
DragonnZhang Jul 13, 2026
7dbff4b
fix(desktop): separate transform data output lines (#5525)
DragonnZhang Jul 13, 2026
0b94813
fix(desktop): handle Windows file mentions (#5523)
DragonnZhang Jul 13, 2026
f1f666f
fix(desktop): consolidate path boundary checks (#5545)
DragonnZhang Jul 13, 2026
e516910
fix(desktop): reject fractional transfer sizes (#5527)
DragonnZhang Jul 13, 2026
416c1db
feat(desktop): show file preview in a resizable side panel instead of…
DragonnZhang Jul 13, 2026
d241dab
feat(memory): confirm auto-generated skills before persisting (#5616)
DragonnZhang Jul 13, 2026
bee2b93
fix(desktop): reject unsafe source slugs before deletion (#5829)
DragonnZhang Jul 13, 2026
8047f89
fix(desktop): harden remaining source path validation (#5914)
DragonnZhang Jul 13, 2026
ef26ef0
feat(memory): add a git-shared team memory tier (#5886)
DragonnZhang Jul 13, 2026
dfff8da
fix(desktop): normalize source slug validation errors (#5911)
DragonnZhang Jul 13, 2026
d1b2864
feat(desktop): voice dictation in the desktop app (#5856)
DragonnZhang Jul 13, 2026
76fb3ad
fix(cli): switch TUI prefix ✦→◆ to fix glyph overflow on some termina…
DragonnZhang Jul 13, 2026
8089c9d
fix(desktop): enforce transform_data isolation (#6285)
DragonnZhang Jul 13, 2026
304128e
fix(desktop): preserve glued automation history records (#6344)
DragonnZhang Jul 13, 2026
71625bb
fix(desktop): preserve MCP URL query suffixes (#6587)
DragonnZhang Jul 13, 2026
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
114 changes: 106 additions & 8 deletions .agents/skills/desktop-brand-builder/scripts/brand-create.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ import {
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { extname, join, resolve } from 'node:path';

interface BrandInput {
Expand DownExpand Up@@ -130,10 +132,15 @@ async function run(cmd: string[], cwd: string): Promise<void> {
}
}

interface BrandAssetsResult {
macIcon: string;
hasAssetsCar: boolean;
}

async function writeBrandAssets(
config: BrandConfig,
desktopRoot: string,
): Promise<string> {
): Promise<BrandAssetsResult> {
const requireFromDesktop = createRequire(join(desktopRoot, 'package.json'));
const sharp = requireFromDesktop('sharp') as typeof import('sharp');
const electronDir = join(desktopRoot, 'apps', 'electron');
Expand All@@ -157,7 +164,7 @@ async function writeBrandAssets(
await writePng(join(brandDir, 'dock.png'), 512);
await writePng(join(brandDir, 'symbol.png'), 512);

if (process.platform !== 'darwin') return 'icon.png';
if (process.platform !== 'darwin') return { macIcon: 'icon.png', hasAssetsCar: false };

const iconset = join(brandDir, 'icon.iconset');
rmSync(iconset, { recursive: true, force: true });
Expand All@@ -184,7 +191,91 @@ async function writeBrandAssets(
['iconutil', '-c', 'icns', iconset, '-o', join(brandDir, 'icon.icns')],
brandDir,
);
return 'icon.icns';

const hasAssetsCar = await compileAssetsCar(config, brandDir, writePng);
return { macIcon: 'icon.icns', hasAssetsCar };
}

async function compileAssetsCar(
config: BrandConfig,
brandDir: string,
writePng: (output: string, size: number) => Promise<void>,
): Promise<boolean> {
const xcassets = join(brandDir, 'Assets.xcassets');
const appiconset = join(xcassets, 'AppIcon.appiconset');
rmSync(xcassets, { recursive: true, force: true });
mkdirSync(appiconset, { recursive: true });

writeFileSync(
join(xcassets, 'Contents.json'),
JSON.stringify({ info: { author: 'xcode', version: 1 } }),
);

const entries = [
{ file: 'icon_16.png', size: 16, scale: '1x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '2x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '1x', dims: '32x32' },
{ file: 'icon_64.png', size: 64, scale: '2x', dims: '32x32' },
{ file: 'icon_128.png', size: 128, scale: '1x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '2x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '1x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '2x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '1x', dims: '512x512' },
{ file: 'icon_1024.png', size: 1024, scale: '2x', dims: '512x512' },
];

const uniqueSizes = new Set(entries.map((e) => e.size));
for (const size of uniqueSizes) {
await writePng(join(appiconset, `icon_${size}.png`), size);
}

writeFileSync(
join(appiconset, 'Contents.json'),
JSON.stringify({
images: entries.map((e) => ({
filename: e.file,
idiom: 'mac',
scale: e.scale,
size: e.dims,
})),
info: { author: 'xcode', version: 1 },
}),
);

const outDir = mkdtempSync(join(tmpdir(), 'assets-car-'));
const partialPlist = join(outDir, 'partial-info.plist');
const proc = Bun.spawn({
cmd: [
'xcrun', 'actool', xcassets,
'--compile', outDir,
'--app-icon', 'AppIcon',
'--platform', 'macosx',
'--minimum-deployment-target', '14.0',
'--output-partial-info-plist', partialPlist,
],
cwd: brandDir,
stdout: 'pipe',
stderr: 'pipe',
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
console.log('Warning: actool compilation failed, skipping Assets.car');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

const compiledCar = join(outDir, 'Assets.car');
if (!existsSync(compiledCar)) {
console.log('Warning: actool produced no Assets.car, skipping');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

copyFileSync(compiledCar, join(brandDir, 'Assets.car'));
rmSync(xcassets, { recursive: true, force: true });
rmSync(outDir, { recursive: true, force: true });
console.log('Assets.car compiled successfully');
return true;
}

function tsString(value: string): string {
Expand All@@ -203,8 +294,11 @@ function helpMenuLinks(config: BrandConfig): string {
]`;
}

function brandBlock(config: BrandConfig, macIcon: string): string {
function brandBlock(config: BrandConfig, macIcon: string, hasAssetsCar: boolean): string {
const resourceDir = `resources/brands/${config.brandId}`;
const liquidGlassLine = hasAssetsCar
? `\n liquidGlassAssetsCar: ${tsString(`${resourceDir}/Assets.car`)},`
: '';

return ` ${tsString(config.brandId)}: {
id: ${tsString(config.brandId)},
Expand All@@ -223,7 +317,7 @@ function brandBlock(config: BrandConfig, macIcon: string): string {
macIcon: ${tsString(`${resourceDir}/${macIcon}`)},
winIcon: ${tsString(`${resourceDir}/icon.png`)},
linuxIcon: ${tsString(`${resourceDir}/icon.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},${liquidGlassLine}
},
credits: '',
creditsShort: '',
Expand All@@ -236,6 +330,7 @@ function registerBrand(
config: BrandConfig,
desktopRoot: string,
macIcon: string,
hasAssetsCar: boolean,
): void {
const brandingPath = join(
desktopRoot,
Expand All@@ -259,22 +354,25 @@ function registerBrand(

writeFileSync(
brandingPath,
source.replace(marker, `\n${brandBlock(config, macIcon)}${marker}`),
source.replace(marker, `\n${brandBlock(config, macIcon, hasAssetsCar)}${marker}`),
);
}

async function main(): Promise<void> {
const desktopRoot = desktopRootFromArgs();
const config = loadConfig(configPathFromArgs());
const macIcon = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon);
const { macIcon, hasAssetsCar } = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon, hasAssetsCar);

console.log(`Created brand ${config.brandId}`);
console.log(`App name: ${config.appName}`);
console.log(`App ID: ${config.appId}`);
console.log(
`Assets: ${join(desktopRoot, 'apps', 'electron', 'resources', 'brands', config.brandId)}`,
);
if (hasAssetsCar) {
console.log('Assets.car: generated (macOS 26+ Liquid Glass icon)');
}
}

main().catch((error: unknown) => {
Expand Down
3 changes: 3 additions & 0 deletions apps/electron/build/entitlements.mac.plist
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,5 +10,8 @@
<!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Voice dictation: microphone access under the hardened runtime. -->
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
2 changes: 2 additions & 0 deletions apps/electron/electron-builder.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,6 +72,8 @@ mac:
# The value must match --app-icon used in actool (see afterPack.js)
extendInfo:
CFBundleIconName: AppIcon
# Voice dictation: shown in the macOS microphone permission prompt.
NSMicrophoneUsageDescription: Qwen Code uses the microphone for voice dictation in the prompt composer.
target:
- target: dmg
arch:
Expand Down
42 changes: 39 additions & 3 deletions apps/electron/src/main/__tests__/network-proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,18 @@ describe('parseNoProxyRules', () => {

it('parses host:port', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(rules).toEqual([{ host: 'example.com', port: 8080, wildcard: false }]);
expect(rules).toEqual([
{ host: 'example.com', port: 8080, wildcard: false },
]);
});

it('does not parse malformed port suffixes', () => {
expect(parseNoProxyRules('example.com:443abc')).toEqual([
{ host: 'example.com:443abc', wildcard: false },
]);
expect(parseNoProxyRules('[::1]:abc')).toEqual([
{ host: '[::1]:abc', wildcard: false },
]);
});
});

Expand All@@ -55,7 +66,9 @@ describe('shouldBypassProxy', () => {
it('respects port-scoped rules', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('http://example.com:8080/path', rules)).toBe(true);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(false);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(
false,
);
});

it('matches implicit default ports', () => {
Expand All@@ -71,7 +84,9 @@ describe('shouldBypassProxy', () => {

// Explicit port that differs from rule should not match
const rules8080 = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(false);
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(
false,
);
});

it('wildcard bypasses everything', () => {
Expand All@@ -84,6 +99,27 @@ describe('shouldBypassProxy', () => {
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
});

it('respects IPv6 port-scoped rules', () => {
const rules = parseNoProxyRules('[::1]:3000');
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
expect(shouldBypassProxy('http://[::1]:3001/path', rules)).toBe(false);
});

it('does not bypass for malformed port suffixes', () => {
expect(
shouldBypassProxy(
'https://example.com/path',
parseNoProxyRules('example.com:443abc'),
),
).toBe(false);
expect(
shouldBypassProxy(
'http://[::1]:3000/path',
parseNoProxyRules('[::1]:abc'),
),
).toBe(false);
});

it('matches exact IP literal', () => {
const rules = parseNoProxyRules('192.168.1.1');
expect(shouldBypassProxy('http://192.168.1.1/', rules)).toBe(true);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ const requestContext = {

const getDefaultThinkingLevelMock = mock(() => 'think');
const setDefaultThinkingLevelMock = mock((_level: string) => true);
const setVoiceModelMock = mock((_model: string) => {});
let mockedWorkspace: Record<string, unknown> | null = null;
let mockedWorkspaceConfig: Record<string, unknown> | null = null;
const getWorkspaceByNameOrIdMock = mock(
Expand DownExpand Up@@ -64,9 +65,14 @@ mock.module('@craft-agent/shared/config', () => ({
getWorkspaceByNameOrId: getWorkspaceByNameOrIdMock,
getDefaultThinkingLevel: getDefaultThinkingLevelMock,
setDefaultThinkingLevel: setDefaultThinkingLevelMock,
setVoiceModel: setVoiceModelMock,
isProtectedWorkspace: () => false,
}));

mock.module('@craft-agent/shared/config/storage', () => ({
setVoiceModel: setVoiceModelMock,
}));

mock.module('@craft-agent/shared/workspaces', () => ({
loadWorkspaceConfig: loadWorkspaceConfigMock,
}));
Expand DownExpand Up@@ -94,6 +100,7 @@ describe('settings default thinking RPC handlers', () => {
handlers.clear();
getDefaultThinkingLevelMock.mockClear();
setDefaultThinkingLevelMock.mockClear();
setVoiceModelMock.mockClear();
mockedWorkspace = null;
mockedWorkspaceConfig = null;
getWorkspaceByNameOrIdMock.mockClear();
Expand DownExpand Up@@ -189,6 +196,17 @@ describe('settings default thinking RPC handlers', () => {
expect(setDefaultThinkingLevelMock).not.toHaveBeenCalled();
});

it('accepts dated voice model variants supported by the transport resolver', async () => {
const setHandler = handlers.get(RPC_CHANNELS.input.SET_VOICE_MODEL);
expect(setHandler).toBeTruthy();

await setHandler!(requestContext, 'qwen3-asr-flash-2025-06-01');

expect(setVoiceModelMock).toHaveBeenCalledWith(
'qwen3-asr-flash-2025-06-01',
);
});

it('returns global permission mode through Qwen ACP', async () => {
const getHandler = handlers.get(
RPC_CHANNELS.settings.GET_GLOBAL_PERMISSION_MODE,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' chore(sync): sync qwen-code desktop updates by DragonnZhang · Pull Request #74 · modelstudioai/openwork · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6885faa
fix(cli): Preserve mid-turn image messages (#5183)
DragonnZhang Jul 13, 2026
94fe082
fix(desktop): address git branch badge review (#5247)
DragonnZhang Jul 13, 2026
cf4a78d
feat(extensions): add i18n support for extension displayName and desc…
DragonnZhang Jul 13, 2026
09d73a3
feat(desktop): compile macOS 26+ Liquid Glass Assets.car in brand-cre…
DragonnZhang Jul 13, 2026
19e2ef5
fix: Expand Windows ~\\ home paths and hide phantom (session) entries…
DragonnZhang Jul 13, 2026
737d4ef
fix(desktop): detect WebP and AVI in RIFF magic-byte sniffing (#5336)
DragonnZhang Jul 13, 2026
22f410a
fix(desktop): accept uppercase icon URL schemes (#5470)
DragonnZhang Jul 13, 2026
b803618
fix: accept uppercase endpoint URL schemes (#5443)
DragonnZhang Jul 13, 2026
946447a
fix(desktop): preserve uppercase favicon URLs (#5463)
DragonnZhang Jul 13, 2026
70e468c
fix(desktop): parse NO_PROXY ports strictly (#5498)
DragonnZhang Jul 13, 2026
fc6ac77
test(desktop): update blocked scheme open-url assertion (#5529)
DragonnZhang Jul 13, 2026
78b6348
fix(desktop): restore locale parity (#5537)
DragonnZhang Jul 13, 2026
9da8d0c
fix(desktop): parse server ports strictly (#5509)
DragonnZhang Jul 13, 2026
1b23e7d
fix(desktop): validate generic oauth token responses (#5511)
DragonnZhang Jul 13, 2026
287f48d
fix(desktop): allow double dots in bundle filenames (#5515)
DragonnZhang Jul 13, 2026
11c032e
test(desktop): align interceptor packaging contract (#5531)
DragonnZhang Jul 13, 2026
4085fcc
fix(desktop): keep sibling paths absolute (#5517)
DragonnZhang Jul 13, 2026
ef039a2
test(desktop): enable feedback flag in permission tests (#5533)
DragonnZhang Jul 13, 2026
7dbff4b
fix(desktop): separate transform data output lines (#5525)
DragonnZhang Jul 13, 2026
0b94813
fix(desktop): handle Windows file mentions (#5523)
DragonnZhang Jul 13, 2026
f1f666f
fix(desktop): consolidate path boundary checks (#5545)
DragonnZhang Jul 13, 2026
e516910
fix(desktop): reject fractional transfer sizes (#5527)
DragonnZhang Jul 13, 2026
416c1db
feat(desktop): show file preview in a resizable side panel instead of…
DragonnZhang Jul 13, 2026
d241dab
feat(memory): confirm auto-generated skills before persisting (#5616)
DragonnZhang Jul 13, 2026
bee2b93
fix(desktop): reject unsafe source slugs before deletion (#5829)
DragonnZhang Jul 13, 2026
8047f89
fix(desktop): harden remaining source path validation (#5914)
DragonnZhang Jul 13, 2026
ef26ef0
feat(memory): add a git-shared team memory tier (#5886)
DragonnZhang Jul 13, 2026
dfff8da
fix(desktop): normalize source slug validation errors (#5911)
DragonnZhang Jul 13, 2026
d1b2864
feat(desktop): voice dictation in the desktop app (#5856)
DragonnZhang Jul 13, 2026
76fb3ad
fix(cli): switch TUI prefix ✦→◆ to fix glyph overflow on some termina…
DragonnZhang Jul 13, 2026
8089c9d
fix(desktop): enforce transform_data isolation (#6285)
DragonnZhang Jul 13, 2026
304128e
fix(desktop): preserve glued automation history records (#6344)
DragonnZhang Jul 13, 2026
71625bb
fix(desktop): preserve MCP URL query suffixes (#6587)
DragonnZhang Jul 13, 2026
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
114 changes: 106 additions & 8 deletions .agents/skills/desktop-brand-builder/scripts/brand-create.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ import {
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { extname, join, resolve } from 'node:path';

interface BrandInput {
Expand DownExpand Up@@ -130,10 +132,15 @@ async function run(cmd: string[], cwd: string): Promise<void> {
}
}

interface BrandAssetsResult {
macIcon: string;
hasAssetsCar: boolean;
}

async function writeBrandAssets(
config: BrandConfig,
desktopRoot: string,
): Promise<string> {
): Promise<BrandAssetsResult> {
const requireFromDesktop = createRequire(join(desktopRoot, 'package.json'));
const sharp = requireFromDesktop('sharp') as typeof import('sharp');
const electronDir = join(desktopRoot, 'apps', 'electron');
Expand All@@ -157,7 +164,7 @@ async function writeBrandAssets(
await writePng(join(brandDir, 'dock.png'), 512);
await writePng(join(brandDir, 'symbol.png'), 512);

if (process.platform !== 'darwin') return 'icon.png';
if (process.platform !== 'darwin') return { macIcon: 'icon.png', hasAssetsCar: false };

const iconset = join(brandDir, 'icon.iconset');
rmSync(iconset, { recursive: true, force: true });
Expand All@@ -184,7 +191,91 @@ async function writeBrandAssets(
['iconutil', '-c', 'icns', iconset, '-o', join(brandDir, 'icon.icns')],
brandDir,
);
return 'icon.icns';

const hasAssetsCar = await compileAssetsCar(config, brandDir, writePng);
return { macIcon: 'icon.icns', hasAssetsCar };
}

async function compileAssetsCar(
config: BrandConfig,
brandDir: string,
writePng: (output: string, size: number) => Promise<void>,
): Promise<boolean> {
const xcassets = join(brandDir, 'Assets.xcassets');
const appiconset = join(xcassets, 'AppIcon.appiconset');
rmSync(xcassets, { recursive: true, force: true });
mkdirSync(appiconset, { recursive: true });

writeFileSync(
join(xcassets, 'Contents.json'),
JSON.stringify({ info: { author: 'xcode', version: 1 } }),
);

const entries = [
{ file: 'icon_16.png', size: 16, scale: '1x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '2x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '1x', dims: '32x32' },
{ file: 'icon_64.png', size: 64, scale: '2x', dims: '32x32' },
{ file: 'icon_128.png', size: 128, scale: '1x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '2x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '1x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '2x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '1x', dims: '512x512' },
{ file: 'icon_1024.png', size: 1024, scale: '2x', dims: '512x512' },
];

const uniqueSizes = new Set(entries.map((e) => e.size));
for (const size of uniqueSizes) {
await writePng(join(appiconset, `icon_${size}.png`), size);
}

writeFileSync(
join(appiconset, 'Contents.json'),
JSON.stringify({
images: entries.map((e) => ({
filename: e.file,
idiom: 'mac',
scale: e.scale,
size: e.dims,
})),
info: { author: 'xcode', version: 1 },
}),
);

const outDir = mkdtempSync(join(tmpdir(), 'assets-car-'));
const partialPlist = join(outDir, 'partial-info.plist');
const proc = Bun.spawn({
cmd: [
'xcrun', 'actool', xcassets,
'--compile', outDir,
'--app-icon', 'AppIcon',
'--platform', 'macosx',
'--minimum-deployment-target', '14.0',
'--output-partial-info-plist', partialPlist,
],
cwd: brandDir,
stdout: 'pipe',
stderr: 'pipe',
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
console.log('Warning: actool compilation failed, skipping Assets.car');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

const compiledCar = join(outDir, 'Assets.car');
if (!existsSync(compiledCar)) {
console.log('Warning: actool produced no Assets.car, skipping');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

copyFileSync(compiledCar, join(brandDir, 'Assets.car'));
rmSync(xcassets, { recursive: true, force: true });
rmSync(outDir, { recursive: true, force: true });
console.log('Assets.car compiled successfully');
return true;
}

function tsString(value: string): string {
Expand All@@ -203,8 +294,11 @@ function helpMenuLinks(config: BrandConfig): string {
]`;
}

function brandBlock(config: BrandConfig, macIcon: string): string {
function brandBlock(config: BrandConfig, macIcon: string, hasAssetsCar: boolean): string {
const resourceDir = `resources/brands/${config.brandId}`;
const liquidGlassLine = hasAssetsCar
? `\n liquidGlassAssetsCar: ${tsString(`${resourceDir}/Assets.car`)},`
: '';

return ` ${tsString(config.brandId)}: {
id: ${tsString(config.brandId)},
Expand All@@ -223,7 +317,7 @@ function brandBlock(config: BrandConfig, macIcon: string): string {
macIcon: ${tsString(`${resourceDir}/${macIcon}`)},
winIcon: ${tsString(`${resourceDir}/icon.png`)},
linuxIcon: ${tsString(`${resourceDir}/icon.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},${liquidGlassLine}
},
credits: '',
creditsShort: '',
Expand All@@ -236,6 +330,7 @@ function registerBrand(
config: BrandConfig,
desktopRoot: string,
macIcon: string,
hasAssetsCar: boolean,
): void {
const brandingPath = join(
desktopRoot,
Expand All@@ -259,22 +354,25 @@ function registerBrand(

writeFileSync(
brandingPath,
source.replace(marker, `\n${brandBlock(config, macIcon)}${marker}`),
source.replace(marker, `\n${brandBlock(config, macIcon, hasAssetsCar)}${marker}`),
);
}

async function main(): Promise<void> {
const desktopRoot = desktopRootFromArgs();
const config = loadConfig(configPathFromArgs());
const macIcon = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon);
const { macIcon, hasAssetsCar } = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon, hasAssetsCar);

console.log(`Created brand ${config.brandId}`);
console.log(`App name: ${config.appName}`);
console.log(`App ID: ${config.appId}`);
console.log(
`Assets: ${join(desktopRoot, 'apps', 'electron', 'resources', 'brands', config.brandId)}`,
);
if (hasAssetsCar) {
console.log('Assets.car: generated (macOS 26+ Liquid Glass icon)');
}
}

main().catch((error: unknown) => {
Expand Down
3 changes: 3 additions & 0 deletions apps/electron/build/entitlements.mac.plist
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,5 +10,8 @@
<!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Voice dictation: microphone access under the hardened runtime. -->
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
2 changes: 2 additions & 0 deletions apps/electron/electron-builder.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,6 +72,8 @@ mac:
# The value must match --app-icon used in actool (see afterPack.js)
extendInfo:
CFBundleIconName: AppIcon
# Voice dictation: shown in the macOS microphone permission prompt.
NSMicrophoneUsageDescription: Qwen Code uses the microphone for voice dictation in the prompt composer.
target:
- target: dmg
arch:
Expand Down
42 changes: 39 additions & 3 deletions apps/electron/src/main/__tests__/network-proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,18 @@ describe('parseNoProxyRules', () => {

it('parses host:port', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(rules).toEqual([{ host: 'example.com', port: 8080, wildcard: false }]);
expect(rules).toEqual([
{ host: 'example.com', port: 8080, wildcard: false },
]);
});

it('does not parse malformed port suffixes', () => {
expect(parseNoProxyRules('example.com:443abc')).toEqual([
{ host: 'example.com:443abc', wildcard: false },
]);
expect(parseNoProxyRules('[::1]:abc')).toEqual([
{ host: '[::1]:abc', wildcard: false },
]);
});
});

Expand All@@ -55,7 +66,9 @@ describe('shouldBypassProxy', () => {
it('respects port-scoped rules', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('http://example.com:8080/path', rules)).toBe(true);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(false);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(
false,
);
});

it('matches implicit default ports', () => {
Expand All@@ -71,7 +84,9 @@ describe('shouldBypassProxy', () => {

// Explicit port that differs from rule should not match
const rules8080 = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(false);
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(
false,
);
});

it('wildcard bypasses everything', () => {
Expand All@@ -84,6 +99,27 @@ describe('shouldBypassProxy', () => {
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
});

it('respects IPv6 port-scoped rules', () => {
const rules = parseNoProxyRules('[::1]:3000');
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
expect(shouldBypassProxy('http://[::1]:3001/path', rules)).toBe(false);
});

it('does not bypass for malformed port suffixes', () => {
expect(
shouldBypassProxy(
'https://example.com/path',
parseNoProxyRules('example.com:443abc'),
),
).toBe(false);
expect(
shouldBypassProxy(
'http://[::1]:3000/path',
parseNoProxyRules('[::1]:abc'),
),
).toBe(false);
});

it('matches exact IP literal', () => {
const rules = parseNoProxyRules('192.168.1.1');
expect(shouldBypassProxy('http://192.168.1.1/', rules)).toBe(true);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ const requestContext = {

const getDefaultThinkingLevelMock = mock(() => 'think');
const setDefaultThinkingLevelMock = mock((_level: string) => true);
const setVoiceModelMock = mock((_model: string) => {});
let mockedWorkspace: Record<string, unknown> | null = null;
let mockedWorkspaceConfig: Record<string, unknown> | null = null;
const getWorkspaceByNameOrIdMock = mock(
Expand DownExpand Up@@ -64,9 +65,14 @@ mock.module('@craft-agent/shared/config', () => ({
getWorkspaceByNameOrId: getWorkspaceByNameOrIdMock,
getDefaultThinkingLevel: getDefaultThinkingLevelMock,
setDefaultThinkingLevel: setDefaultThinkingLevelMock,
setVoiceModel: setVoiceModelMock,
isProtectedWorkspace: () => false,
}));

mock.module('@craft-agent/shared/config/storage', () => ({
setVoiceModel: setVoiceModelMock,
}));

mock.module('@craft-agent/shared/workspaces', () => ({
loadWorkspaceConfig: loadWorkspaceConfigMock,
}));
Expand DownExpand Up@@ -94,6 +100,7 @@ describe('settings default thinking RPC handlers', () => {
handlers.clear();
getDefaultThinkingLevelMock.mockClear();
setDefaultThinkingLevelMock.mockClear();
setVoiceModelMock.mockClear();
mockedWorkspace = null;
mockedWorkspaceConfig = null;
getWorkspaceByNameOrIdMock.mockClear();
Expand DownExpand Up@@ -189,6 +196,17 @@ describe('settings default thinking RPC handlers', () => {
expect(setDefaultThinkingLevelMock).not.toHaveBeenCalled();
});

it('accepts dated voice model variants supported by the transport resolver', async () => {
const setHandler = handlers.get(RPC_CHANNELS.input.SET_VOICE_MODEL);
expect(setHandler).toBeTruthy();

await setHandler!(requestContext, 'qwen3-asr-flash-2025-06-01');

expect(setVoiceModelMock).toHaveBeenCalledWith(
'qwen3-asr-flash-2025-06-01',
);
});

it('returns global permission mode through Qwen ACP', async () => {
const getHandler = handlers.get(
RPC_CHANNELS.settings.GET_GLOBAL_PERMISSION_MODE,
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); chore(sync): sync qwen-code desktop updates by DragonnZhang · Pull Request #74 · modelstudioai/openwork · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6885faa
fix(cli): Preserve mid-turn image messages (#5183)
DragonnZhang Jul 13, 2026
94fe082
fix(desktop): address git branch badge review (#5247)
DragonnZhang Jul 13, 2026
cf4a78d
feat(extensions): add i18n support for extension displayName and desc…
DragonnZhang Jul 13, 2026
09d73a3
feat(desktop): compile macOS 26+ Liquid Glass Assets.car in brand-cre…
DragonnZhang Jul 13, 2026
19e2ef5
fix: Expand Windows ~\\ home paths and hide phantom (session) entries…
DragonnZhang Jul 13, 2026
737d4ef
fix(desktop): detect WebP and AVI in RIFF magic-byte sniffing (#5336)
DragonnZhang Jul 13, 2026
22f410a
fix(desktop): accept uppercase icon URL schemes (#5470)
DragonnZhang Jul 13, 2026
b803618
fix: accept uppercase endpoint URL schemes (#5443)
DragonnZhang Jul 13, 2026
946447a
fix(desktop): preserve uppercase favicon URLs (#5463)
DragonnZhang Jul 13, 2026
70e468c
fix(desktop): parse NO_PROXY ports strictly (#5498)
DragonnZhang Jul 13, 2026
fc6ac77
test(desktop): update blocked scheme open-url assertion (#5529)
DragonnZhang Jul 13, 2026
78b6348
fix(desktop): restore locale parity (#5537)
DragonnZhang Jul 13, 2026
9da8d0c
fix(desktop): parse server ports strictly (#5509)
DragonnZhang Jul 13, 2026
1b23e7d
fix(desktop): validate generic oauth token responses (#5511)
DragonnZhang Jul 13, 2026
287f48d
fix(desktop): allow double dots in bundle filenames (#5515)
DragonnZhang Jul 13, 2026
11c032e
test(desktop): align interceptor packaging contract (#5531)
DragonnZhang Jul 13, 2026
4085fcc
fix(desktop): keep sibling paths absolute (#5517)
DragonnZhang Jul 13, 2026
ef039a2
test(desktop): enable feedback flag in permission tests (#5533)
DragonnZhang Jul 13, 2026
7dbff4b
fix(desktop): separate transform data output lines (#5525)
DragonnZhang Jul 13, 2026
0b94813
fix(desktop): handle Windows file mentions (#5523)
DragonnZhang Jul 13, 2026
f1f666f
fix(desktop): consolidate path boundary checks (#5545)
DragonnZhang Jul 13, 2026
e516910
fix(desktop): reject fractional transfer sizes (#5527)
DragonnZhang Jul 13, 2026
416c1db
feat(desktop): show file preview in a resizable side panel instead of…
DragonnZhang Jul 13, 2026
d241dab
feat(memory): confirm auto-generated skills before persisting (#5616)
DragonnZhang Jul 13, 2026
bee2b93
fix(desktop): reject unsafe source slugs before deletion (#5829)
DragonnZhang Jul 13, 2026
8047f89
fix(desktop): harden remaining source path validation (#5914)
DragonnZhang Jul 13, 2026
ef26ef0
feat(memory): add a git-shared team memory tier (#5886)
DragonnZhang Jul 13, 2026
dfff8da
fix(desktop): normalize source slug validation errors (#5911)
DragonnZhang Jul 13, 2026
d1b2864
feat(desktop): voice dictation in the desktop app (#5856)
DragonnZhang Jul 13, 2026
76fb3ad
fix(cli): switch TUI prefix ✦→◆ to fix glyph overflow on some termina…
DragonnZhang Jul 13, 2026
8089c9d
fix(desktop): enforce transform_data isolation (#6285)
DragonnZhang Jul 13, 2026
304128e
fix(desktop): preserve glued automation history records (#6344)
DragonnZhang Jul 13, 2026
71625bb
fix(desktop): preserve MCP URL query suffixes (#6587)
DragonnZhang Jul 13, 2026
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
114 changes: 106 additions & 8 deletions .agents/skills/desktop-brand-builder/scripts/brand-create.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,10 +3,12 @@ import {
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { extname, join, resolve } from 'node:path';

interface BrandInput {
Expand DownExpand Up@@ -130,10 +132,15 @@ async function run(cmd: string[], cwd: string): Promise<void> {
}
}

interface BrandAssetsResult {
macIcon: string;
hasAssetsCar: boolean;
}

async function writeBrandAssets(
config: BrandConfig,
desktopRoot: string,
): Promise<string> {
): Promise<BrandAssetsResult> {
const requireFromDesktop = createRequire(join(desktopRoot, 'package.json'));
const sharp = requireFromDesktop('sharp') as typeof import('sharp');
const electronDir = join(desktopRoot, 'apps', 'electron');
Expand All@@ -157,7 +164,7 @@ async function writeBrandAssets(
await writePng(join(brandDir, 'dock.png'), 512);
await writePng(join(brandDir, 'symbol.png'), 512);

if (process.platform !== 'darwin') return 'icon.png';
if (process.platform !== 'darwin') return { macIcon: 'icon.png', hasAssetsCar: false };

const iconset = join(brandDir, 'icon.iconset');
rmSync(iconset, { recursive: true, force: true });
Expand All@@ -184,7 +191,91 @@ async function writeBrandAssets(
['iconutil', '-c', 'icns', iconset, '-o', join(brandDir, 'icon.icns')],
brandDir,
);
return 'icon.icns';

const hasAssetsCar = await compileAssetsCar(config, brandDir, writePng);
return { macIcon: 'icon.icns', hasAssetsCar };
}

async function compileAssetsCar(
config: BrandConfig,
brandDir: string,
writePng: (output: string, size: number) => Promise<void>,
): Promise<boolean> {
const xcassets = join(brandDir, 'Assets.xcassets');
const appiconset = join(xcassets, 'AppIcon.appiconset');
rmSync(xcassets, { recursive: true, force: true });
mkdirSync(appiconset, { recursive: true });

writeFileSync(
join(xcassets, 'Contents.json'),
JSON.stringify({ info: { author: 'xcode', version: 1 } }),
);

const entries = [
{ file: 'icon_16.png', size: 16, scale: '1x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '2x', dims: '16x16' },
{ file: 'icon_32.png', size: 32, scale: '1x', dims: '32x32' },
{ file: 'icon_64.png', size: 64, scale: '2x', dims: '32x32' },
{ file: 'icon_128.png', size: 128, scale: '1x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '2x', dims: '128x128' },
{ file: 'icon_256.png', size: 256, scale: '1x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '2x', dims: '256x256' },
{ file: 'icon_512.png', size: 512, scale: '1x', dims: '512x512' },
{ file: 'icon_1024.png', size: 1024, scale: '2x', dims: '512x512' },
];

const uniqueSizes = new Set(entries.map((e) => e.size));
for (const size of uniqueSizes) {
await writePng(join(appiconset, `icon_${size}.png`), size);
}

writeFileSync(
join(appiconset, 'Contents.json'),
JSON.stringify({
images: entries.map((e) => ({
filename: e.file,
idiom: 'mac',
scale: e.scale,
size: e.dims,
})),
info: { author: 'xcode', version: 1 },
}),
);

const outDir = mkdtempSync(join(tmpdir(), 'assets-car-'));
const partialPlist = join(outDir, 'partial-info.plist');
const proc = Bun.spawn({
cmd: [
'xcrun', 'actool', xcassets,
'--compile', outDir,
'--app-icon', 'AppIcon',
'--platform', 'macosx',
'--minimum-deployment-target', '14.0',
'--output-partial-info-plist', partialPlist,
],
cwd: brandDir,
stdout: 'pipe',
stderr: 'pipe',
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
console.log('Warning: actool compilation failed, skipping Assets.car');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

const compiledCar = join(outDir, 'Assets.car');
if (!existsSync(compiledCar)) {
console.log('Warning: actool produced no Assets.car, skipping');
rmSync(xcassets, { recursive: true, force: true });
return false;
}

copyFileSync(compiledCar, join(brandDir, 'Assets.car'));
rmSync(xcassets, { recursive: true, force: true });
rmSync(outDir, { recursive: true, force: true });
console.log('Assets.car compiled successfully');
return true;
}

function tsString(value: string): string {
Expand All@@ -203,8 +294,11 @@ function helpMenuLinks(config: BrandConfig): string {
]`;
}

function brandBlock(config: BrandConfig, macIcon: string): string {
function brandBlock(config: BrandConfig, macIcon: string, hasAssetsCar: boolean): string {
const resourceDir = `resources/brands/${config.brandId}`;
const liquidGlassLine = hasAssetsCar
? `\n liquidGlassAssetsCar: ${tsString(`${resourceDir}/Assets.car`)},`
: '';

return ` ${tsString(config.brandId)}: {
id: ${tsString(config.brandId)},
Expand All@@ -223,7 +317,7 @@ function brandBlock(config: BrandConfig, macIcon: string): string {
macIcon: ${tsString(`${resourceDir}/${macIcon}`)},
winIcon: ${tsString(`${resourceDir}/icon.png`)},
linuxIcon: ${tsString(`${resourceDir}/icon.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},${liquidGlassLine}
},
credits: '',
creditsShort: '',
Expand All@@ -236,6 +330,7 @@ function registerBrand(
config: BrandConfig,
desktopRoot: string,
macIcon: string,
hasAssetsCar: boolean,
): void {
const brandingPath = join(
desktopRoot,
Expand All@@ -259,22 +354,25 @@ function registerBrand(

writeFileSync(
brandingPath,
source.replace(marker, `\n${brandBlock(config, macIcon)}${marker}`),
source.replace(marker, `\n${brandBlock(config, macIcon, hasAssetsCar)}${marker}`),
);
}

async function main(): Promise<void> {
const desktopRoot = desktopRootFromArgs();
const config = loadConfig(configPathFromArgs());
const macIcon = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon);
const { macIcon, hasAssetsCar } = await writeBrandAssets(config, desktopRoot);
registerBrand(config, desktopRoot, macIcon, hasAssetsCar);

console.log(`Created brand ${config.brandId}`);
console.log(`App name: ${config.appName}`);
console.log(`App ID: ${config.appId}`);
console.log(
`Assets: ${join(desktopRoot, 'apps', 'electron', 'resources', 'brands', config.brandId)}`,
);
if (hasAssetsCar) {
console.log('Assets.car: generated (macOS 26+ Liquid Glass icon)');
}
}

main().catch((error: unknown) => {
Expand Down
3 changes: 3 additions & 0 deletions apps/electron/build/entitlements.mac.plist
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,5 +10,8 @@
<!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Voice dictation: microphone access under the hardened runtime. -->
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
2 changes: 2 additions & 0 deletions apps/electron/electron-builder.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,6 +72,8 @@ mac:
# The value must match --app-icon used in actool (see afterPack.js)
extendInfo:
CFBundleIconName: AppIcon
# Voice dictation: shown in the macOS microphone permission prompt.
NSMicrophoneUsageDescription: Qwen Code uses the microphone for voice dictation in the prompt composer.
target:
- target: dmg
arch:
Expand Down
42 changes: 39 additions & 3 deletions apps/electron/src/main/__tests__/network-proxy.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,7 +30,18 @@ describe('parseNoProxyRules', () => {

it('parses host:port', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(rules).toEqual([{ host: 'example.com', port: 8080, wildcard: false }]);
expect(rules).toEqual([
{ host: 'example.com', port: 8080, wildcard: false },
]);
});

it('does not parse malformed port suffixes', () => {
expect(parseNoProxyRules('example.com:443abc')).toEqual([
{ host: 'example.com:443abc', wildcard: false },
]);
expect(parseNoProxyRules('[::1]:abc')).toEqual([
{ host: '[::1]:abc', wildcard: false },
]);
});
});

Expand All@@ -55,7 +66,9 @@ describe('shouldBypassProxy', () => {
it('respects port-scoped rules', () => {
const rules = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('http://example.com:8080/path', rules)).toBe(true);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(false);
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(
false,
);
});

it('matches implicit default ports', () => {
Expand All@@ -71,7 +84,9 @@ describe('shouldBypassProxy', () => {

// Explicit port that differs from rule should not match
const rules8080 = parseNoProxyRules('example.com:8080');
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(false);
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(
false,
);
});

it('wildcard bypasses everything', () => {
Expand All@@ -84,6 +99,27 @@ describe('shouldBypassProxy', () => {
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
});

it('respects IPv6 port-scoped rules', () => {
const rules = parseNoProxyRules('[::1]:3000');
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
expect(shouldBypassProxy('http://[::1]:3001/path', rules)).toBe(false);
});

it('does not bypass for malformed port suffixes', () => {
expect(
shouldBypassProxy(
'https://example.com/path',
parseNoProxyRules('example.com:443abc'),
),
).toBe(false);
expect(
shouldBypassProxy(
'http://[::1]:3000/path',
parseNoProxyRules('[::1]:abc'),
),
).toBe(false);
});

it('matches exact IP literal', () => {
const rules = parseNoProxyRules('192.168.1.1');
expect(shouldBypassProxy('http://192.168.1.1/', rules)).toBe(true);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ const requestContext = {

const getDefaultThinkingLevelMock = mock(() => 'think');
const setDefaultThinkingLevelMock = mock((_level: string) => true);
const setVoiceModelMock = mock((_model: string) => {});
let mockedWorkspace: Record<string, unknown> | null = null;
let mockedWorkspaceConfig: Record<string, unknown> | null = null;
const getWorkspaceByNameOrIdMock = mock(
Expand DownExpand Up@@ -64,9 +65,14 @@ mock.module('@craft-agent/shared/config', () => ({
getWorkspaceByNameOrId: getWorkspaceByNameOrIdMock,
getDefaultThinkingLevel: getDefaultThinkingLevelMock,
setDefaultThinkingLevel: setDefaultThinkingLevelMock,
setVoiceModel: setVoiceModelMock,
isProtectedWorkspace: () => false,
}));

mock.module('@craft-agent/shared/config/storage', () => ({
setVoiceModel: setVoiceModelMock,
}));

mock.module('@craft-agent/shared/workspaces', () => ({
loadWorkspaceConfig: loadWorkspaceConfigMock,
}));
Expand DownExpand Up@@ -94,6 +100,7 @@ describe('settings default thinking RPC handlers', () => {
handlers.clear();
getDefaultThinkingLevelMock.mockClear();
setDefaultThinkingLevelMock.mockClear();
setVoiceModelMock.mockClear();
mockedWorkspace = null;
mockedWorkspaceConfig = null;
getWorkspaceByNameOrIdMock.mockClear();
Expand DownExpand Up@@ -189,6 +196,17 @@ describe('settings default thinking RPC handlers', () => {
expect(setDefaultThinkingLevelMock).not.toHaveBeenCalled();
});

it('accepts dated voice model variants supported by the transport resolver', async () => {
const setHandler = handlers.get(RPC_CHANNELS.input.SET_VOICE_MODEL);
expect(setHandler).toBeTruthy();

await setHandler!(requestContext, 'qwen3-asr-flash-2025-06-01');

expect(setVoiceModelMock).toHaveBeenCalledWith(
'qwen3-asr-flash-2025-06-01',
);
});

it('returns global permission mode through Qwen ACP', async () => {
const getHandler = handlers.get(
RPC_CHANNELS.settings.GET_GLOBAL_PERMISSION_MODE,
Expand Down
Loading