Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5b0afa8
feat: add SEA binary build for linux/amd64
netbrah Mar 20, 2026
7470246
feat: add QWEN_CODE_HOME env var for config dir override
netbrah Mar 20, 2026
1405efa
chore: add .bin/.gitkeep, exclude .bin/ contents from tracking
netbrah Mar 20, 2026
3200883
fix: bracket notation for process.env in strict TS, HUSKY=0 in Docker…
netbrah Mar 20, 2026
634ae2b
feat: support QWEN_CODE_BRAND env var for startup banner rebrand
netbrah Mar 20, 2026
206b40b
fix: skip V8 memory relaunch for SEA binaries
netbrah Mar 20, 2026
7e3d3c5
fix: skip entire relaunch/sandbox block for SEA binaries
netbrah Mar 20, 2026
9a39ec9
feat: APEX ASCII logo for branded startup splash
netbrah Mar 20, 2026
14fac4a
commit
netbrah Mar 20, 2026
9a9fe8b
feat(core): pre-flight context budget trimming for Anthropic and OpenAI
netbrah Mar 21, 2026
0e1f243
robustness(core): enforce history immutability with readonly types
netbrah Mar 21, 2026
45e30be
fix(core): add retry logic for transient SSL/TLS and network errors
netbrah Mar 21, 2026
9677e84
feat(core): resilient subagent tool rejection with contextual feedback
netbrah Mar 21, 2026
4d399a8
feat(tools): add omission placeholder detector for edit and write
netbrah Mar 21, 2026
91f731b
feat(mcp): add readOnlyTools config for MCP servers
netbrah Mar 21, 2026
c774db2
feat(core): upstream port bundle — read_many_files, JIT context, work…
netbrah Mar 21, 2026
cdf925e
feat(core): context management and tool execution enhancements
netbrah Mar 21, 2026
f493c6a
feat(core): OpenAI Responses API (/v1/responses) native support
netbrah Mar 22, 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
Empty file added .bin/.gitkeep
Empty file.
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,16 @@ integration-tests/terminal-capture/scenarios/screenshots/
# storybook
*storybook.log
storybook-static

