Skip to content

Commit 3ca23e8

Browse files
committed
Merge remote-tracking branch 'origin/main' into loop/command-palette-recents
# Conflicts: # docs/loop/feature-ledger.md
2 parents b7862bc + 4455781 commit 3ca23e8

165 files changed

Lines changed: 11546 additions & 833 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/desktop-brand-builder/scripts/brand-create.ts

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import {
33
copyFileSync,
44
existsSync,
55
mkdirSync,
6+
mkdtempSync,
67
readFileSync,
78
rmSync,
89
writeFileSync,
910
} from 'node:fs';
11+
import { tmpdir } from 'node:os';
1012
import { extname, join, resolve } from 'node:path';
1113

1214
interface BrandInput {
@@ -130,10 +132,15 @@ async function run(cmd: string[], cwd: string): Promise<void> {
130132
}
131133
}
132134

135+
interface BrandAssetsResult {
136+
macIcon: string;
137+
hasAssetsCar: boolean;
138+
}
139+
133140
async function writeBrandAssets(
134141
config: BrandConfig,
135142
desktopRoot: string,
136-
): Promise<string> {
143+
): Promise<BrandAssetsResult> {
137144
const requireFromDesktop = createRequire(join(desktopRoot, 'package.json'));
138145
const sharp = requireFromDesktop('sharp') as typeof import('sharp');
139146
const electronDir = join(desktopRoot, 'apps', 'electron');
@@ -157,7 +164,7 @@ async function writeBrandAssets(
157164
await writePng(join(brandDir, 'dock.png'), 512);
158165
await writePng(join(brandDir, 'symbol.png'), 512);
159166

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

162169
const iconset = join(brandDir, 'icon.iconset');
163170
rmSync(iconset, { recursive: true, force: true });
@@ -184,7 +191,91 @@ async function writeBrandAssets(
184191
['iconutil', '-c', 'icns', iconset, '-o', join(brandDir, 'icon.icns')],
185192
brandDir,
186193
);
187-
return 'icon.icns';
194+
195+
const hasAssetsCar = await compileAssetsCar(config, brandDir, writePng);
196+
return { macIcon: 'icon.icns', hasAssetsCar };
197+
}
198+
199+
async function compileAssetsCar(
200+
config: BrandConfig,
201+
brandDir: string,
202+
writePng: (output: string, size: number) => Promise<void>,
203+
): Promise<boolean> {
204+
const xcassets = join(brandDir, 'Assets.xcassets');
205+
const appiconset = join(xcassets, 'AppIcon.appiconset');
206+
rmSync(xcassets, { recursive: true, force: true });
207+
mkdirSync(appiconset, { recursive: true });
208+
209+
writeFileSync(
210+
join(xcassets, 'Contents.json'),
211+
JSON.stringify({ info: { author: 'xcode', version: 1 } }),
212+
);
213+
214+
const entries = [
215+
{ file: 'icon_16.png', size: 16, scale: '1x', dims: '16x16' },
216+
{ file: 'icon_32.png', size: 32, scale: '2x', dims: '16x16' },
217+
{ file: 'icon_32.png', size: 32, scale: '1x', dims: '32x32' },
218+
{ file: 'icon_64.png', size: 64, scale: '2x', dims: '32x32' },
219+
{ file: 'icon_128.png', size: 128, scale: '1x', dims: '128x128' },
220+
{ file: 'icon_256.png', size: 256, scale: '2x', dims: '128x128' },
221+
{ file: 'icon_256.png', size: 256, scale: '1x', dims: '256x256' },
222+
{ file: 'icon_512.png', size: 512, scale: '2x', dims: '256x256' },
223+
{ file: 'icon_512.png', size: 512, scale: '1x', dims: '512x512' },
224+
{ file: 'icon_1024.png', size: 1024, scale: '2x', dims: '512x512' },
225+
];
226+
227+
const uniqueSizes = new Set(entries.map((e) => e.size));
228+
for (const size of uniqueSizes) {
229+
await writePng(join(appiconset, `icon_${size}.png`), size);
230+
}
231+
232+
writeFileSync(
233+
join(appiconset, 'Contents.json'),
234+
JSON.stringify({
235+
images: entries.map((e) => ({
236+
filename: e.file,
237+
idiom: 'mac',
238+
scale: e.scale,
239+
size: e.dims,
240+
})),
241+
info: { author: 'xcode', version: 1 },
242+
}),
243+
);
244+
245+
const outDir = mkdtempSync(join(tmpdir(), 'assets-car-'));
246+
const partialPlist = join(outDir, 'partial-info.plist');
247+
const proc = Bun.spawn({
248+
cmd: [
249+
'xcrun', 'actool', xcassets,
250+
'--compile', outDir,
251+
'--app-icon', 'AppIcon',
252+
'--platform', 'macosx',
253+
'--minimum-deployment-target', '14.0',
254+
'--output-partial-info-plist', partialPlist,
255+
],
256+
cwd: brandDir,
257+
stdout: 'pipe',
258+
stderr: 'pipe',
259+
});
260+
const exitCode = await proc.exited;
261+
if (exitCode !== 0) {
262+
console.log('Warning: actool compilation failed, skipping Assets.car');
263+
rmSync(xcassets, { recursive: true, force: true });
264+
return false;
265+
}
266+
267+
const compiledCar = join(outDir, 'Assets.car');
268+
if (!existsSync(compiledCar)) {
269+
console.log('Warning: actool produced no Assets.car, skipping');
270+
rmSync(xcassets, { recursive: true, force: true });
271+
return false;
272+
}
273+
274+
copyFileSync(compiledCar, join(brandDir, 'Assets.car'));
275+
rmSync(xcassets, { recursive: true, force: true });
276+
rmSync(outDir, { recursive: true, force: true });
277+
console.log('Assets.car compiled successfully');
278+
return true;
188279
}
189280

190281
function tsString(value: string): string {
@@ -203,8 +294,11 @@ function helpMenuLinks(config: BrandConfig): string {
203294
]`;
204295
}
205296

206-
function brandBlock(config: BrandConfig, macIcon: string): string {
297+
function brandBlock(config: BrandConfig, macIcon: string, hasAssetsCar: boolean): string {
207298
const resourceDir = `resources/brands/${config.brandId}`;
299+
const liquidGlassLine = hasAssetsCar
300+
? `\n liquidGlassAssetsCar: ${tsString(`${resourceDir}/Assets.car`)},`
301+
: '';
208302

209303
return ` ${tsString(config.brandId)}: {
210304
id: ${tsString(config.brandId)},
@@ -223,7 +317,7 @@ function brandBlock(config: BrandConfig, macIcon: string): string {
223317
macIcon: ${tsString(`${resourceDir}/${macIcon}`)},
224318
winIcon: ${tsString(`${resourceDir}/icon.png`)},
225319
linuxIcon: ${tsString(`${resourceDir}/icon.png`)},
226-
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},
320+
devDockIcon: ${tsString(`${resourceDir}/dock.png`)},${liquidGlassLine}
227321
},
228322
credits: '',
229323
creditsShort: '',
@@ -236,6 +330,7 @@ function registerBrand(
236330
config: BrandConfig,
237331
desktopRoot: string,
238332
macIcon: string,
333+
hasAssetsCar: boolean,
239334
): void {
240335
const brandingPath = join(
241336
desktopRoot,
@@ -259,22 +354,25 @@ function registerBrand(
259354

260355
writeFileSync(
261356
brandingPath,
262-
source.replace(marker, `\n${brandBlock(config, macIcon)}${marker}`),
357+
source.replace(marker, `\n${brandBlock(config, macIcon, hasAssetsCar)}${marker}`),
263358
);
264359
}
265360

266361
async function main(): Promise<void> {
267362
const desktopRoot = desktopRootFromArgs();
268363
const config = loadConfig(configPathFromArgs());
269-
const macIcon = await writeBrandAssets(config, desktopRoot);
270-
registerBrand(config, desktopRoot, macIcon);
364+
const { macIcon, hasAssetsCar } = await writeBrandAssets(config, desktopRoot);
365+
registerBrand(config, desktopRoot, macIcon, hasAssetsCar);
271366

272367
console.log(`Created brand ${config.brandId}`);
273368
console.log(`App name: ${config.appName}`);
274369
console.log(`App ID: ${config.appId}`);
275370
console.log(
276371
`Assets: ${join(desktopRoot, 'apps', 'electron', 'resources', 'brands', config.brandId)}`,
277372
);
373+
if (hasAssetsCar) {
374+
console.log('Assets.car: generated (macOS 26+ Liquid Glass icon)');
375+
}
278376
}
279377

280378
main().catch((error: unknown) => {

apps/electron/build/entitlements.mac.plist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@
1010
<!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
1111
<key>com.apple.security.cs.disable-library-validation</key>
1212
<true/>
13+
<!-- Voice dictation: microphone access under the hardened runtime. -->
14+
<key>com.apple.security.device.audio-input</key>
15+
<true/>
1316
</dict>
1417
</plist>

apps/electron/electron-builder.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ mac:
7272
# The value must match --app-icon used in actool (see afterPack.js)
7373
extendInfo:
7474
CFBundleIconName: AppIcon
75+
# Voice dictation: shown in the macOS microphone permission prompt.
76+
NSMicrophoneUsageDescription: Qwen Code uses the microphone for voice dictation in the prompt composer.
7577
target:
7678
- target: dmg
7779
arch:

apps/electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@craft-agent/electron",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"description": "Electron desktop app for Qwen Code",
55
"main": "dist/main.cjs",
66
"private": true,

apps/electron/src/main/__tests__/network-proxy.test.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,18 @@ describe('parseNoProxyRules', () => {
3030

3131
it('parses host:port', () => {
3232
const rules = parseNoProxyRules('example.com:8080');
33-
expect(rules).toEqual([{ host: 'example.com', port: 8080, wildcard: false }]);
33+
expect(rules).toEqual([
34+
{ host: 'example.com', port: 8080, wildcard: false },
35+
]);
36+
});
37+
38+
it('does not parse malformed port suffixes', () => {
39+
expect(parseNoProxyRules('example.com:443abc')).toEqual([
40+
{ host: 'example.com:443abc', wildcard: false },
41+
]);
42+
expect(parseNoProxyRules('[::1]:abc')).toEqual([
43+
{ host: '[::1]:abc', wildcard: false },
44+
]);
3445
});
3546
});
3647

@@ -55,7 +66,9 @@ describe('shouldBypassProxy', () => {
5566
it('respects port-scoped rules', () => {
5667
const rules = parseNoProxyRules('example.com:8080');
5768
expect(shouldBypassProxy('http://example.com:8080/path', rules)).toBe(true);
58-
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(false);
69+
expect(shouldBypassProxy('http://example.com:9090/path', rules)).toBe(
70+
false,
71+
);
5972
});
6073

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

7285
// Explicit port that differs from rule should not match
7386
const rules8080 = parseNoProxyRules('example.com:8080');
74-
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(false);
87+
expect(shouldBypassProxy('https://example.com/path', rules8080)).toBe(
88+
false,
89+
);
7590
});
7691

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

102+
it('respects IPv6 port-scoped rules', () => {
103+
const rules = parseNoProxyRules('[::1]:3000');
104+
expect(shouldBypassProxy('http://[::1]:3000/path', rules)).toBe(true);
105+
expect(shouldBypassProxy('http://[::1]:3001/path', rules)).toBe(false);
106+
});
107+
108+
it('does not bypass for malformed port suffixes', () => {
109+
expect(
110+
shouldBypassProxy(
111+
'https://example.com/path',
112+
parseNoProxyRules('example.com:443abc'),
113+
),
114+
).toBe(false);
115+
expect(
116+
shouldBypassProxy(
117+
'http://[::1]:3000/path',
118+
parseNoProxyRules('[::1]:abc'),
119+
),
120+
).toBe(false);
121+
});
122+
87123
it('matches exact IP literal', () => {
88124
const rules = parseNoProxyRules('192.168.1.1');
89125
expect(shouldBypassProxy('http://192.168.1.1/', rules)).toBe(true);

apps/electron/src/main/handlers/__tests__/settings-default-thinking.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const requestContext = {
1111

1212
const getDefaultThinkingLevelMock = mock(() => 'think');
1313
const setDefaultThinkingLevelMock = mock((_level: string) => true);
14+
const setVoiceModelMock = mock((_model: string) => {});
1415
let mockedWorkspace: Record<string, unknown> | null = null;
1516
let mockedWorkspaceConfig: Record<string, unknown> | null = null;
1617
const getWorkspaceByNameOrIdMock = mock(
@@ -64,9 +65,14 @@ mock.module('@craft-agent/shared/config', () => ({
6465
getWorkspaceByNameOrId: getWorkspaceByNameOrIdMock,
6566
getDefaultThinkingLevel: getDefaultThinkingLevelMock,
6667
setDefaultThinkingLevel: setDefaultThinkingLevelMock,
68+
setVoiceModel: setVoiceModelMock,
6769
isProtectedWorkspace: () => false,
6870
}));
6971

72+
mock.module('@craft-agent/shared/config/storage', () => ({
73+
setVoiceModel: setVoiceModelMock,
74+
}));
75+
7076
mock.module('@craft-agent/shared/workspaces', () => ({
7177
loadWorkspaceConfig: loadWorkspaceConfigMock,
7278
}));
@@ -94,6 +100,7 @@ describe('settings default thinking RPC handlers', () => {
94100
handlers.clear();
95101
getDefaultThinkingLevelMock.mockClear();
96102
setDefaultThinkingLevelMock.mockClear();
103+
setVoiceModelMock.mockClear();
97104
mockedWorkspace = null;
98105
mockedWorkspaceConfig = null;
99106
getWorkspaceByNameOrIdMock.mockClear();
@@ -189,6 +196,17 @@ describe('settings default thinking RPC handlers', () => {
189196
expect(setDefaultThinkingLevelMock).not.toHaveBeenCalled();
190197
});
191198

199+
it('accepts dated voice model variants supported by the transport resolver', async () => {
200+
const setHandler = handlers.get(RPC_CHANNELS.input.SET_VOICE_MODEL);
201+
expect(setHandler).toBeTruthy();
202+
203+
await setHandler!(requestContext, 'qwen3-asr-flash-2025-06-01');
204+
205+
expect(setVoiceModelMock).toHaveBeenCalledWith(
206+
'qwen3-asr-flash-2025-06-01',
207+
);
208+
});
209+
192210
it('returns global permission mode through Qwen ACP', async () => {
193211
const getHandler = handlers.get(
194212
RPC_CHANNELS.settings.GET_GLOBAL_PERMISSION_MODE,

0 commit comments

Comments
 (0)