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
13b181b
fix(webui): retry session load while closing
yiliang114 Aug 10, 2026
6ac08da
fix(desktop): support enterprise LAN addresses
yiliang114 Aug 10, 2026
a8b0b1f
fix(desktop): reject unbounded LAN masks
yiliang114 Aug 10, 2026
0d8791f
chore(desktop): sync 0.1.1 candidate with main
yiliang114 Aug 10, 2026
aebd9f7
Merge remote-tracking branch 'origin/main' into codex/desktop-011-rel…
yiliang114 Aug 10, 2026
04267c5
fix(webui): close desktop session and voice regressions
yiliang114 Aug 10, 2026
2039865
Merge remote-tracking branch 'origin/main' into codex/desktop-011-rel…
yiliang114 Aug 10, 2026
5b64ce9
fix(desktop): refresh signed runtime checksums
yiliang114 Aug 10, 2026
2d64eae
fix(desktop): close 0.1.1 regression gaps
yiliang114 Aug 10, 2026
da2477b
fix(webui): make quick voice presses start recording
yiliang114 Aug 10, 2026
97c5103
fix(webui): make quick voice presses start recording
yiliang114 Aug 10, 2026
a929e88
fix(web-shell): keep voice clicks responsive
yiliang114 Aug 10, 2026
f57a9bd
fix(webui): settle timed-out session loads cleanly
yiliang114 Aug 10, 2026
81e5eb7
fix(web-shell): keep voice clicks responsive
yiliang114 Aug 10, 2026
51c9d48
fix(desktop): finalize interaction regressions
yiliang114 Aug 10, 2026
11b2a30
fix(web-shell): preserve hold click semantics
yiliang114 Aug 10, 2026
59ab9be
fix(desktop): reject stale packaged runtimes
yiliang114 Aug 10, 2026
400fe53
fix(serve): avoid cancelling closed sessions
yiliang114 Aug 10, 2026
4baaa3c
fix(web-shell): preserve hold click suppression
yiliang114 Aug 11, 2026
d011c0e
fix(desktop): consolidate 0.1.1 regressions
yiliang114 Aug 11, 2026
e727b57
fix(web-shell): preserve hold click suppression
yiliang114 Aug 11, 2026
b30e12a
fix(desktop): center and localize local control
yiliang114 Aug 11, 2026
904bc23
Merge remote-tracking branch 'origin/main' into cx/resolve-8896
yiliang114 Aug 11, 2026
6f7be7f
test(desktop): pin interaction regressions
yiliang114 Aug 11, 2026
68f3934
Merge remote-tracking branch 'origin/main' into cx/resolve-8896
yiliang114 Aug 11, 2026
76e1c39
fix: close network and session race gaps
yiliang114 Aug 11, 2026
b99649e
Merge branch 'main' into codex/desktop-011-followups
yiliang114 Aug 11, 2026
6965a6e
Merge branch 'main' into codex/desktop-011-followups
yiliang114 Aug 11, 2026
f3dd3ab
fix(desktop): remove duplicate network.contains check in spawn_proxy
yiliang114 Aug 11, 2026
a62448d
Merge commit 'refs/codex/pr-8884-base' into pr-8896
yiliang114 Aug 11, 2026
ef1f96b
fix(desktop): clear stale click suppression and exclude virtual inter…
yiliang114 Aug 11, 2026
01c4ff2
test(desktop): cover cross-platform close fallbacks
yiliang114 Aug 11, 2026
87d09b1
Merge remote-tracking branch 'origin/main' into codex/desktop-011-fol…
yiliang114 Aug 11, 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
13 changes: 9 additions & 4 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,6 @@ jobs:
working-directory: 'packages/desktop-shell'
run: 'npm run build:runtime'

- name: 'Verify bundled runtime'
working-directory: 'packages/desktop-shell'
run: 'npm run smoke:runtime'

- name: 'Run desktop tests'
working-directory: 'packages/desktop-shell'
run: 'npm test'
Expand Down Expand Up @@ -361,6 +357,15 @@ jobs:
echo "::warning::Node.js runtime binary not found at $node_bin; no Node.js binary signed."
fi

- name: 'Refresh bundled runtime checksums after signing (macOS)'
Comment thread
yiliang114 marked this conversation as resolved.
if: "runner.os == 'macOS' && inputs.dry_run == false"
Comment thread
yiliang114 marked this conversation as resolved.
working-directory: 'packages/desktop-shell'
run: 'node scripts/prepare-runtime.js --refresh-checksums'