# SEA binary build artifacts
.bin/*
!.bin/.gitkeep
sea-config.json

# Local scratch files
*.md.bak
tmp*.md
CONTAP-*.md
RCAs/
AGENT-PROMPTS.md
CONTRIBUTION-ROADMAP.md
19 changes: 19 additions & 0 deletions Dockerfile.sea
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Dockerfile.sea — Build qwen-code SEA binary for linux/amd64
#
# Usage:
# docker buildx build --platform linux/amd64 -f Dockerfile.sea -o type=local,dest=.bin/ .
#
# Output: .bin/qwen-code-linux-amd64
FROM docker.repo.eng.netapp.com/node:22.15.0 AS builder

WORKDIR /build

COPY . .

RUN npm ci --ignore-scripts
RUN npm run build
RUN npm run bundle
RUN BUNDLE_NATIVE_MODULES=false node scripts/build_binary.js

FROM scratch AS output
COPY --from=builder /build/dist/binary/linux-x64/qwen-code /qwen-code-linux-amd64
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"build-and-start": "npm run build && npm run start",
"build:vscode": "node scripts/build_vscode_companion.js",
"build:all": "npm run build && npm run build:sandbox && npm run build:vscode",
"build:sea": "docker buildx build --platform linux/amd64 -f Dockerfile.sea -o type=local,dest=.bin/ .",
"relink": "npm run build && npm run bundle && npm link",
"deploy": "npm run build && npm run build:sea && npm run deploy:remote",
"deploy:remote": "scp .bin/qwen-code-linux-amd64 ${QWEN_DEPLOY_HOST:-curosr}:~/bin/qwen",
"build:packages": "npm run build --workspaces",
"build:sandbox": "node scripts/build_sandbox.js",
"bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/config/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { t } from '../i18n/index.js';
*/
const DEFAULT_ENV_KEYS: Record<string, string> = {
[AuthType.USE_OPENAI]: 'OPENAI_API_KEY',
[AuthType.USE_OPENAI_RESPONSES]: 'OPENAI_API_KEY',
[AuthType.USE_ANTHROPIC]: 'ANTHROPIC_API_KEY',
[AuthType.USE_GEMINI]: 'GEMINI_API_KEY',
[AuthType.USE_VERTEX_AI]: 'GOOGLE_API_KEY',
Expand Down Expand Up @@ -142,7 +143,10 @@ export function validateAuthMethod(
const settings = loadSettings();
loadEnvironment(settings.merged);

if (authMethod === AuthType.USE_OPENAI) {
if (
authMethod === AuthType.USE_OPENAI ||
authMethod === AuthType.USE_OPENAI_RESPONSES
) {
const { hasKey, checkedEnvKey, isExplicitEnvKey } = hasApiKeyForAuth(
authMethod,
settings.merged,
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ export async function parseArguments(): Promise<CliArgs> {
type: 'string',
choices: [
AuthType.USE_OPENAI,
AuthType.USE_OPENAI_RESPONSES,
AuthType.USE_ANTHROPIC,
AuthType.QWEN_OAUTH,
AuthType.USE_GEMINI,
Expand Down Expand Up @@ -1117,6 +1118,7 @@ export async function loadCliConfig(
skipStartupContext: settings.model?.skipStartupContext ?? false,
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
truncateToolOutputLines: settings.tools?.truncateToolOutputLines,
toolOutputMasking: settings.experimental?.toolOutputMasking,
eventEmitter: appEvents,
gitCoAuthor: settings.general?.gitCoAuthor,
output: {
Expand Down
7 changes: 3 additions & 4 deletions packages/cli/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,10 @@ function findEnvFile(settings: Settings, startDir: string): string | null {
const homeDir = homedir();
const isTrusted = isWorkspaceTrusted(settings).isTrusted;

// Pre-compute user-level .env paths for fast comparison
const globalQwenDir = Storage.getGlobalQwenDir();
const userLevelPaths = new Set([
path.normalize(path.join(homeDir, '.env')),
path.normalize(path.join(homeDir, QWEN_DIR, '.env')),
path.normalize(path.join(globalQwenDir, '.env')),
]);

// Determine if we can use this .env file based on trust settings
Expand All @@ -437,8 +437,7 @@ function findEnvFile(settings: Settings, startDir: string): string | null {

const parentDir = path.dirname(currentDir);
if (parentDir === currentDir || !parentDir) {
// At home directory - check fallback .env files
const homeGeminiEnvPath = path.join(homeDir, QWEN_DIR, '.env');
const homeGeminiEnvPath = path.join(globalQwenDir, '.env');
if (fs.existsSync(homeGeminiEnvPath)) {
return homeGeminiEnvPath;
}
Expand Down
54 changes: 53 additions & 1 deletion packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1586,7 +1586,59 @@ const SETTINGS_SCHEMA = {
default: {},
description: 'Setting to enable experimental features',
showInDialog: false,
properties: {},
properties: {
toolOutputMasking: {
type: 'object',
label: 'Tool Output Masking',
category: 'Experimental',
requiresRestart: true,
default: {},
description:
'Mask older bulky tool outputs to save context (full output kept on disk).',
showInDialog: false,
properties: {
enabled: {
type: 'boolean',
label: 'Enable Tool Output Masking',
category: 'Experimental',
requiresRestart: true,
default: true,
description: 'Enables tool output masking to save tokens.',
showInDialog: false,
},
toolProtectionThreshold: {
type: 'number',
label: 'Tool Protection Threshold',
category: 'Experimental',
requiresRestart: true,
default: 50_000,
description:
'Token budget of the newest tool outputs to keep unmasked.',
showInDialog: false,
},
minPrunableTokensThreshold: {
type: 'number',
label: 'Min Prunable Tokens Threshold',
category: 'Experimental',
requiresRestart: true,
default: 30_000,
description:
'Minimum prunable tokens required before a masking pass runs.',
showInDialog: false,
},
protectLatestTurn: {
type: 'boolean',
label: 'Protect Latest Turn',
category: 'Experimental',
requiresRestart: true,
default: true,
description:
'When true, the most recent conversation turn is never masked.',
showInDialog: false,
},
},
},
},
},
} as const satisfies SettingsSchema;

Expand Down
31 changes: 25 additions & 6 deletions packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,27 @@ export function validateDnsResolutionOrder(
return defaultValue;
}

let _isSEA: boolean | undefined;
function isSEABinary(): boolean {
if (_isSEA !== undefined) return _isSEA;
try {
// node:sea exists only in Single Executable Application builds.
// eslint-disable-next-line @typescript-eslint/no-require-imports, no-restricted-syntax
const sea = require('node:sea') as { isSea?: () => boolean };
_isSEA = typeof sea.isSea === 'function' && sea.isSea();
} catch {
_isSEA = false;
}
return _isSEA;
}

function getNodeMemoryArgs(isDebugMode: boolean): string[] {
// SEA binaries can't be relaunched with V8 flags via argv — they'd be
// parsed as CLI arguments by yargs. Skip memory tuning entirely.
if (isSEABinary() || process.env['QWEN_CODE_NO_RELAUNCH']) {
return [];
}

const totalMemoryMB = os.totalmem() / (1024 * 1024);
const heapStats = v8.getHeapStatistics();
const currentMaxOldSpaceSizeMb = Math.floor(
Expand All @@ -96,10 +116,6 @@ function getNodeMemoryArgs(isDebugMode: boolean): string[] {
);
}

if (process.env['QWEN_CODE_NO_RELAUNCH']) {
return [];
}

if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) {
if (isDebugMode) {
writeStderrLine(
Expand Down Expand Up @@ -243,8 +259,11 @@ export async function main() {
}
}

// hop into sandbox if we are outside and sandboxing is enabled
if (!process.env['SANDBOX']) {
// hop into sandbox if we are outside and sandboxing is enabled.
// SEA binaries skip this entirely — relaunchAppInChildProcess passes
// process.argv[1] (undefined in SEA) as a positional arg, which yargs
// treats as a one-shot prompt, breaking interactive mode.
if (!process.env['SANDBOX'] && !isSEABinary()) {
const memoryArgs = settings.merged.advanced?.autoConfigureMemory
? getNodeMemoryArgs(isDebugMode)
: [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export class SystemController extends BaseController {
undefined, // authProviderType
undefined, // targetAudience
undefined, // targetServiceAccount
undefined, // readOnlyTools
'sdk', // type
);
}
Expand Down Expand Up @@ -254,6 +255,7 @@ export class SystemController extends BaseController {
authProvider,
config.targetAudience,
config.targetServiceAccount,
config.readOnlyTools,
);
}

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/nonInteractive/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ export interface CLIMcpServerConfig {
| 'service_account_impersonation';
targetAudience?: string;
targetServiceAccount?: string;
readOnlyTools?: boolean;
}

export interface CLIControlInitializeRequest {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ui/commands/arenaCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ function executeArenaCommand(
let chatHistory;
try {
const fullHistory = config.getGeminiClient().getHistory();
chatHistory = stripStartupContext(fullHistory);
chatHistory = [...stripStartupContext(fullHistory)];
} catch {
debugLogger.debug('Could not retrieve chat history for arena agents');
}
Expand Down
18 changes: 17 additions & 1 deletion packages/cli/src/ui/components/AsciiArt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,27 @@
* SPDX-License-Identifier: Apache-2.0
*/

export const shortAsciiLogo = `
const qwenAsciiLogo = `
▄▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄▄▄▄▄ ▄▄▄ ▄▄
██╔═══██╗██║ ██║██╔════╝████╗ ██║
██║ ██║██║ █╗ ██║█████╗ ██╔██╗ ██║
██║▄▄ ██║██║███╗██║██╔══╝ ██║╚██╗██║
╚██████╔╝╚███╔███╔╝███████╗██║ ╚████║
╚══▀▀═╝ ╚══╝╚══╝ ╚══════╝╚═╝ ╚═══╝
`;

const apexAsciiLogo = `
█████╗ ██████╗ ███████╗██╗ ██╗
██╔══██╗██╔══██╗██╔════╝╚██╗██╔╝
███████║██████╔╝█████╗ ╚███╔╝
██╔══██║██╔═══╝ ██╔══╝ ██╔██╗
██║ ██║██║ ███████╗██╔╝ ██╗
╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝
`;

const brandLogos: Record<string, string> = {
APEX: apexAsciiLogo,
};

export const shortAsciiLogo =
brandLogos[process.env['QWEN_CODE_BRAND'] ?? ''] ?? qwenAsciiLogo;
4 changes: 2 additions & 2 deletions packages/cli/src/ui/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,10 @@ export const Header: React.FC<HeaderProps> = ({
flexGrow={showLogo ? 0 : 1}
width={showLogo ? availableInfoPanelWidth : undefined}
>
{/* Title line: >_ Qwen Code (v{version}) */}
{/* Title line: >_ Brand (v{version}) */}
<Text>
<Text bold color={theme.text.accent}>
&gt;_ Qwen Code
&gt;_ {process.env['QWEN_CODE_BRAND'] || 'Qwen Code'}
</Text>
<Text color={theme.text.secondary}> (v{version})</Text>
</Text>
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/utils/systemInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export async function getExtendedSystemInfo(
// Get base URL and apiKeyEnvKey if using OpenAI or Anthropic auth
const contentGeneratorConfig =
baseInfo.selectedAuthType === AuthType.USE_OPENAI ||
baseInfo.selectedAuthType === AuthType.USE_OPENAI_RESPONSES ||
baseInfo.selectedAuthType === AuthType.USE_ANTHROPIC
? context.services.config?.getContentGeneratorConfig()
: undefined;
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/utils/windowTitle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
* @returns The computed window title, either from CLI_TITLE environment variable or the default Gemini title
*/
export function computeWindowTitle(folderName: string): string {
const title = process.env['CLI_TITLE'] || `Qwen - ${folderName}`;
const brand = process.env['QWEN_CODE_BRAND'] || 'Qwen';
const title = process.env['CLI_TITLE'] || `${brand} - ${folderName}`;

// Remove control characters that could cause issues in terminal titles
return title.replace(
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/agents/runtime/agent-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,8 @@ export class AgentCore {

if (!allowedToolNames.has(fc.name)) {
const toolName = String(fc.name);
const errorMessage = `Tool "${toolName}" not found. Tools must use the exact names provided.`;
const availableToolsList = Array.from(allowedToolNames).join(', ');
const errorMessage = `Tool "${toolName}" is not available. You must acknowledge this, rethink your strategy, and use only the available tools: [${availableToolsList}]. Do NOT retry with the same tool name.`;

// Emit TOOL_CALL event for visibility
this.eventEmitter?.emit(AgentEventType.TOOL_CALL, {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/agents/runtime/agent-headless.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1184,7 +1184,7 @@ describe('subagent.ts', () => {
const editResult = toolResultEvents.find((e) => e.name === 'edit_file');
expect(editResult).toBeDefined();
expect(editResult!.success).toBe(false);
expect(editResult!.error).toContain('not found');
expect(editResult!.error).toContain('not available');
expect(editResult!.callId).toBe('call_edit');

// 5. Verify allowed tool result has success=true
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,7 @@ describe('Server Config (config.ts)', () => {
describe('getTruncateToolOutputThreshold', () => {
it('should return the default threshold', () => {
const config = new Config(baseParams);
expect(config.getTruncateToolOutputThreshold()).toBe(25_000);
expect(config.getTruncateToolOutputThreshold()).toBe(80_000);
});

it('should use a custom truncateToolOutputThreshold if provided', () => {
Expand Down
Loading
Loading