- name: 'Verify bundled runtime'
working-directory: 'packages/desktop-shell'
run: 'npm run smoke:runtime'

- name: 'Build desktop installers'
working-directory: 'packages/desktop-shell'
shell: 'bash'
Expand Down
39 changes: 38 additions & 1 deletion packages/acp-bridge/src/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4485,7 +4485,6 @@ describe('createAcpSessionBridge', () => {
hasMore: false,
});

await expect(refresh).rejects.toBeInstanceOf(SessionNotFoundError);
await expect(refresh).rejects.toMatchObject({
code: 'session_closing',
});
Comment thread
yiliang114 marked this conversation as resolved.
Expand Down Expand Up @@ -20990,6 +20989,44 @@ describe('session idle reaper', () => {
await bridge.shutdown();
});

it('does not cancel a session the agent already closed', async () => {
Comment thread
yiliang114 marked this conversation as resolved.
const handle = makeChannel({
extMethodImpl: (method) =>
method === SERVE_CONTROL_EXT_METHODS.sessionClose
? { closed: true }
: {},
});
const bridge = makeBridge({
channelFactory: async () => handle.channel,
});
const session = await bridge.spawnOrAttach({ workspaceCwd: WS_A });

await bridge.closeSession(session.sessionId);

expect(handle.agent.cancelCalls).toEqual([]);
await bridge.shutdown();
});

it('cancels a session the agent did not close', async () => {
const handle = makeChannel({
extMethodImpl: (method) =>
method === SERVE_CONTROL_EXT_METHODS.sessionClose
? { closed: false }
: {},
});
const bridge = makeBridge({
channelFactory: async () => handle.channel,
});
const session = await bridge.spawnOrAttach({ workspaceCwd: WS_A });

await bridge.closeSession(session.sessionId);

expect(handle.agent.cancelCalls).toEqual([
{ sessionId: session.sessionId },
]);
await bridge.shutdown();
});

it('reaps multiple orphaned sessions in one tick', async () => {
vi.useFakeTimers();
try {
Expand Down
54 changes: 32 additions & 22 deletions packages/acp-bridge/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4207,7 +4207,7 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
requireFlush?: boolean;
timeoutMs?: number;
},
): Promise<void> => {
): Promise<boolean> => {
if (!ci || ci.channel !== entry.channel) {
if (opts?.throwOnFailure === true) {
writeStderrLine(
Expand All @@ -4218,7 +4218,7 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
`ACP session close channel unavailable for ${entry.sessionId}`,
);
}
return;
return false;
}
try {
const closeRequest = entry.connection.extMethod(
Expand All @@ -4232,7 +4232,7 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
const observedCloseRequest = opts?.timeoutMs
? withTimeout(closeRequest, opts.timeoutMs, label)
: closeRequest;
await Promise.race([
const response = await Promise.race([
opts?.throwOnFailure === true
? observedCloseRequest
: withTimeout(
Expand All @@ -4242,6 +4242,7 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
),
getTransportClosedReject(entry),
]);
return response['closed'] === true;
} catch (err) {
writeStderrLine(
`qwen serve: ${label} ACP session close notification failed ` +
Expand All @@ -4252,6 +4253,7 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
if (opts?.throwOnFailure === true) {
throw err;
}
return false;
}
};

Expand Down Expand Up @@ -5900,19 +5902,25 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
`for session ${JSON.stringify(sessionId)} — channel cleanup skipped (entry's channel already torn down)`,
);
}
let agentSessionClosed = false;
try {
// Resolve permission waits before asking the agent to drain active turns;
// otherwise a turn blocked in requestPermission can deadlock close.
permissionMediator.forgetSession(sessionId);
entry.pendingPermissionIds.clear();
entry.pendingInteractions.clear();
await notifyAgentSessionClose(entry, ci, 'closeSession', {
throwOnFailure: true,
requireFlush: closeOpts?.requireAgentClose === true,
...(closeOpts?.agentCloseTimeoutMs !== undefined
? { timeoutMs: closeOpts.agentCloseTimeoutMs }
: {}),
});
agentSessionClosed = await notifyAgentSessionClose(
entry,
ci,
'closeSession',
{
throwOnFailure: true,
requireFlush: closeOpts?.requireAgentClose === true,
...(closeOpts?.agentCloseTimeoutMs !== undefined
? { timeoutMs: closeOpts.agentCloseTimeoutMs }
: {}),
},
);
} catch (error) {
// A child RequestError is a definitive close refusal: the child kept
// the session live, so a retry is safe. A transport failure has an
Expand Down Expand Up @@ -5983,18 +5991,20 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
// `session_closed` is terminal. Close the bus before ACP cancel so any
// late cancellation frames from the agent are intentionally dropped.
entry.events.close();
try {
await telemetry.withSpan(
'session.close.cancel_active_prompt',
{
'qwen-code.daemon.bridge.operation':
'session.close.cancel_active_prompt',
'session.id': sessionId,
},
async () => await entry.connection.cancel({ sessionId }),
);
} catch {
/* no active prompt or session already torn down */
if (!agentSessionClosed) {
try {
await telemetry.withSpan(
'session.close.cancel_active_prompt',
{
'qwen-code.daemon.bridge.operation':
'session.close.cancel_active_prompt',
'session.id': sessionId,
},
async () => await entry.connection.cancel({ sessionId }),
);
} catch {
/* no active prompt or session already torn down */
}
}
if (ci && hasNoChannelWork(ci)) {
await reapPendingEmptyChannel(ci);
Expand Down
20 changes: 13 additions & 7 deletions packages/desktop-shell/bootstrap/local-control.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,27 +148,33 @@
<section class="card" aria-live="polite">
<header>
<div>
<h1>Local Control</h1>
<p>Continue this session from your phone.</p>
<h1 data-i18n="heading">Local Control</h1>
<p data-i18n="subtitle">Continue this session from your phone.</p>
</div>
<span id="badge" class="badge">Off</span>
<span id="badge" class="badge" data-i18n="off">Off</span>
</header>

<section id="inactive">
<p>Turn this on, then scan from a phone on the same trusted Wi-Fi.</p>
<div class="notice">
<p data-i18n="inactiveCopy">
Turn this on, then scan from a phone on the same trusted Wi-Fi.
</p>
<div class="notice" data-i18n="inactiveNotice">
Uses unencrypted HTTP. Phone access stays closed until enabled.
</div>
</section>

<section id="active" hidden>
<div id="qr" class="qr" aria-label="Local Control QR code"></div>
<div
id="qr"
class="qr"
aria-label="Local Control QR code"
></div>
<div id="url" class="url"></div>
<div id="sleep" class="notice"></div>
</section>

<div id="error" class="error" hidden></div>
<button id="toggle">Turn on Local Control</button>
<button id="toggle" data-i18n="turnOn">Turn on Local Control</button>
</section>
</main>
<script src="./local-control.js"></script>
Expand Down
59 changes: 52 additions & 7 deletions packages/desktop-shell/bootstrap/local-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,68 @@ const sleep = document.querySelector('#sleep');
const error = document.querySelector('#error');
const toggle = document.querySelector('#toggle');

const messages = {
en: {
title: 'Local Control',
heading: 'Local Control',
subtitle: 'Continue this session from your phone.',
off: 'Off',
on: 'On',
inactiveCopy:
'Turn this on, then scan from a phone on the same trusted Wi-Fi.',
inactiveNotice: 'Uses unencrypted HTTP. Phone access stays closed until enabled.',
qrLabel: 'Local Control QR code',
turnOn: 'Turn on Local Control',
disconnect: 'Disconnect phone access',
awake: 'Trusted Wi-Fi · Unencrypted · Re-enable after network changes',
maySleep:
'Trusted Wi-Fi · Unencrypted · May sleep · Re-enable after network changes',
bridgeUnavailable: 'The Desktop bridge is unavailable.',
},
'zh-CN': {
title: '本地控制',
heading: '本地控制',
subtitle: '在手机上继续当前会话。',
off: '关闭',
on: '已开启',
inactiveCopy: '开启后,使用同一受信任 Wi-Fi 中的手机扫码。',
inactiveNotice: '使用未加密 HTTP。开启前,手机访问保持关闭。',
qrLabel: '本地控制二维码',
turnOn: '开启本地控制',
disconnect: '断开手机访问',
awake: '受信任 Wi-Fi · 未加密 · 网络变化后需重新开启',
maySleep: '受信任 Wi-Fi · 未加密 · 可能休眠 · 网络变化后需重新开启',
bridgeUnavailable: '桌面端桥接不可用。',
},
};

const language = navigator.language.toLowerCase().startsWith('zh')
Comment thread
yiliang114 marked this conversation as resolved.
? 'zh-CN'
: 'en';
const t = (key) => messages[language][key];
Comment thread
yiliang114 marked this conversation as resolved.

document.documentElement.lang = language;
document.title = `Qwen Code ${t('title')}`;
document.querySelectorAll('[data-i18n]').forEach((element) => {
element.textContent = t(element.dataset.i18n);
});
qr.setAttribute('aria-label', t('qrLabel'));

let enabled = false;

function render(state) {
enabled = state.active;
badge.textContent = enabled ? 'On' : 'Off';
badge.textContent = enabled ? t('on') : t('off');
badge.className = `badge${enabled ? ' on' : ''}`;
inactive.hidden = enabled;
active.hidden = !enabled;
toggle.textContent = enabled
? 'Disconnect phone access'
: 'Turn on Local Control';
toggle.textContent = enabled ? t('disconnect') : t('turnOn');
toggle.className = enabled ? 'stop' : '';
qr.innerHTML = enabled ? state.qrSvg || '' : '';
url.textContent = enabled ? state.url || '' : '';
sleep.textContent = state.sleepInhibited
? 'Trusted Wi-Fi · Unencrypted · Re-enable after network changes'
: 'Trusted Wi-Fi · Unencrypted · May sleep · Re-enable after network changes';
? t('awake')
: t('maySleep');
error.hidden = true;
error.textContent = '';
}
Expand All @@ -54,7 +99,7 @@ toggle.addEventListener('click', toggleLocalControl);

async function initialize() {
if (!invoke || !listen) {
throw new Error('The Desktop bridge is unavailable.');
throw new Error(t('bridgeUnavailable'));
}
await listen('local-control-changed', ({ payload }) => render(payload));
render(await invoke('local_control_status'));
Expand Down
17 changes: 13 additions & 4 deletions packages/desktop-shell/scripts/prepare-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ const sourceRoot = process.env.QWEN_CODE_ROOT
: repoRoot;
const runtimeDir = path.join(packageDir, 'runtime');
const packageRoot = path.join(runtimeDir, 'qwen-code');
const refreshChecksums = process.argv.indexOf('--refresh-checksums');
Comment thread
yiliang114 marked this conversation as resolved.
if (refreshChecksums !== -1) {
const root = process.argv[refreshChecksums + 1]
? path.resolve(process.argv[refreshChecksums + 1])
: packageRoot;
writeChecksums(root);
console.log(`Refreshed desktop runtime checksums at ${root}`);
process.exit(0);
}
const libDir = path.join(packageRoot, 'lib');
const nodeDir = path.join(packageRoot, 'node');
const qwenCodeVersion = JSON.parse(
Expand Down Expand Up @@ -249,18 +258,18 @@ function gitCommit(directory) {
}).trim();
}

function writeChecksums() {
function writeChecksums(root = packageRoot) {
const checksums = {};
for (const file of runtimeFiles(packageRoot)) {
const relative = path.relative(packageRoot, file).split(path.sep).join('/');
for (const file of runtimeFiles(root)) {
const relative = path.relative(root, file).split(path.sep).join('/');
if (relative === 'checksums.json') continue;
checksums[relative] = crypto
.createHash('sha256')
.update(fs.readFileSync(file))
.digest('hex');
}
fs.writeFileSync(
path.join(packageRoot, 'checksums.json'),
path.join(root, 'checksums.json'),
`${JSON.stringify(checksums, null, 2)}\n`,
);
}
Expand Down
22 changes: 22 additions & 0 deletions packages/desktop-shell/scripts/smoke-packaged.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ const packageDir = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..',
);
const repoRoot = path.resolve(packageDir, '../..');
const executable = process.argv[2];
if (!executable)
throw new Error('Usage: node scripts/smoke-packaged.js <executable>');
if (!fs.statSync(executable, { throwIfNoEntry: false })?.isFile()) {
throw new Error(`Packaged executable is missing: ${executable}`);
}
verifyMacRuntimeCommit();

const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'qwen-desktop-smoke-'));
const isolatedHome = path.join(workspace, 'home');
Expand Down Expand Up @@ -208,3 +210,23 @@ function terminate(pid) {
// The process may already have exited after the smoke succeeded or failed.
}
}

function verifyMacRuntimeCommit() {
Comment thread
yiliang114 marked this conversation as resolved.
if (process.platform !== 'darwin') return;
const manifestPath = path.resolve(
path.dirname(executable),
'../Resources/runtime/qwen-code/manifest.json',
);
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const expected =
process.env.QWEN_CODE_COMMIT ||
execFileSync('git', ['rev-parse', 'HEAD'], {
cwd: process.env.QWEN_CODE_ROOT || repoRoot,
encoding: 'utf8',
}).trim();
if (manifest.qwenCodeCommit !== expected) {
throw new Error(
`Packaged runtime commit mismatch: expected ${expected}, found ${manifest.qwenCodeCommit || 'missing'}`,
);
}
}
Loading
Loading