mirror of
https://github.com/stablyai/orca.git
synced 2026-10-06 16:02:25 +00:00
Register supervised Qoder China and Qwen Code (#24616)
* Add Qoder session history and search with real CLI coverage * Allow the real Qoder marker file to end with a newline * Keep Qoder tool output out of history previews and search * Keep Qoder search pages readable by older clients * Verify persisted Qoder history after a real generated and resumed task * Negotiate Qoder filters before searching an older execution host * Combine search client imports for the CI plugin gate * Keep the relay search oracle aligned with legacy agent filtering * Register supervised Qoder China and Qwen lifecycle integration * Cover Qoder China mobile assets and mixed-host resume gates * Verify Qoder provider tags against the older released wire parser * Verify China and Qwen keep independent Windows hook scripts * Verify Qoder registrations against the installed older Windows release * test(qoder): align search capability contracts and pin old-host fencing * fix(qoder): rank exact picker identities and command aliases first * test(qoder): preserve the regional CLI shared icon expectation Keep the full bundled-asset and no-remote-image checks, with an explicit shared-logo basename for Qoder China. The map also works with older catalog type unions. * fix(qoder): align China catalog entry with fallback order --------- Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>
This commit is contained in:
co-authored by
Orca Integration Recovery
parent
08ee7ba9ef
commit
843607b1bc
@@ -9,6 +9,7 @@
|
||||
"../src/main/claude/claude-hook-event-versions.ts",
|
||||
"../src/main/claude/claude-managed-hook-events.ts",
|
||||
"../src/main/qoder/hook-service.ts",
|
||||
"../src/main/qwen-code/hook-service.ts",
|
||||
"../src/main/codebuddy/hook-service.ts",
|
||||
"../src/main/agent-hooks/hook-stdin-contract.ts",
|
||||
"../src/main/agent-hooks/hook-post-command.ts",
|
||||
|
||||
@@ -20,6 +20,7 @@ export const MOBILE_AGENT_ICON_ASSETS: Partial<Record<TuiAgent, ImageSourcePropT
|
||||
'prime-agent': require('../../../src/shared/agent-icons/prime-agent.png'),
|
||||
codebuddy: require('../../../src/shared/agent-icons/codebuddy.png'),
|
||||
qoder: require('../../../src/shared/agent-icons/qoder.png'),
|
||||
'qoder-cn': require('../../../src/shared/agent-icons/qoder-cn.png'),
|
||||
gemini: require('../../../src/shared/agent-icons/gemini.png'),
|
||||
antigravity: require('../../../src/shared/agent-icons/antigravity.png'),
|
||||
goose: require('../../../src/shared/agent-icons/goose.png'),
|
||||
|
||||
@@ -28,6 +28,7 @@ export const MOBILE_TUI_AGENT_FAVICON_DOMAINS: Partial<Record<TuiAgent, string>>
|
||||
omp: 'omp.sh',
|
||||
'prime-agent': 'primeintellect.ai',
|
||||
qoder: 'qoder.com',
|
||||
'qoder-cn': 'qoder.cn',
|
||||
gemini: 'gemini.google.com',
|
||||
antigravity: 'antigravity.google',
|
||||
goose: 'goose-docs.ai',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { codebuddyHookService } from '../codebuddy/hook-service'
|
||||
import { qoderHookService } from '../qoder/hook-service'
|
||||
import { qwenCodeHookService } from '../qwen-code/hook-service'
|
||||
import { qoderCnHookService, qoderHookService } from '../qoder/hook-service'
|
||||
import type { AgentHookInstallStatus } from '../../shared/agent-hook-types'
|
||||
import type { HookInstallAgent } from '../../shared/telemetry-events'
|
||||
import { ampHookService } from '../amp/hook-service'
|
||||
@@ -47,6 +48,8 @@ export const MANAGED_AGENT_HOOK_INSTALLERS: readonly ManagedAgentHookInstaller[]
|
||||
['codex', () => codexHookService.install()],
|
||||
['gemini', () => geminiHookService.install()],
|
||||
['qoder', () => qoderHookService.install()],
|
||||
['qoder-cn', () => qoderCnHookService.install()],
|
||||
['qwen-code', () => qwenCodeHookService.install()],
|
||||
['codebuddy', () => codebuddyHookService.install()],
|
||||
['antigravity', () => antigravityHookService.install()],
|
||||
['amp', () => ampHookService.install()],
|
||||
@@ -75,6 +78,8 @@ export const MANAGED_AGENT_HOOK_SCRIPT_REFRESHERS: readonly ManagedAgentHookScri
|
||||
['codex', () => codexHookService.refreshManagedScripts()],
|
||||
['gemini', () => geminiHookService.refreshManagedScripts()],
|
||||
['qoder', () => qoderHookService.refreshManagedScripts()],
|
||||
['qoder-cn', () => qoderCnHookService.refreshManagedScripts()],
|
||||
['qwen-code', () => qwenCodeHookService.refreshManagedScripts()],
|
||||
['codebuddy', () => codebuddyHookService.refreshManagedScripts()],
|
||||
['antigravity', () => antigravityHookService.refreshManagedScripts()],
|
||||
['cursor', () => cursorHookService.refreshManagedScripts()],
|
||||
@@ -95,6 +100,8 @@ export const MANAGED_AGENT_HOOK_REMOVERS: readonly ManagedAgentHookRemover[] = [
|
||||
['codex', () => codexHookService.remove()],
|
||||
['gemini', () => geminiHookService.remove()],
|
||||
['qoder', () => qoderHookService.remove()],
|
||||
['qoder-cn', () => qoderCnHookService.remove()],
|
||||
['qwen-code', () => qwenCodeHookService.remove()],
|
||||
['codebuddy', () => codebuddyHookService.remove()],
|
||||
['antigravity', () => antigravityHookService.remove()],
|
||||
['amp', () => ampHookService.remove()],
|
||||
@@ -121,6 +128,8 @@ export const MANAGED_AGENT_HOOK_STATUS_READERS: readonly ManagedAgentHookStatusR
|
||||
['codex', () => codexHookService.getStatus()],
|
||||
['gemini', () => geminiHookService.getStatus()],
|
||||
['qoder', () => qoderHookService.getStatus()],
|
||||
['qoder-cn', () => qoderCnHookService.getStatus()],
|
||||
['qwen-code', () => qwenCodeHookService.getStatus()],
|
||||
['codebuddy', () => codebuddyHookService.getStatus()],
|
||||
['antigravity', () => antigravityHookService.getStatus()],
|
||||
['amp', () => ampHookService.getStatus()],
|
||||
|
||||
@@ -92,6 +92,34 @@ const buildersByAgent = new Map<string, CommandBuilders>([
|
||||
remote: (path) => [getClaudeRemoteCommand(path)]
|
||||
}
|
||||
],
|
||||
[
|
||||
'qoder-cn',
|
||||
{
|
||||
local: (path) => [
|
||||
getManagedLifecycleHook(path, {
|
||||
configDirName: '.qoder-cn',
|
||||
scriptBaseName: 'qoder-cn-hook',
|
||||
usesWindowsCompatLauncher: true,
|
||||
windowsHookShell: 'powershell'
|
||||
}).command
|
||||
],
|
||||
remote: (path) => [getClaudeRemoteCommand(path)]
|
||||
}
|
||||
],
|
||||
[
|
||||
'qwen-code',
|
||||
{
|
||||
local: (path) => [
|
||||
getManagedLifecycleHook(path, {
|
||||
configDirName: '.qwen',
|
||||
scriptBaseName: 'qwen-code-hook',
|
||||
usesWindowsCompatLauncher: true,
|
||||
windowsHookShell: 'powershell'
|
||||
}).command
|
||||
],
|
||||
remote: (path) => [getClaudeRemoteCommand(path)]
|
||||
}
|
||||
],
|
||||
[
|
||||
'codebuddy',
|
||||
{
|
||||
@@ -233,7 +261,10 @@ describe('managed hook command contract', () => {
|
||||
// Native PowerShell hooks evaluate these variables without Grok's dollar-byte scanner.
|
||||
const scannedCommand =
|
||||
platform === 'win32' &&
|
||||
(agent === 'qoder' || agent === 'codebuddy') &&
|
||||
(agent === 'qoder' ||
|
||||
agent === 'qoder-cn' ||
|
||||
agent === 'qwen-code' ||
|
||||
agent === 'codebuddy') &&
|
||||
command.startsWith('$scriptPath = Join-Path')
|
||||
? command
|
||||
.replaceAll('$LASTEXITCODE', '')
|
||||
|
||||
@@ -23,7 +23,8 @@ import { kimiHookService } from '../kimi/hook-service'
|
||||
import { dshHookService } from '../dsh/hook-service'
|
||||
import { museHookService } from '../muse/hook-service'
|
||||
import { openClaudeHookService } from '../openclaude/hook-service'
|
||||
import { qoderHookService } from '../qoder/hook-service'
|
||||
import { qwenCodeHookService } from '../qwen-code/hook-service'
|
||||
import { qoderCnHookService, qoderHookService } from '../qoder/hook-service'
|
||||
import { zcodeHookService } from '../zcode/hook-service'
|
||||
import { MANAGED_AGENT_HOOK_INSTALLERS } from './managed-agent-hook-controls'
|
||||
import { REMOTE_MANAGED_HOOK_INSTALLER_AGENTS } from './remote-managed-hook-installers'
|
||||
@@ -54,6 +55,8 @@ describe('remote hook service registry coverage', () => {
|
||||
['kimi', kimiHookService],
|
||||
['muse', museHookService],
|
||||
['qoder', qoderHookService],
|
||||
['qoder-cn', qoderCnHookService],
|
||||
['qwen-code', qwenCodeHookService],
|
||||
['codebuddy', codebuddyHookService],
|
||||
['zcode', zcodeHookService],
|
||||
['dsh', dshHookService]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { codebuddyHookService } from '../codebuddy/hook-service'
|
||||
import { qoderHookService } from '../qoder/hook-service'
|
||||
import { qwenCodeHookService } from '../qwen-code/hook-service'
|
||||
import { qoderCnHookService, qoderHookService } from '../qoder/hook-service'
|
||||
import type { SFTPWrapper } from 'ssh2'
|
||||
import type { AgentHookInstallStatus, AgentHookTarget } from '../../shared/agent-hook-types'
|
||||
import { ampHookService } from '../amp/hook-service'
|
||||
@@ -65,6 +66,8 @@ const REMOTE_MANAGED_HOOK_INSTALLERS: readonly RemoteManagedHookInstaller[] = [
|
||||
],
|
||||
['gemini', (sftp, remoteHome) => geminiHookService.installRemote(sftp, remoteHome)],
|
||||
['qoder', (sftp, remoteHome) => qoderHookService.installRemote(sftp, remoteHome)],
|
||||
['qoder-cn', (sftp, remoteHome) => qoderCnHookService.installRemote(sftp, remoteHome)],
|
||||
['qwen-code', (sftp, remoteHome) => qwenCodeHookService.installRemote(sftp, remoteHome)],
|
||||
['codebuddy', (sftp, remoteHome) => codebuddyHookService.installRemote(sftp, remoteHome)],
|
||||
['antigravity', (sftp, remoteHome) => antigravityHookService.installRemote(sftp, remoteHome)],
|
||||
['amp', (sftp, remoteHome) => ampHookService.installRemote(sftp, remoteHome)],
|
||||
|
||||
@@ -23,6 +23,8 @@ afterEach(() => vi.restoreAllMocks())
|
||||
* mimo-code's boundary is an explicit-prompt MessagePart, which the gate handles separately. */
|
||||
const NEW_TURN_EVENT: Record<AgentHookSource, string | null> = {
|
||||
qoder: 'SessionStart',
|
||||
'qoder-cn': 'SessionStart',
|
||||
'qwen-code': 'SessionStart',
|
||||
claude: 'SessionStart',
|
||||
kimi: 'UserPromptSubmit',
|
||||
codebuddy: 'UserPromptSubmit',
|
||||
|
||||
@@ -47,6 +47,7 @@ describe('applyAgentWorkspaceTrustToSpawn', () => {
|
||||
'cursor',
|
||||
'copilot',
|
||||
'qoder',
|
||||
'qoder-cn',
|
||||
'antigravity'
|
||||
])
|
||||
)
|
||||
|
||||
@@ -18,8 +18,14 @@ import { wrapWindowsDirectCmdHookCommand } from '../agent-hooks/windows-direct-c
|
||||
import type { ClaudeManagedHookPlan } from './claude-managed-hook-events'
|
||||
|
||||
export type ClaudeCompatibleHookSettings = {
|
||||
configDirName: '.claude' | '.openclaude' | '.qoder' | '.codebuddy'
|
||||
scriptBaseName: 'claude-hook' | 'openclaude-hook' | 'qoder-hook' | 'codebuddy-hook'
|
||||
configDirName: '.claude' | '.openclaude' | '.qoder' | '.qoder-cn' | '.qwen' | '.codebuddy'
|
||||
scriptBaseName:
|
||||
| 'claude-hook'
|
||||
| 'openclaude-hook'
|
||||
| 'qoder-hook'
|
||||
| 'qoder-cn-hook'
|
||||
| 'qwen-code-hook'
|
||||
| 'codebuddy-hook'
|
||||
usesWindowsCompatLauncher: boolean
|
||||
windowsHookShell?: 'powershell'
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import { ClaudeHookService } from './hook-service'
|
||||
import { getWindowsManagedLifecycleHook } from './hook-settings'
|
||||
import { getWindowsClaudeHookEntry, getWindowsClaudeHookPayloadPath } from './windows-hook-files'
|
||||
import { codebuddyHookService } from '../codebuddy/hook-service'
|
||||
import { qoderHookService } from '../qoder/hook-service'
|
||||
import { qoderHookService, qoderCnHookService } from '../qoder/hook-service'
|
||||
import { qwenCodeHookService } from '../qwen-code/hook-service'
|
||||
import { openClaudeHookService } from '../openclaude/hook-service'
|
||||
|
||||
const { home } = vi.hoisted(() => ({ home: { path: '' } }))
|
||||
@@ -143,15 +144,16 @@ describe('Windows Claude hook files', () => {
|
||||
it('keeps compatible agents on their existing single-file scripts', async () => {
|
||||
for (const [name, compatible] of [
|
||||
['qoder', qoderHookService],
|
||||
['qoder-cn', qoderCnHookService],
|
||||
['qwen-code', qwenCodeHookService],
|
||||
['codebuddy', codebuddyHookService],
|
||||
['openclaude', openClaudeHookService]
|
||||
] as const) {
|
||||
expect(compatible.install().state).toBe('installed')
|
||||
await compatible.refreshManagedScripts()
|
||||
const path = join(home.path, '.orca', 'agent-hooks', `${name}-hook.cmd`)
|
||||
expect(readFileSync(path, 'utf8')).toContain(
|
||||
`/hook/${name === 'openclaude' ? 'claude' : name}`
|
||||
)
|
||||
const source = name === 'openclaude' ? 'claude' : name
|
||||
expect(readFileSync(path, 'utf8')).toContain(`/hook/${source}`)
|
||||
expect(readFileSync(path, 'utf8')).not.toContain('claude-hook-impl.cmd')
|
||||
expect(existsSync(join(home.path, '.orca', 'agent-hooks', `${name}-hook-impl.cmd`))).toBe(
|
||||
false
|
||||
|
||||
@@ -52,6 +52,7 @@ export const AGENT_TRUST_INHERITS_FROM_A_HOME: Record<AgentTrustPreset, boolean>
|
||||
cursor: false,
|
||||
copilot: true,
|
||||
qoder: true,
|
||||
'qoder-cn': true,
|
||||
antigravity: false
|
||||
}
|
||||
|
||||
@@ -66,6 +67,7 @@ export const AGENT_TRUST_KEYED_BY_START_FOLDER: Record<AgentTrustPreset, boolean
|
||||
cursor: false,
|
||||
copilot: false,
|
||||
qoder: false,
|
||||
'qoder-cn': false,
|
||||
antigravity: false
|
||||
}
|
||||
|
||||
@@ -106,6 +108,8 @@ async function writePreset(
|
||||
return markCursorWorkspaceTrusted(storedPath, host.agentHome)
|
||||
case 'copilot':
|
||||
return markCopilotFolderTrusted(storedPath, host.agentHome)
|
||||
case 'qoder-cn':
|
||||
return markQoderWorkspaceTrusted(storedPath, host.agentHome, '.qoder-cn')
|
||||
case 'qoder':
|
||||
return markQoderWorkspaceTrusted(storedPath, host.agentHome)
|
||||
case 'antigravity':
|
||||
|
||||
@@ -11,8 +11,9 @@ vi.mock('node:os', async (importOriginal) => ({
|
||||
vi.mock('electron', () => ({ app: { getPath: () => sandbox.home } }))
|
||||
import { getManagedLifecycleHook, hasSameManagedHookInvocation } from '../claude/hook-settings'
|
||||
import { createManagedCommandMatcher } from '../agent-hooks/installer-utils'
|
||||
import { qoderHookService, QODER_HOOK_EVENTS } from './hook-service'
|
||||
import { qoderCnHookService, qoderHookService, QODER_HOOK_EVENTS } from './hook-service'
|
||||
import { markQoderWorkspaceTrusted, withQoderTrustedWorkspace } from './workspace-trust'
|
||||
import { qwenCodeHookService, QWEN_CODE_HOOK_EVENTS } from '../qwen-code/hook-service'
|
||||
|
||||
beforeAll(() => {
|
||||
sandbox.home = mkdtempSync(join(tmpdir(), 'orca-qoder-test-'))
|
||||
@@ -96,3 +97,52 @@ describe('Qoder Windows hook shell', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
service: qoderCnHookService,
|
||||
directory: '.qoder-cn',
|
||||
source: 'qoder-cn',
|
||||
events: QODER_HOOK_EVENTS
|
||||
},
|
||||
{
|
||||
service: qwenCodeHookService,
|
||||
directory: '.qwen',
|
||||
source: 'qwen-code',
|
||||
events: QWEN_CODE_HOOK_EVENTS
|
||||
}
|
||||
])(
|
||||
'installs and removes $source hooks without changing user settings',
|
||||
({ service, directory, source, events }) => {
|
||||
const configDir = join(sandbox.home, directory)
|
||||
mkdirSync(configDir, { recursive: true })
|
||||
const path = join(configDir, 'settings.json')
|
||||
const userHook = { matcher: '', hooks: [{ type: 'command', command: 'echo user-owned' }] }
|
||||
writeFileSync(
|
||||
path,
|
||||
JSON.stringify({
|
||||
model: 'custom',
|
||||
statusLine: { command: 'user-status' },
|
||||
hooks: { Stop: [userHook] }
|
||||
})
|
||||
)
|
||||
expect(service.install().state).toBe('installed')
|
||||
const installed = JSON.parse(readFileSync(path, 'utf8'))
|
||||
expect(Object.keys(installed.hooks).sort()).toEqual([...events].sort())
|
||||
expect(installed.statusLine).toEqual({ command: 'user-status' })
|
||||
expect(
|
||||
readFileSync(join(sandbox.home, '.orca', 'agent-hooks', `${source}-hook.sh`), 'utf8')
|
||||
).toContain(`/hook/${source}`)
|
||||
if (source === 'qoder-cn') {
|
||||
markQoderWorkspaceTrusted('/cn-workspace', sandbox.home, '.qoder-cn')
|
||||
expect(JSON.parse(readFileSync(path, 'utf8')).permissions.trustDirectories).toEqual([
|
||||
'/cn-workspace'
|
||||
])
|
||||
}
|
||||
expect(service.remove().state).toBe('not_installed')
|
||||
expect(JSON.parse(readFileSync(path, 'utf8'))).toMatchObject({
|
||||
model: 'custom',
|
||||
hooks: { Stop: [userHook] }
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -36,3 +36,16 @@ export const qoderHookService = new ClaudeHookService({
|
||||
},
|
||||
hookPlan: QODER_MANAGED_HOOK_PLAN
|
||||
})
|
||||
|
||||
export const qoderCnHookService = new ClaudeHookService({
|
||||
agent: 'qoder-cn',
|
||||
source: 'qoder-cn',
|
||||
displayName: 'Qoder CLI China',
|
||||
settings: {
|
||||
configDirName: '.qoder-cn',
|
||||
scriptBaseName: 'qoder-cn-hook',
|
||||
usesWindowsCompatLauncher: true,
|
||||
windowsHookShell: 'powershell'
|
||||
},
|
||||
hookPlan: QODER_MANAGED_HOOK_PLAN
|
||||
})
|
||||
|
||||
@@ -24,14 +24,18 @@ export function withQoderTrustedWorkspace(
|
||||
}
|
||||
}
|
||||
|
||||
export function markQoderWorkspaceTrusted(workspacePath: string, home: string): void {
|
||||
export function markQoderWorkspaceTrusted(
|
||||
workspacePath: string,
|
||||
home: string,
|
||||
configDirName: '.qoder' | '.qoder-cn' = '.qoder'
|
||||
): void {
|
||||
let canonicalPath = workspacePath
|
||||
try {
|
||||
canonicalPath = realpathSync.native(workspacePath)
|
||||
} catch {
|
||||
/* Keep the supplied path when absent. */
|
||||
}
|
||||
const configPath = join(home, '.qoder', 'settings.json')
|
||||
const configPath = join(home, configDirName, 'settings.json')
|
||||
const config = readHooksJson(configPath)
|
||||
if (!config) {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ClaudeHookService } from '../claude/hook-service'
|
||||
import type { ClaudeManagedHookPlan } from '../claude/claude-managed-hook-events'
|
||||
|
||||
// Qwen's hook enum is authoritative; Claude's version table and statusline do not apply.
|
||||
export const QWEN_CODE_HOOK_EVENTS = [
|
||||
'SessionStart',
|
||||
'SessionEnd',
|
||||
'UserPromptSubmit',
|
||||
'PreToolUse',
|
||||
'PostToolUse',
|
||||
'PostToolUseFailure',
|
||||
'PermissionRequest',
|
||||
'Stop',
|
||||
'StopFailure',
|
||||
'Notification',
|
||||
'SubagentStart',
|
||||
'SubagentStop'
|
||||
] as const
|
||||
|
||||
export const QWEN_CODE_MANAGED_HOOK_PLAN: ClaudeManagedHookPlan = {
|
||||
install: QWEN_CODE_HOOK_EVENTS.map((eventName) => ({ eventName, definition: {} })),
|
||||
retire: [],
|
||||
statusLine: 'leave'
|
||||
}
|
||||
|
||||
export const qwenCodeHookService = new ClaudeHookService({
|
||||
agent: 'qwen-code',
|
||||
source: 'qwen-code',
|
||||
displayName: 'Qwen Code',
|
||||
settings: {
|
||||
configDirName: '.qwen',
|
||||
scriptBaseName: 'qwen-code-hook',
|
||||
usesWindowsCompatLauncher: true,
|
||||
windowsHookShell: 'powershell'
|
||||
},
|
||||
hookPlan: QWEN_CODE_MANAGED_HOOK_PLAN
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"capturedAt": "2026-10-02T07:27:56.152Z",
|
||||
"platform": "darwin",
|
||||
"command": [
|
||||
"qoderclicn",
|
||||
"--config-dir",
|
||||
"/tmp/orca-qoder-validation-1002/cn-config",
|
||||
"--cwd",
|
||||
"/tmp/orca-qoder-validation-1002/workspace"
|
||||
],
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"note": "Qoder CLI China 1.1.65; accepted trust in isolated config; sign-in dialog; no China account",
|
||||
"exitCode": 129
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
[8m[?u]11;?\[>q[>4;?m[c[2K␍[0m]0;◇ Qoder CLI CN | Ready ]8;;[0m7[r8[J␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
[H[?25h[?25l[?1006l[?1002l[?1004h]0;◇ Qoder CLI CN | Ready [?2004h[?2004h[?1004h[0m␍
|
||||
[2G[38;5;78m [39m[4G[38;5;78m██████[39m[39G╭─[42G[38;5;78m[1mTips[22m[39m[47G[38;5;78m[1mfor[22m[39m[51G[38;5;78m[1mgetting[22m[39m[59G[38;5;78m[1mstarted[22m[39m[67G───────────────────────────────────╮␍
|
||||
[2G[38;5;78m██[39m[10G[38;5;78m██[39m[39G│[41G1.[44GUse[48GAGENTS.md[58Gfiles[64Gto[67Gcustomize[77Ginteractions[102G│␍
|
||||
[2G[38;5;78m██[39m[6G[38;5;78m██[39m[10G[38;5;78m██[39m[14G[1mQoder[22m[20G[1mCLI[22m[24G[48;5;78m[38;5;16m[1m CN [22m[39m[49m[29Gv1.1.65[39G│[41G2.[44G/help[50Gfor[54Gmore[59Ginformation[102G│␍
|
||||
[2G[38;5;78m██[39m[8G[38;5;78m██[39m[39G│[41G3.[44GAsk[48Gcoding[55Gquestions,[66Gedit[71Gcode[76Gor[79Grun[83Gcommands[102G│␍
|
||||
[2G[38;5;78m [39m[4G[38;5;78m████[39m[10G[38;5;78m██[39m[14G[1mNot[22m[18G[1mLogin[22m[24GPlease[31GAuth[39G│[41G4.[44GBe[47Gspecific[56Gfor[60Gthe[64Gbest[69Gresults[102G│␍
|
||||
[39G╰──────────────────────────────────────────────────────────────╯␍
|
||||
␍
|
||||
␍
|
||||
[1m Do[22m[5G[1myou[22m[9G[1mtrust[22m[15G[1mthe[22m[19G[1mfiles[22m[25G[1min[22m[28G[1mthis[22m[33G[1mfolder?[22m␍
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────␍
|
||||
[2GPlease[9Gconfirm[17Gthis[22Gis[25Gyour[30Gown[34Gproject[42Gor[45Gfrom[50Ga[52Gtrusted[60Gsource.[68GOnce[73Gtrusted,[82GQoder[88GCLI[92GCN[95Gcan[99Gread,[105Gmodify,[113Gand␍
|
||||
[2Gexecute[10Gfiles[16Ghere,[22Gand[26Gwill[31Gload[36Glocal[42Gconfigurations[57G(commands,[68Ghooks,[75GMCP[79Gservers,[88Gagents,[96Gskills,[104Gsettings).␍
|
||||
␍
|
||||
[2G[1m/private/tmp/orca-qoder-validation-1002/workspace[22m␍
|
||||
␍
|
||||
[3G❯[5G1.[8GTrust[14Gfolder␍
|
||||
[5G2.[8GDon't[14Gtrust[20Gand[24Gexit␍
|
||||
␍
|
||||
[2G[3m↑/↓[23m[6G[3mnavigate[23m[15G[3m·[23m[17G[3mEnter[23m[23G[3mselect[23m[30G[3m·[23m[32G[3mEsc[23m[36G[3mback[23m␍
|
||||
[0m[>0q[c[0m␍[11A ● Warning: True color (24-bit) support[41Gnot[45Gdetected.[55GUsing[61Ga[63Gterminal[72Gwith[77Gtrue[82Gcolor[88Genabled[96Gwill[101Gresult[108Gin[111Ga[113Gbetter␍[1B visual experience.[K␍[1C[1B[K␍[1B[1m [22m[1mD[22m[1mo[22m[1m [22m[1my[22m[1mo[22m[1mu[22m[1m [22m[1mt[22m[1mr[22m[1mu[22m[1ms[22m[1mt[22m[1m [22m[1mt[22m[1mh[22m[1me[22m[1m [22m[1mf[22m[1mi[22m[1ml[22m[1me[22m[1ms[22m[1m [22m[1mi[22m[1mn[22m[1m [22m[1mt[22m[1mh[22m[1mi[22m[1ms[22m[1m [22m[1mf[22m[1mo[22m[1ml[22m[1md[22m[1me[22m[1mr[22m[1m?[22m[K␍[1B────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────␍[1C[1BPlease confirm this is your own project or from a[52Gtrusted[60Gsource.[68GOnce[73Gtrusted,[82GQoder[88GCLI[92GCN[95Gcan[99Gread,[105Gmodify,[113Gand␍[1C[1Bexecute[10Gfiles[16Ghere,[22Gand[26Gwill[31Gload[36Glocal[42Gconfigurations[57G(commands,[68Ghooks,[75GMCP[79Gservers,[88Gagents,[96Gskills,[104Gsettings).␍[2C[1B[K␍[1C[1B[1m/[22m[1mp[22m[1mr[22m[1mi[22m[1mv[22m[1ma[22m[1mt[22m[1me[22m[1m/[22m[1mt[22m[1mm[22m[1mp[22m[1m/[22m[1mo[22m[1mr[22m[1mc[22m[1ma[22m[1m-[22m[1mq[22m[1mo[22m[1md[22m[1me[22m[1mr[22m[1m-[22m[1mv[22m[1ma[22m[1ml[22m[1mi[22m[1md[22m[1ma[22m[1mt[22m[1mi[22m[1mo[22m[1mn[22m[1m-[22m[1m1[22m[1m0[22m[1m0[22m[1m2[22m[1m/[22m[1mw[22m[1mo[22m[1mr[22m[1mk[22m[1ms[22m[1mp[22m[1ma[22m[1mc[22m[1me[22m␍[1C[2B ❯ 1. Trust folder[K␍
|
||||
[5G2.[8GDon't[14Gtrust[20Gand[24Gexit␍
|
||||
␍
|
||||
[2G[3m↑/↓[23m[6G[3mnavigate[23m[15G[3m·[23m[17G[3mEnter[23m[23G[3mselect[23m[30G[3m·[23m[32G[3mEsc[23m[36G[3mback[23m␍
|
||||
[0m[0m[2K[1A[2K[1A[2K[G[1A␍[1C[8A[1mW[22m[1me[22m[1ml[22m[1mc[22m[7G[1mm[22m[1me[22m[1m [22m[1mt[22m[1mo[22m[1m [22m[1mQ[22m[1mo[22m[1md[22m[1me[22m[1mr[22m[19G[1mC[22m[1mL[22m[1mI[22m[1m [22m[1mC[22m[1mN[22m[K␍[1C[2BSign in to get[17Gstarted, [27Gr exit[34Gthe applicati[48Gn.[K␍[1C[1B[K␍[2C[1B❯[5G1.[8GSign[13Gin[16Gto[19Gcontinue␍[1C[1B 2. Exit the application[K␍[1C[2B[3m↑[23m[3m/[23m[3m↓[23m[3m [23m[3mn[23m[3ma[23m[3mv[23m[3mi[23m[3mg[23m[3ma[23m[3mt[23m[3me[23m[3m [23m[3m·[23m[3m [23m[3mE[23m[3mn[23m[3mt[23m[3me[23m[3mr[23m[3m [23m[3ms[23m[3me[23m[3ml[23m[3me[23m[3mc[23m[3mt[23m[3m [23m[3m·[23m[3m [23m[3mE[23m[3ms[23m[3mc[23m[3m [23m[3mb[23m[3ma[23m[3mc[23m[3mk[23m␍[1B[K␍[1B[K␍[1B[K␍[2A[0m
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"capturedAt": "2026-10-02T07:26:38.777Z",
|
||||
"platform": "darwin",
|
||||
"command": [
|
||||
"qoderclicn",
|
||||
"--config-dir",
|
||||
"/tmp/orca-qoder-validation-1002/cn-config",
|
||||
"--cwd",
|
||||
"/tmp/orca-qoder-validation-1002/workspace"
|
||||
],
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"note": "Qoder CLI China 1.1.65; isolated config; no China account",
|
||||
"exitCode": 129
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
[8m[?u]11;?\[>q[>4;?m[c[2K␍[0m]0;◇ Qoder CLI CN | Ready ]8;;[0m7[r8[J␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
␍
|
||||
[H[?25h[?25l[?1006l[?1002l[?1004h]0;◇ Qoder CLI CN | Ready [?2004h[?2004h[?1004h[0m␍
|
||||
[2G[38;5;78m [39m[4G[38;5;78m██████[39m[39G╭─[42G[38;5;78m[1mTips[22m[39m[47G[38;5;78m[1mfor[22m[39m[51G[38;5;78m[1mgetting[22m[39m[59G[38;5;78m[1mstarted[22m[39m[67G───────────────────────────────────╮␍
|
||||
[2G[38;5;78m██[39m[10G[38;5;78m██[39m[39G│[41G1.[44GUse[48GAGENTS.md[58Gfiles[64Gto[67Gcustomize[77Ginteractions[102G│␍
|
||||
[2G[38;5;78m██[39m[6G[38;5;78m██[39m[10G[38;5;78m██[39m[14G[1mQoder[22m[20G[1mCLI[22m[24G[48;5;78m[38;5;16m[1m CN [22m[39m[49m[29Gv1.1.65[39G│[41G2.[44G/help[50Gfor[54Gmore[59Ginformation[102G│␍
|
||||
[2G[38;5;78m██[39m[8G[38;5;78m██[39m[39G│[41G3.[44GAsk[48Gcoding[55Gquestions,[66Gedit[71Gcode[76Gor[79Grun[83Gcommands[102G│␍
|
||||
[2G[38;5;78m [39m[4G[38;5;78m████[39m[10G[38;5;78m██[39m[14G[1mNot[22m[18G[1mLogin[22m[24GPlease[31GAuth[39G│[41G4.[44GBe[47Gspecific[56Gfor[60Gthe[64Gbest[69Gresults[102G│␍
|
||||
[39G╰──────────────────────────────────────────────────────────────╯␍
|
||||
␍
|
||||
␍
|
||||
[1m Do[22m[5G[1myou[22m[9G[1mtrust[22m[15G[1mthe[22m[19G[1mfiles[22m[25G[1min[22m[28G[1mthis[22m[33G[1mfolder?[22m␍
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────␍
|
||||
[2GPlease[9Gconfirm[17Gthis[22Gis[25Gyour[30Gown[34Gproject[42Gor[45Gfrom[50Ga[52Gtrusted[60Gsource.[68GOnce[73Gtrusted,[82GQoder[88GCLI[92GCN[95Gcan[99Gread,[105Gmodify,[113Gand␍
|
||||
[2Gexecute[10Gfiles[16Ghere,[22Gand[26Gwill[31Gload[36Glocal[42Gconfigurations[57G(commands,[68Ghooks,[75GMCP[79Gservers,[88Gagents,[96Gskills,[104Gsettings).␍
|
||||
␍
|
||||
[2G[1m/private/tmp/orca-qoder-validation-1002/workspace[22m␍
|
||||
␍
|
||||
[3G❯[5G1.[8GTrust[14Gfolder␍
|
||||
[5G2.[8GDon't[14Gtrust[20Gand[24Gexit␍
|
||||
␍
|
||||
[2G[3m↑/↓[23m[6G[3mnavigate[23m[15G[3m·[23m[17G[3mEnter[23m[23G[3mselect[23m[30G[3m·[23m[32G[3mEsc[23m[36G[3mback[23m␍
|
||||
[0m[>0q[c[0m␍[11A ● Warning: True color (24-bit) support[41Gnot[45Gdetected.[55GUsing[61Ga[63Gterminal[72Gwith[77Gtrue[82Gcolor[88Genabled[96Gwill[101Gresult[108Gin[111Ga[113Gbetter␍[1B visual experience.[K␍[1C[1B[K␍[1B[1m [22m[1mD[22m[1mo[22m[1m [22m[1my[22m[1mo[22m[1mu[22m[1m [22m[1mt[22m[1mr[22m[1mu[22m[1ms[22m[1mt[22m[1m [22m[1mt[22m[1mh[22m[1me[22m[1m [22m[1mf[22m[1mi[22m[1ml[22m[1me[22m[1ms[22m[1m [22m[1mi[22m[1mn[22m[1m [22m[1mt[22m[1mh[22m[1mi[22m[1ms[22m[1m [22m[1mf[22m[1mo[22m[1ml[22m[1md[22m[1me[22m[1mr[22m[1m?[22m[K␍[1B────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────␍[1C[1BPlease confirm this is your own project or from a[52Gtrusted[60Gsource.[68GOnce[73Gtrusted,[82GQoder[88GCLI[92GCN[95Gcan[99Gread,[105Gmodify,[113Gand␍[1C[1Bexecute[10Gfiles[16Ghere,[22Gand[26Gwill[31Gload[36Glocal[42Gconfigurations[57G(commands,[68Ghooks,[75GMCP[79Gservers,[88Gagents,[96Gskills,[104Gsettings).␍[2C[1B[K␍[1C[1B[1m/[22m[1mp[22m[1mr[22m[1mi[22m[1mv[22m[1ma[22m[1mt[22m[1me[22m[1m/[22m[1mt[22m[1mm[22m[1mp[22m[1m/[22m[1mo[22m[1mr[22m[1mc[22m[1ma[22m[1m-[22m[1mq[22m[1mo[22m[1md[22m[1me[22m[1mr[22m[1m-[22m[1mv[22m[1ma[22m[1ml[22m[1mi[22m[1md[22m[1ma[22m[1mt[22m[1mi[22m[1mo[22m[1mn[22m[1m-[22m[1m1[22m[1m0[22m[1m0[22m[1m2[22m[1m/[22m[1mw[22m[1mo[22m[1mr[22m[1mk[22m[1ms[22m[1mp[22m[1ma[22m[1mc[22m[1me[22m␍[1C[2B ❯ 1. Trust folder[K␍
|
||||
[5G2.[8GDon't[14Gtrust[20Gand[24Gexit␍
|
||||
␍
|
||||
[2G[3m↑/↓[23m[6G[3mnavigate[23m[15G[3m·[23m[17G[3mEnter[23m[23G[3mselect[23m[30G[3m·[23m[32G[3mEsc[23m[36G[3mback[23m␍
|
||||
[0m
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"description": "qoder-cn: title x first-party status on a painted screen; screen x foreground under the titles that leave the low lanes open; dialog order x title. Each read clocked and clockless.",
|
||||
"cases": {
|
||||
"title=working-spinner status=none screen=present fg=agent clock=clocked": "now=working edge=working quiet=working wait=pending",
|
||||
"title=working-spinner status=none screen=present fg=agent clock=clockless": "verdict=working wait=pending",
|
||||
"title=working-spinner status=done-fresh screen=present fg=agent clock=clocked": "now=working edge=working quiet=working wait=pending",
|
||||
"title=working-spinner status=done-fresh screen=present fg=agent clock=clockless": "verdict=working wait=pending",
|
||||
"title=working-spinner status=done-stale screen=present fg=agent clock=clocked": "now=working edge=working quiet=working wait=pending",
|
||||
"title=working-spinner status=done-stale screen=present fg=agent clock=clockless": "verdict=working wait=pending",
|
||||
"title=working-spinner status=working-fresh screen=present fg=agent clock=clocked": "now=working edge=working quiet=working wait=pending",
|
||||
"title=working-spinner status=working-fresh screen=present fg=agent clock=clockless": "verdict=working wait=pending",
|
||||
"title=working-spinner status=working-stale screen=present fg=agent clock=clocked": "now=working edge=working quiet=working wait=pending",
|
||||
"title=working-spinner status=working-stale screen=present fg=agent clock=clockless": "verdict=working wait=pending",
|
||||
"title=working-spinner status=blocked-fresh screen=present fg=agent clock=clocked": "now=working edge=working quiet=working wait=pending",
|
||||
"title=working-spinner status=blocked-fresh screen=present fg=agent clock=clockless": "verdict=working wait=pending",
|
||||
"title=working-spinner status=blocked-stale screen=present fg=agent clock=clocked": "now=working edge=working quiet=working wait=pending",
|
||||
"title=working-spinner status=blocked-stale screen=present fg=agent clock=clockless": "verdict=working wait=pending",
|
||||
"title=name-only status=none screen=present fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=name-only status=none screen=present fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=name-only status=done-fresh screen=present fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=name-only status=done-fresh screen=present fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=name-only status=done-stale screen=present fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=name-only status=done-stale screen=present fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=name-only status=working-fresh screen=present fg=agent clock=clocked": "now=working edge=working quiet=working wait=pending",
|
||||
"title=name-only status=working-fresh screen=present fg=agent clock=clockless": "verdict=working wait=pending",
|
||||
"title=name-only status=working-stale screen=present fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=name-only status=working-stale screen=present fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=name-only status=blocked-fresh screen=present fg=agent clock=clocked": "now=working edge=working quiet=working wait=pending",
|
||||
"title=name-only status=blocked-fresh screen=present fg=agent clock=clockless": "verdict=working wait=pending",
|
||||
"title=name-only status=blocked-stale screen=present fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=name-only status=blocked-stale screen=present fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=none status=none screen=present fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=none status=none screen=present fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=none status=done-fresh screen=present fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=none status=done-fresh screen=present fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=none status=done-stale screen=present fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=none status=done-stale screen=present fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=none status=working-fresh screen=present fg=agent clock=clocked": "now=working edge=working quiet=working wait=pending",
|
||||
"title=none status=working-fresh screen=present fg=agent clock=clockless": "verdict=working wait=pending",
|
||||
"title=none status=working-stale screen=present fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=none status=working-stale screen=present fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=none status=blocked-fresh screen=present fg=agent clock=clocked": "now=working edge=working quiet=working wait=pending",
|
||||
"title=none status=blocked-fresh screen=present fg=agent clock=clockless": "verdict=working wait=pending",
|
||||
"title=none status=blocked-stale screen=present fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=none status=blocked-stale screen=present fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=name-only status=none screen=present fg=shell clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=name-only status=none screen=present fg=shell clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=name-only status=none screen=untrusted fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=name-only status=none screen=untrusted fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=name-only status=none screen=untrusted fg=shell clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=name-only status=none screen=untrusted fg=shell clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=name-only status=none screen=absent fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=name-only status=none screen=absent fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=name-only status=none screen=absent fg=shell clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=name-only status=none screen=absent fg=shell clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=none status=none screen=present fg=shell clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=none status=none screen=present fg=shell clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=none status=none screen=untrusted fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=none status=none screen=untrusted fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=none status=none screen=untrusted fg=shell clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=none status=none screen=untrusted fg=shell clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=none status=none screen=absent fg=agent clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=none status=none screen=absent fg=agent clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=none status=none screen=absent fg=shell clock=clocked": "now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"title=none status=none screen=absent fg=shell clock=clockless": "verdict=pending:closed wait=pending",
|
||||
"title=working-spinner status=none screen=dialog-last fg=agent clock=clocked": "now=blocked:agent-trust-workspace edge=blocked:agent-trust-workspace quiet=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"title=working-spinner status=none screen=dialog-last fg=agent clock=clockless": "verdict=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"title=working-spinner status=none screen=ready-last fg=agent clock=clocked": "now=blocked:agent-trust-workspace edge=blocked:agent-trust-workspace quiet=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"title=working-spinner status=none screen=ready-last fg=agent clock=clockless": "verdict=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"title=name-only status=none screen=dialog-last fg=agent clock=clocked": "now=blocked:agent-trust-workspace edge=blocked:agent-trust-workspace quiet=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"title=name-only status=none screen=dialog-last fg=agent clock=clockless": "verdict=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"title=name-only status=none screen=ready-last fg=agent clock=clocked": "now=blocked:agent-trust-workspace edge=blocked:agent-trust-workspace quiet=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"title=name-only status=none screen=ready-last fg=agent clock=clockless": "verdict=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"title=none status=none screen=dialog-last fg=agent clock=clocked": "now=blocked:agent-trust-workspace edge=blocked:agent-trust-workspace quiet=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"title=none status=none screen=dialog-last fg=agent clock=clockless": "verdict=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"title=none status=none screen=ready-last fg=agent clock=clocked": "now=blocked:agent-trust-workspace edge=blocked:agent-trust-workspace quiet=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"title=none status=none screen=ready-last fg=agent clock=clockless": "verdict=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"description": "qoder-cn recording at 120x40 replayed on the agent pane, one entry per chunk",
|
||||
"observations": {
|
||||
"clocked": [
|
||||
"0-23: now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"24-36: now=pending:closed edge=pending:closed quiet=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"37-41: now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"42-45: now=pending:closed edge=pending:closed quiet=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"46: now=blocked:agent-trust-workspace edge=blocked:agent-trust-workspace quiet=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"47-65: now=pending:closed edge=pending:closed quiet=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"66-79: now=pending:closed edge=pending:closed quiet=pending:closed wait=pending"
|
||||
],
|
||||
"clockless": [
|
||||
"0-23: verdict=pending:closed wait=pending",
|
||||
"24-36: verdict=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"37-41: verdict=pending:closed wait=pending",
|
||||
"42-45: verdict=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"46: verdict=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"47-65: verdict=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"66-79: verdict=pending:closed wait=pending"
|
||||
]
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"description": "qoder-cn recording at 120x40 replayed on the unknown pane, one entry per chunk",
|
||||
"observations": {
|
||||
"clocked": [
|
||||
"0: now=pending:open edge=pending:open quiet=pending:open wait=ready@poll",
|
||||
"1-23: now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"24-36: now=pending:closed edge=pending:closed quiet=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"37-41: now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"42-45: now=pending:closed edge=pending:closed quiet=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"46: now=blocked:agent-trust-workspace edge=blocked:agent-trust-workspace quiet=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"47-65: now=pending:closed edge=pending:closed quiet=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"66-79: now=pending:closed edge=pending:closed quiet=pending:closed wait=pending"
|
||||
],
|
||||
"clockless": [
|
||||
"0: verdict=pending:open wait=ready@poll",
|
||||
"1-23: verdict=pending:closed wait=pending",
|
||||
"24-36: verdict=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"37-41: verdict=pending:closed wait=pending",
|
||||
"42-45: verdict=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"46: verdict=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"47-65: verdict=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"66-79: verdict=pending:closed wait=pending"
|
||||
]
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"description": "qoder-cn recording at 120x40 replayed on the agent pane, one entry per chunk",
|
||||
"observations": {
|
||||
"clocked": [
|
||||
"0-23: now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"24-36: now=pending:closed edge=pending:closed quiet=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"37-41: now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"42-45: now=pending:closed edge=pending:closed quiet=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"46: now=blocked:agent-trust-workspace edge=blocked:agent-trust-workspace quiet=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"47-66: now=pending:closed edge=pending:closed quiet=pending:closed wait=blocked:agent-trust-workspace@poll"
|
||||
],
|
||||
"clockless": [
|
||||
"0-23: verdict=pending:closed wait=pending",
|
||||
"24-36: verdict=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"37-41: verdict=pending:closed wait=pending",
|
||||
"42-45: verdict=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"46: verdict=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"47-66: verdict=pending:closed wait=blocked:agent-trust-workspace@poll"
|
||||
]
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"description": "qoder-cn recording at 120x40 replayed on the unknown pane, one entry per chunk",
|
||||
"observations": {
|
||||
"clocked": [
|
||||
"0: now=pending:open edge=pending:open quiet=pending:open wait=ready@poll",
|
||||
"1-23: now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"24-36: now=pending:closed edge=pending:closed quiet=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"37-41: now=pending:closed edge=pending:closed quiet=pending:closed wait=pending",
|
||||
"42-45: now=pending:closed edge=pending:closed quiet=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"46: now=blocked:agent-trust-workspace edge=blocked:agent-trust-workspace quiet=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"47-66: now=pending:closed edge=pending:closed quiet=pending:closed wait=blocked:agent-trust-workspace@poll"
|
||||
],
|
||||
"clockless": [
|
||||
"0: verdict=pending:open wait=ready@poll",
|
||||
"1-23: verdict=pending:closed wait=pending",
|
||||
"24-36: verdict=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"37-41: verdict=pending:closed wait=pending",
|
||||
"42-45: verdict=pending:closed wait=blocked:agent-trust-workspace@poll",
|
||||
"46: verdict=blocked:agent-trust-workspace wait=blocked:agent-trust-workspace@start",
|
||||
"47-66: verdict=pending:closed wait=blocked:agent-trust-workspace@poll"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -48,3 +48,25 @@ describe('captured Qoder 1.1.64 startup', () => {
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('captured Qoder China 1.1.65 startup', () => {
|
||||
it.each(['qoder-cn-startup', 'qoder-cn-signin'])('checks the composer in %s', async (fixture) => {
|
||||
const data = readFileSync(join(__dirname, '__fixtures__', `${fixture}.txt`), 'utf8')
|
||||
const title = extractLastOscTitle(
|
||||
data.replaceAll(`${String.fromCharCode(27)}]0;${String.fromCharCode(7)}`, '')
|
||||
)
|
||||
expect(getAgentLabel(normalizeTerminalTitle(title ?? ''))).toBe('Qoder CLI CN')
|
||||
const { runtime, handle } = await createTranscriptPane({
|
||||
paneTitle: title ?? '',
|
||||
foregroundProcess: 'qoderclicn',
|
||||
launchAgent: 'qoder-cn',
|
||||
data,
|
||||
size: { cols: 120, rows: 40 }
|
||||
})
|
||||
expect((await runtime.showTerminal(handle)).agentIdentity).toBe('qoder-cn')
|
||||
const readiness = await runtime
|
||||
.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 800 })
|
||||
.catch(() => null)
|
||||
expect(readiness?.satisfied ?? false).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -95,6 +95,7 @@ const READY_SCREEN_FIXTURES: Partial<
|
||||
codex: { name: 'codex-0157-plain-ready', cols: 120, rows: 40 },
|
||||
muse: { name: 'muse-empty-folder-ready', cols: 120, rows: 32 },
|
||||
qoder: { name: 'qoder-ready', cols: 100, rows: 32 },
|
||||
'qoder-cn': { name: 'qoder-cn-signin', cols: 120, rows: 40 },
|
||||
cursor: { name: 'cursor-agent-idle-after-approval', cols: 80, rows: 24 }
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ const RUNTIME_RECORDERS: readonly (readonly [string, Recorder])[] = [
|
||||
['muse-', { agent: 'muse', foregroundProcess: 'muse' }],
|
||||
['omp-', { agent: 'omp', foregroundProcess: 'omp' }],
|
||||
['prime-agent-', { agent: 'prime-agent', foregroundProcess: 'prime-agent' }],
|
||||
['qoder-cn-', { agent: 'qoder-cn', foregroundProcess: 'qoderclicn' }],
|
||||
['qoder-', { agent: 'qoder', foregroundProcess: 'qodercli' }],
|
||||
['zcode-', { agent: 'zcode', foregroundProcess: 'zcode' }]
|
||||
]
|
||||
|
||||
@@ -73,7 +73,7 @@ export function isKnownReadyPromptBody(
|
||||
readScreenLines: () => readonly string[] | null,
|
||||
hasOutputClock: boolean
|
||||
): boolean {
|
||||
if (agent === 'qoder') {
|
||||
if (agent === 'qoder' || agent === 'qoder-cn') {
|
||||
return isQoderComposerReady(readScreenLines())
|
||||
}
|
||||
// Why before the rules: such an agent settles only on the quiet lane while it has a clock.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import { ptyTuiIdleEvidence, type TuiIdleEvidenceSource } from './tui-idle-evidence-source'
|
||||
import { makeTuiIdlePty } from './tui-idle-wait-test-harness'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
BrowserWindow: { fromId: vi.fn(() => null) },
|
||||
webContents: { fromId: vi.fn(() => null) },
|
||||
ipcMain: { on: vi.fn(), removeListener: vi.fn() },
|
||||
app: { getPath: vi.fn(() => '/tmp') }
|
||||
}))
|
||||
|
||||
function source(agent: TuiAgent): TuiIdleEvidenceSource {
|
||||
return {
|
||||
quiescenceMs: 3000,
|
||||
getTabTitle: () => null,
|
||||
getAdoptedPtyIdleStatus: () => 'idle',
|
||||
getPaneAgent: () => agent,
|
||||
getFirstPartyAgentStatus: () => null,
|
||||
readScreenLines: () => null
|
||||
}
|
||||
}
|
||||
|
||||
describe('ptyTuiIdleEvidence', () => {
|
||||
it.each([
|
||||
['qoder', 'qoder-trust-dialog'],
|
||||
['qoder-cn', 'qoder-cn-signin']
|
||||
] as const)('requires a live composer for adopted %s panes', (agent, fixture) => {
|
||||
const transcript = readFileSync(join(__dirname, '__fixtures__', `${fixture}.txt`), 'utf8')
|
||||
const evidence = ptyTuiIdleEvidence(source(agent), makeTuiIdlePty(), () => transcript)
|
||||
expect(evidence.readPositiveBodyEvidence()).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps adopted idle evidence for other agents', () => {
|
||||
const evidence = ptyTuiIdleEvidence(source('claude'), makeTuiIdlePty(), () => '')
|
||||
expect(evidence.readPositiveBodyEvidence()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -134,7 +134,9 @@ export function ptyTuiIdleEvidence(
|
||||
record: pty,
|
||||
readTailBlockedReason: () => detectTerminalWaitBlockedReason(waitText()),
|
||||
readPositiveBodyEvidence: () =>
|
||||
(agent !== 'qoder' && source.getAdoptedPtyIdleStatus(pty) === 'idle') ||
|
||||
(agent !== 'qoder' &&
|
||||
agent !== 'qoder-cn' &&
|
||||
source.getAdoptedPtyIdleStatus(pty) === 'idle') ||
|
||||
isKnownReadyPromptBody(waitText(), agent, readScreen, pty.lastOutputAt !== null),
|
||||
readQuietReadyBodyEvidence: () => isQuietReadyScreenBody(waitText(), agent, readScreen),
|
||||
readAgentRuleVerdict: () => readAgentRuleVerdict(agent, pty, readScreen, waitText),
|
||||
|
||||
@@ -274,7 +274,7 @@ function rankTuiIdleEvidence(input: TuiIdleEvaluationInput): TuiIdleVerdict {
|
||||
return { kind: 'blocked', reason: blockedReason }
|
||||
}
|
||||
// Qoder publishes "Ready" before its trust dialog is dismissed; only its composer proves input is live.
|
||||
if (input.agent === 'qoder') {
|
||||
if (input.agent === 'qoder' || input.agent === 'qoder-cn') {
|
||||
if (
|
||||
hasFreshWorkingFirstPartyStatus(input.firstPartyStatus) ||
|
||||
input.record.lastAgentStatus === 'working'
|
||||
|
||||
@@ -15,6 +15,8 @@ export function workspaceTrustWritten(home: string, preset: AgentTrustPreset): b
|
||||
}
|
||||
case 'copilot':
|
||||
return existsSync(join(home, '.copilot', 'config.json'))
|
||||
case 'qoder-cn':
|
||||
return existsSync(join(home, '.qoder-cn', 'settings.json'))
|
||||
case 'qoder':
|
||||
return existsSync(join(home, '.qoder', 'settings.json'))
|
||||
case 'antigravity':
|
||||
|
||||
@@ -35,10 +35,11 @@ const AGENTS_WITH_A_RELAY_WRITER: [TuiAgent, AgentTrustPreset][] = [
|
||||
['codex', 'codex'],
|
||||
['cursor', 'cursor'],
|
||||
['copilot', 'copilot'],
|
||||
['qoder', 'qoder']
|
||||
['qoder', 'qoder'],
|
||||
['qoder-cn', 'qoder-cn']
|
||||
]
|
||||
const AGENTS_THAT_INHERIT_TRUST = AGENTS_WITH_A_RELAY_WRITER.filter(([, preset]) =>
|
||||
['claude', 'copilot', 'qoder'].includes(preset)
|
||||
['claude', 'copilot', 'qoder', 'qoder-cn'].includes(preset)
|
||||
)
|
||||
const ALL_PRESETS: AgentTrustPreset[] = [
|
||||
'claude',
|
||||
@@ -46,6 +47,7 @@ const ALL_PRESETS: AgentTrustPreset[] = [
|
||||
'cursor',
|
||||
'copilot',
|
||||
'qoder',
|
||||
'qoder-cn',
|
||||
'antigravity'
|
||||
]
|
||||
|
||||
|
||||
@@ -199,9 +199,11 @@ describe('AgentCombobox', () => {
|
||||
it('renders bundled favicons for favicon-domain agents instead of the remote Google service', () => {
|
||||
// Why: previously loaded from Google's favicon service (#8451). Iterate the
|
||||
// full asset map so missing files/key mismatches fail the test.
|
||||
const sharedIcons = new Map([['qoder-cn', 'qoder']])
|
||||
for (const agent of Object.keys(AGENT_FAVICON_ASSETS) as TuiAgent[]) {
|
||||
const markup = renderToStaticMarkup(<AgentIcon agent={agent} />)
|
||||
expect(markup).toContain(`/shared/agent-icons/${agent}.png`)
|
||||
const iconName = sharedIcons.get(agent) ?? agent
|
||||
expect(markup).toContain(`/shared/agent-icons/${iconName}.png`)
|
||||
expect(markup).not.toContain('https://www.google.com/s2/favicons')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -644,7 +644,8 @@
|
||||
"d443a47995": "Prime Agent",
|
||||
"mimo_code_label": "MiMo Code",
|
||||
"opencode2_label": "OpenCode 2",
|
||||
"codebuddy_label": "CodeBuddy"
|
||||
"codebuddy_label": "CodeBuddy",
|
||||
"qoder_cn_label": "Qoder CLI China"
|
||||
},
|
||||
"skill": {
|
||||
"cli": {
|
||||
|
||||
@@ -128,6 +128,13 @@ export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] =>
|
||||
faviconDomain: 'qoder.com',
|
||||
homepageUrl: 'https://docs.qoder.com/cli/overview'
|
||||
},
|
||||
{
|
||||
id: 'qoder-cn',
|
||||
label: translate('auto.lib.agent.catalog.qoder_cn_label', 'Qoder CLI China'),
|
||||
cmd: 'qoderclicn',
|
||||
faviconDomain: 'qoder.cn',
|
||||
homepageUrl: 'https://docs.qoder.cn/cli/overview'
|
||||
},
|
||||
{
|
||||
id: 'zcode',
|
||||
label: translate('auto.lib.agent.catalog.zcode_label', 'ZCode'),
|
||||
@@ -391,10 +398,7 @@ export function AgentIcon({
|
||||
if (agent === 'copilot') {
|
||||
return <CopilotIcon size={size} />
|
||||
}
|
||||
if (agent === 'opencode') {
|
||||
return <OpenCodeIcon size={size} />
|
||||
}
|
||||
if (agent === 'opencode2') {
|
||||
if (agent === 'opencode' || agent === 'opencode2') {
|
||||
return <OpenCodeIcon size={size} />
|
||||
}
|
||||
const catalogEntry = getAgentCatalog().find((a) => a.id === agent)
|
||||
|
||||
@@ -44,6 +44,7 @@ export const AGENT_FAVICON_ASSETS: Partial<Record<TuiAgent, string>> = {
|
||||
trae: traeUrl,
|
||||
'prime-agent': primeAgentUrl,
|
||||
qoder: qoderUrl,
|
||||
'qoder-cn': qoderUrl,
|
||||
gemini: geminiUrl,
|
||||
antigravity: antigravityUrl,
|
||||
goose: gooseUrl,
|
||||
|
||||
@@ -66,12 +66,43 @@ describe('agent picker search', () => {
|
||||
expect(replaceSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('resolves every catalog command alias to its agent first', () => {
|
||||
it('resolves catalog commands to the first entry owning that command', () => {
|
||||
for (const agent of AGENT_CATALOG) {
|
||||
expect(searchAgentPickerEntries(AGENT_CATALOG, agent.cmd)[0]?.id).toBe(agent.id)
|
||||
const owner = AGENT_CATALOG.find((candidate) => candidate.cmd === agent.cmd)
|
||||
expect(searchAgentPickerEntries(AGENT_CATALOG, agent.cmd)[0]?.id).toBe(owner?.id)
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
['qoder', 'qoder'],
|
||||
['qodercli', 'qoder'],
|
||||
['qoder-cn', 'qoder-cn'],
|
||||
['qoderclicn', 'qoder-cn'],
|
||||
['Qoder CLI China', 'qoder-cn'],
|
||||
['qwen', 'qwen-code']
|
||||
] as const)('resolves explicit identity or command %s to %s', (query, expected) => {
|
||||
expect(searchAgentPickerEntries(AGENT_CATALOG, query)[0]?.id).toBe(expected)
|
||||
})
|
||||
|
||||
it('keeps a stable default for a shared binary while explicit CN remains selectable', () => {
|
||||
const shared = [
|
||||
entry('qoder', 'Qoder CLI', 'qodercli'),
|
||||
entry('qoder-cn', 'Qoder CLI China', 'qodercli')
|
||||
]
|
||||
expect(searchAgentPickerEntries(shared, 'qodercli').map((agent) => agent.id)).toEqual([
|
||||
'qoder',
|
||||
'qoder-cn'
|
||||
])
|
||||
expect(searchAgentPickerEntries(shared, 'qoder-cn')[0]?.id).toBe('qoder-cn')
|
||||
expect(searchAgentPickerEntries(shared, 'Qoder CLI China')[0]?.id).toBe('qoder-cn')
|
||||
expect(
|
||||
searchAgentPickerEntries(
|
||||
shared.filter((agent) => agent.id !== 'qoder'),
|
||||
'qodercli'
|
||||
)[0]?.id
|
||||
).toBe('qoder-cn')
|
||||
})
|
||||
|
||||
it('returns no entries for unrelated text', () => {
|
||||
expect(searchAgentPickerEntries(agents, 'not-an-agent')).toEqual([])
|
||||
})
|
||||
|
||||
@@ -86,20 +86,25 @@ export function agentPickerBlankTerminalMatches(rawQuery: string): boolean {
|
||||
function scoreAgent(agent: AgentCatalogEntry, query: string): number {
|
||||
return Math.min(
|
||||
scoreCandidate(query, agent.label, 0),
|
||||
scoreCandidate(query, agent.id, 600),
|
||||
scoreCandidate(query, agent.cmd, 650),
|
||||
...(agent.searchAliases ?? []).map((alias) => scoreCandidate(query, alias, 650))
|
||||
scoreCandidate(query, agent.id, 600, 1),
|
||||
scoreCandidate(query, agent.cmd, 650, 2),
|
||||
...(agent.searchAliases ?? []).map((alias) => scoreCandidate(query, alias, 650, 2))
|
||||
)
|
||||
}
|
||||
|
||||
function scoreCandidate(query: string, rawCandidate: string, baseScore: number): number {
|
||||
function scoreCandidate(
|
||||
query: string,
|
||||
rawCandidate: string,
|
||||
baseScore: number,
|
||||
exactScore = baseScore
|
||||
): number {
|
||||
const candidate = normalizeSearchText(rawCandidate)
|
||||
if (!candidate) {
|
||||
return NO_MATCH
|
||||
}
|
||||
|
||||
if (candidate === query) {
|
||||
return baseScore
|
||||
return exactScore
|
||||
}
|
||||
if (candidate.startsWith(query)) {
|
||||
return baseScore + 10
|
||||
|
||||
@@ -111,6 +111,7 @@ const ICONABLE_AGENT_TYPES: Record<TuiAgent, true> = {
|
||||
omp: true,
|
||||
'prime-agent': true,
|
||||
qoder: true,
|
||||
'qoder-cn': true,
|
||||
gemini: true,
|
||||
antigravity: true,
|
||||
aider: true,
|
||||
|
||||
@@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest'
|
||||
import { RESUMABLE_TUI_AGENTS } from '../../../shared/agent-session-resume'
|
||||
import {
|
||||
AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QODER_CN_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QWEN_CODE_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY,
|
||||
@@ -53,6 +55,8 @@ describe('agentResumeHostAuthorityCapability', () => {
|
||||
codex: undefined,
|
||||
cursor: AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY,
|
||||
qoder: AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
|
||||
'qoder-cn': AGENT_SESSION_QODER_CN_RESUME_RUNTIME_CAPABILITY,
|
||||
'qwen-code': AGENT_SESSION_QWEN_CODE_RESUME_RUNTIME_CAPABILITY,
|
||||
gemini: undefined,
|
||||
antigravity: undefined,
|
||||
opencode: undefined,
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QODER_CN_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QWEN_CODE_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY,
|
||||
@@ -26,6 +28,8 @@ const RESUME_HOST_AUTHORITY_CAPABILITY_BY_AGENT = {
|
||||
codex: undefined,
|
||||
cursor: AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY,
|
||||
qoder: AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
|
||||
'qoder-cn': AGENT_SESSION_QODER_CN_RESUME_RUNTIME_CAPABILITY,
|
||||
'qwen-code': AGENT_SESSION_QWEN_CODE_RESUME_RUNTIME_CAPABILITY,
|
||||
gemini: undefined,
|
||||
antigravity: undefined,
|
||||
opencode: undefined,
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('remote agent-session launch routing', () => {
|
||||
expect(legacy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['kimi', 'muse', 'codebuddy', 'cursor'] as const)(
|
||||
it.each(['kimi', 'muse', 'codebuddy', 'qoder', 'qoder-cn', 'qwen-code', 'cursor'] as const)(
|
||||
'falls back to legacy when an older host lacks the %s resume capability',
|
||||
async (agent) => {
|
||||
const hostAuthority = vi.fn().mockResolvedValue('structured')
|
||||
@@ -70,7 +70,7 @@ describe('remote agent-session launch routing', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['muse', 'codebuddy'] as const)(
|
||||
it.each(['muse', 'codebuddy', 'qoder', 'qoder-cn', 'qwen-code'] as const)(
|
||||
'uses host authority when the host supports %s resume',
|
||||
async (agent) => {
|
||||
const hostAuthority = vi.fn().mockResolvedValue('host')
|
||||
|
||||
@@ -18,6 +18,7 @@ const HEADLESS_ONE_SHOT_MATCHERS: Partial<
|
||||
Record<TuiAgent, (tokens: readonly string[]) => boolean>
|
||||
> = {
|
||||
qoder: isQoderHeadlessCommand,
|
||||
'qoder-cn': isQoderHeadlessCommand,
|
||||
claude: isPrintModeHeadlessOneShotCommand,
|
||||
codebuddy: isCodebuddyNonInteractiveCommand,
|
||||
trae: isPrintModeHeadlessOneShotCommand,
|
||||
|
||||
@@ -163,6 +163,21 @@ export function normalizeProviderEvent(input: {
|
||||
case 'devin':
|
||||
payload = normalizeDevinEvent(state, eventName, promptText, paneKey, hookPayload)
|
||||
break
|
||||
case 'qoder-cn':
|
||||
payload = normalizeCompatibleLifecycleEvent(
|
||||
source,
|
||||
state,
|
||||
eventName,
|
||||
promptText,
|
||||
paneKey,
|
||||
hookPayload
|
||||
)
|
||||
break
|
||||
case 'qwen-code': {
|
||||
const normalized = normalizeClaudeEvent(state, eventName, promptText, paneKey, hookPayload)
|
||||
payload = normalized ? { ...normalized, agentType: 'qwen-code' } : null
|
||||
break
|
||||
}
|
||||
case 'qoder':
|
||||
payload = normalizeQoderEvent(state, eventName, promptText, paneKey, hookPayload)
|
||||
break
|
||||
|
||||
@@ -25,6 +25,8 @@ import { extractHermesToolFields } from './providers/hermes-tool-fields'
|
||||
export function isNewTurnEvent(source: AgentHookSource, eventName: unknown): boolean {
|
||||
// Why: exhaustive switch so a new AgentHookSource fails typecheck here instead of falling through to false.
|
||||
switch (source) {
|
||||
case 'qoder-cn':
|
||||
case 'qwen-code':
|
||||
case 'qoder':
|
||||
case 'claude':
|
||||
// Why: SessionStart lands an idle row (STA-3386) and must also drop stale
|
||||
@@ -140,6 +142,8 @@ export function extractToolFields(
|
||||
): ToolSnapshot {
|
||||
// Why: exhaustive switch so a new AgentHookSource fails typecheck here instead of silently routing through OpenCode's extractor.
|
||||
switch (source) {
|
||||
case 'qoder-cn':
|
||||
case 'qwen-code':
|
||||
case 'qoder':
|
||||
case 'claude':
|
||||
// Why: Kimi Code uses Claude's tool_name/tool_input payload fields verbatim.
|
||||
|
||||
@@ -48,7 +48,7 @@ function readCompatibleState(
|
||||
}
|
||||
|
||||
export function normalizeCompatibleLifecycleEvent(
|
||||
source: 'qoder' | 'codebuddy',
|
||||
source: 'qoder' | 'qoder-cn' | 'codebuddy',
|
||||
state: HookListenerState,
|
||||
eventName: unknown,
|
||||
promptText: string,
|
||||
|
||||
@@ -6,6 +6,8 @@ export const HOOK_SOURCE_BY_PATHNAME: Readonly<Record<string, AgentHookSource>>
|
||||
'/hook/claude': 'claude',
|
||||
'/hook/codex': 'codex',
|
||||
'/hook/qoder': 'qoder',
|
||||
'/hook/qoder-cn': 'qoder-cn',
|
||||
'/hook/qwen-code': 'qwen-code',
|
||||
'/hook/codebuddy': 'codebuddy',
|
||||
'/hook/gemini': 'gemini',
|
||||
'/hook/antigravity': 'antigravity',
|
||||
|
||||
@@ -39,6 +39,8 @@ const AGENT_HOOK_SOURCES = [
|
||||
'claude',
|
||||
'codex',
|
||||
'qoder',
|
||||
'qoder-cn',
|
||||
'qwen-code',
|
||||
'codebuddy',
|
||||
'gemini',
|
||||
'antigravity',
|
||||
|
||||
@@ -8,6 +8,8 @@ export const AGENT_HOOK_TARGETS = [
|
||||
'openclaude',
|
||||
'codex',
|
||||
'qoder',
|
||||
'qoder-cn',
|
||||
'qwen-code',
|
||||
'codebuddy',
|
||||
'gemini',
|
||||
'antigravity',
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
@@ -27,6 +27,7 @@ const TUI_AGENT_KIND_BY_AGENT = {
|
||||
omp: 'omp',
|
||||
'prime-agent': 'prime-agent',
|
||||
qoder: 'qoder',
|
||||
'qoder-cn': 'qoder-cn',
|
||||
gemini: 'gemini',
|
||||
antigravity: 'antigravity',
|
||||
aider: 'aider',
|
||||
|
||||
@@ -74,6 +74,9 @@ function agentForNormalizedProcess(normalized: string): TuiAgent | undefined {
|
||||
return PROCESS_TO_AGENT.get('codex')
|
||||
}
|
||||
// Qoder's launcher resolves to a versioned native binary.
|
||||
if (/^(?:qoderclicn|qodercn)-\d/.test(normalized)) {
|
||||
return PROCESS_TO_AGENT.get('qoderclicn')
|
||||
}
|
||||
if (/^qodercli-\d/.test(normalized)) {
|
||||
return PROCESS_TO_AGENT.get('qodercli')
|
||||
}
|
||||
@@ -162,6 +165,8 @@ export function isExpectedAgentProcess(
|
||||
return (
|
||||
normalizedProcess === normalizedExpected ||
|
||||
normalizedProcess.startsWith(`${normalizedExpected}.`) ||
|
||||
(['qoderclicn', 'qodercn'].includes(normalizedExpected) &&
|
||||
/^(?:qoderclicn|qodercn)(?:-\d.*)?$/.test(normalizedProcess)) ||
|
||||
(normalizedExpected === 'qodercli' && /^qodercli-\d/.test(normalizedProcess)) ||
|
||||
(normalizedExpected === 'muse' && normalizedProcess.startsWith('muse-bin-'))
|
||||
)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Why: adding kimi to RESUMABLE_TUI_AGENTS grows terminal.ensureAgentSession's enum, and an
|
||||
// older host answers the unknown member with invalid_argument — a code the launch fallback does
|
||||
// not retry on — so clients must probe before taking the host-authority path.
|
||||
export const AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.cursor-resume.v1' as const
|
||||
export const AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY = 'agent-session.kimi-resume.v1' as const
|
||||
export const AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.opencode2-resume.v1' as const
|
||||
export const AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY = 'agent-session.muse-resume.v1' as const
|
||||
export const AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY = 'agent-session.dsh-resume.v1' as const
|
||||
export const AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.codebuddy-resume.v1' as const
|
||||
export const AGENT_SESSION_QODER_CN_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.qoder-cn-resume.v1' as const
|
||||
export const AGENT_SESSION_QWEN_CODE_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.qwen-code-resume.v1' as const
|
||||
export const AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.qoder-resume.v1' as const
|
||||
export const AGENT_SESSION_ZCODE_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.zcode-resume.v1' as const
|
||||
|
||||
export const AGENT_SESSION_RESUME_RUNTIME_CAPABILITIES = [
|
||||
AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QODER_CN_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QWEN_CODE_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_ZCODE_RESUME_RUNTIME_CAPABILITY
|
||||
] as const
|
||||
@@ -8,6 +8,8 @@ export const RESUMABLE_TUI_AGENTS = [
|
||||
'codebuddy',
|
||||
'codex',
|
||||
'qoder',
|
||||
'qoder-cn',
|
||||
'qwen-code',
|
||||
'gemini',
|
||||
'antigravity',
|
||||
'opencode',
|
||||
@@ -197,6 +199,8 @@ export function extractAgentProviderSession(
|
||||
// Native-chat agents: also capture the hook's authoritative transcript_path,
|
||||
// since recent Claude Code names the transcript file with a UUID that differs
|
||||
// from the hook session_id (so the id-based glob no longer finds it).
|
||||
case 'qoder-cn':
|
||||
case 'qwen-code':
|
||||
case 'qoder':
|
||||
case 'codebuddy':
|
||||
case 'claude':
|
||||
@@ -291,6 +295,10 @@ export function getAgentResumeArgv(
|
||||
return providerSession.key === 'conversation_id' ? ['cursor-agent', '--resume', id] : null
|
||||
case 'codex':
|
||||
return providerSession.key === 'session_id' ? ['codex', 'resume', id] : null
|
||||
case 'qoder-cn':
|
||||
return providerSession.key === 'session_id' ? ['qoderclicn', '--resume', id] : null
|
||||
case 'qwen-code':
|
||||
return providerSession.key === 'session_id' ? ['qwen', '--resume', id] : null
|
||||
case 'qoder':
|
||||
return providerSession.key === 'session_id' ? ['qodercli', '--resume', id] : null
|
||||
case 'gemini':
|
||||
|
||||
@@ -74,7 +74,7 @@ function computeAgentLabel(title: string): string | null {
|
||||
return piStateBrand
|
||||
}
|
||||
if (isQoderTerminalTitle(title)) {
|
||||
return 'Qoder CLI'
|
||||
return title.includes('Qoder CLI CN') ? 'Qoder CLI CN' : 'Qoder CLI'
|
||||
}
|
||||
if (isGeminiTerminalTitle(title)) {
|
||||
return 'Gemini CLI'
|
||||
|
||||
@@ -132,7 +132,8 @@ export function createAgentStatusTracker(
|
||||
export function normalizeTerminalTitle(title: string): string {
|
||||
const qoderStatus = qoderTitleStatus(title)
|
||||
if (qoderStatus) {
|
||||
return `${qoderStatus === 'working' ? '✦' : qoderStatus === 'permission' ? '▲' : '◇'} Qoder CLI`
|
||||
const label = title.includes('Qoder CLI CN') ? 'Qoder CLI CN' : 'Qoder CLI'
|
||||
return `${qoderStatus === 'working' ? '✦' : qoderStatus === 'permission' ? '▲' : '◇'} ${label}`
|
||||
}
|
||||
if (!title) {
|
||||
return title
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
import { AGENT_SESSION_RESUME_RUNTIME_CAPABILITIES } from './agent-session-resume-runtime-capabilities'
|
||||
export {
|
||||
AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QODER_CN_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QWEN_CODE_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_ZCODE_RESUME_RUNTIME_CAPABILITY
|
||||
} from './agent-session-resume-runtime-capabilities'
|
||||
import { AGENT_SESSION_BACKGROUND_TASK_CHILD_VIEWS_CAPABILITY } from './agent-session-background-task-child-views-capability'
|
||||
import { AGENT_SESSION_STOP_RUNTIME_CAPABILITIES } from './agent-session-stop-capabilities'
|
||||
import { REMOTE_SERVER_UPDATE_CAPABILITY } from './remote-server-update'
|
||||
@@ -246,22 +259,6 @@ export const AGENT_SESSION_QUESTION_ANSWERS_RUNTIME_CAPABILITY =
|
||||
// stop capability above, which a client can advertise while predating this.
|
||||
export const AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY =
|
||||
'agent-session.background-task-row-stop.v1' as const
|
||||
// Why: adding kimi to RESUMABLE_TUI_AGENTS grows terminal.ensureAgentSession's enum, and an
|
||||
// older host answers the unknown member with invalid_argument — a code the launch fallback does
|
||||
// not retry on — so clients must probe before taking the host-authority path.
|
||||
export const AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.cursor-resume.v1' as const
|
||||
export const AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY = 'agent-session.kimi-resume.v1' as const
|
||||
export const AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.opencode2-resume.v1' as const
|
||||
export const AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY = 'agent-session.muse-resume.v1' as const
|
||||
export const AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY = 'agent-session.dsh-resume.v1' as const
|
||||
export const AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.codebuddy-resume.v1' as const
|
||||
export const AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.qoder-resume.v1' as const
|
||||
export const AGENT_SESSION_ZCODE_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.zcode-resume.v1' as const
|
||||
// Why: older runtimes strip mutation owner fields, so clients must fence writes before RPC.
|
||||
export const FILE_MUTATION_OWNERSHIP_RUNTIME_CAPABILITY = 'files.mutation-ownership.v1' as const
|
||||
export const FILE_MUTATION_OWNERSHIP_UPDATE_REQUIRED_MESSAGE =
|
||||
@@ -414,14 +411,7 @@ export const RUNTIME_CAPABILITIES = [
|
||||
AGENT_SESSION_TURN_ITEM_CAPABILITY,
|
||||
AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY,
|
||||
AGENT_SESSION_BACKGROUND_TASK_CHILD_VIEWS_CAPABILITY,
|
||||
AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_ZCODE_RESUME_RUNTIME_CAPABILITY,
|
||||
...AGENT_SESSION_RESUME_RUNTIME_CAPABILITIES,
|
||||
FILE_MUTATION_OWNERSHIP_RUNTIME_CAPABILITY,
|
||||
GITHUB_MARK_PR_READY_RUNTIME_CAPABILITY,
|
||||
GITLAB_READY_FOR_REVIEW_RUNTIME_CAPABILITY,
|
||||
|
||||
@@ -158,3 +158,46 @@ it('settles manual compaction without interrupting automatic compaction', () =>
|
||||
agentType: 'qoder'
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['qoder-cn', 'qwen-code'] as const)('keeps %s lifecycle and resume attributed', (agent) => {
|
||||
const state = createHookListenerState()
|
||||
const send = (hook_event_name: string, extra = {}) =>
|
||||
normalizeAndAccept(state, agent, {
|
||||
hook_event_name,
|
||||
session_id: 'provider-proof',
|
||||
...extra
|
||||
})
|
||||
expect(send('SessionStart', { source: 'startup' })?.payload).toMatchObject({
|
||||
agentType: agent,
|
||||
state: 'done',
|
||||
sessionBoundary: true
|
||||
})
|
||||
expect(send('UserPromptSubmit', { prompt: 'Create proof.txt' })?.payload).toMatchObject({
|
||||
agentType: agent,
|
||||
state: 'working'
|
||||
})
|
||||
expect(send('PermissionRequest', { tool_name: 'Write' })?.payload).toMatchObject({
|
||||
agentType: agent,
|
||||
state: 'waiting'
|
||||
})
|
||||
expect(send('Stop', { last_assistant_message: 'Done' })?.payload).toMatchObject({
|
||||
agentType: agent,
|
||||
state: 'done'
|
||||
})
|
||||
expect(send('SessionStart', { source: 'resume' })?.providerSession?.id).toBe('provider-proof')
|
||||
expect(getAgentResumeArgv(agent, { key: 'session_id', id: 'provider-proof' })).toEqual([
|
||||
agent === 'qoder-cn' ? 'qoderclicn' : 'qwen',
|
||||
'--resume',
|
||||
'provider-proof'
|
||||
])
|
||||
})
|
||||
|
||||
it.each([
|
||||
'qoderclicn',
|
||||
'qodercn',
|
||||
'/home/test/.qoder-cn/bin/qoderclicn-1.1.65',
|
||||
'C:\\Qoder\\qoderclicn.exe'
|
||||
])('recognizes the China executable %s', (command) => {
|
||||
expect(recognizeAgentProcessFromCommandLine(command)?.agent).toBe('qoder-cn')
|
||||
expect(recognizeAgentProcessFromCommandLine(`${command} --print hello`)).toBeNull()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildAgentStartupPlan, buildAgentResumeStartupPlan } from './tui-agent-startup'
|
||||
|
||||
describe('Qoder China launch recipe identity', () => {
|
||||
it.each(['darwin', 'linux', 'win32'] as const)(
|
||||
'preserves the official China executable on %s',
|
||||
(platform) => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'qoder-cn',
|
||||
prompt: 'fixture',
|
||||
cmdOverrides: {},
|
||||
platform
|
||||
})
|
||||
expect(plan?.agent).toBe('qoder-cn')
|
||||
expect(plan?.launchCommand).toBe("qoderclicn --prompt-interactive 'fixture'")
|
||||
expect(
|
||||
buildAgentResumeStartupPlan({
|
||||
agent: 'qoder-cn',
|
||||
providerSession: { key: 'session_id', id: 'fixture-session' },
|
||||
cmdOverrides: {},
|
||||
platform
|
||||
})?.launchCommand
|
||||
).toBe("qoderclicn '--resume' 'fixture-session'")
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['darwin', 'linux', 'win32'] as const)(
|
||||
'preserves an explicitly configured shared-binary --cn recipe on %s',
|
||||
(platform) => {
|
||||
const cmdOverrides = { 'qoder-cn': 'qodercli --cn' }
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'qoder-cn',
|
||||
prompt: 'fixture',
|
||||
cmdOverrides,
|
||||
platform
|
||||
})
|
||||
expect(plan?.agent).toBe('qoder-cn')
|
||||
expect(plan?.launchCommand).toBe("qodercli --cn --prompt-interactive 'fixture'")
|
||||
expect(
|
||||
buildAgentResumeStartupPlan({
|
||||
agent: 'qoder-cn',
|
||||
providerSession: { key: 'session_id', id: 'fixture-session' },
|
||||
cmdOverrides,
|
||||
platform
|
||||
})?.launchCommand
|
||||
).toBe("qodercli --cn '--resume' 'fixture-session'")
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -26,6 +26,7 @@ export const SKILLS_CLI_AGENT_KEY_BY_TUI_AGENT = {
|
||||
omp: null,
|
||||
'prime-agent': null,
|
||||
qoder: 'qoder',
|
||||
'qoder-cn': 'qoder-cn',
|
||||
gemini: 'gemini-cli',
|
||||
antigravity: 'antigravity',
|
||||
aider: null,
|
||||
|
||||
@@ -21,6 +21,7 @@ export const AGENT_KIND_VALUES = [
|
||||
'omp',
|
||||
'prime-agent',
|
||||
'qoder',
|
||||
'qoder-cn',
|
||||
'gemini',
|
||||
'antigravity',
|
||||
'aider',
|
||||
|
||||
@@ -175,7 +175,7 @@ function computeAgentLabel(title: string): string | null {
|
||||
return 'DeepSeek Harness'
|
||||
}
|
||||
if (isQoderTerminalTitle(title)) {
|
||||
return 'Qoder CLI'
|
||||
return title.includes('Qoder CLI CN') ? 'Qoder CLI CN' : 'Qoder CLI'
|
||||
}
|
||||
if (isGeminiTerminalTitle(title)) {
|
||||
return 'Gemini CLI'
|
||||
@@ -258,6 +258,8 @@ const TITLE_LABEL_TO_AGENT: Partial<Record<string, TuiAgent>> = {
|
||||
OpenClaude: 'openclaude',
|
||||
Codex: 'codex',
|
||||
'Qoder CLI': 'qoder',
|
||||
'Qoder CLI CN': 'qoder-cn',
|
||||
'Qoder CLI China': 'qoder-cn',
|
||||
'Gemini CLI': 'gemini',
|
||||
'GitHub Copilot': 'copilot',
|
||||
Grok: 'grok',
|
||||
|
||||
@@ -38,7 +38,7 @@ export type TuiAgentConfig = {
|
||||
/** Claude Code follows pasted text only where the user's typed words ask, so dispatch briefs need a typed lead line. */
|
||||
pasteNeedsTypedRequest?: boolean
|
||||
/** Trust Orca pre-writes at PTY spawn (agent-workspace-trust.ts) so the agent's first-launch "trust this folder?" menu doesn't consume the bracketed paste. */
|
||||
preflightTrust?: 'claude' | 'cursor' | 'copilot' | 'codex' | 'antigravity' | 'qoder'
|
||||
preflightTrust?: 'claude' | 'cursor' | 'copilot' | 'codex' | 'antigravity' | 'qoder' | 'qoder-cn'
|
||||
/** Agent-specific signal that the composer is ready for paste, stronger than the default quiet-render window. */
|
||||
draftPasteReadySignal?: DraftPasteReadySignal
|
||||
/** Hard deadline for the agent's composer readiness signal. */
|
||||
|
||||
@@ -146,6 +146,12 @@ const TUI_AGENT_CONFIG_SOURCE: Record<TuiAgent, TuiAgentConfigSource> = {
|
||||
promptInjectionMode: 'flag-prompt-interactive',
|
||||
preflightTrust: 'qoder'
|
||||
},
|
||||
'qoder-cn': {
|
||||
detectCmd: 'qoderclicn',
|
||||
detectCmdAliases: ['qodercn'],
|
||||
promptInjectionMode: 'flag-prompt-interactive',
|
||||
preflightTrust: 'qoder-cn'
|
||||
},
|
||||
gemini: {
|
||||
detectCmd: 'gemini',
|
||||
promptInjectionMode: 'flag-prompt-interactive'
|
||||
|
||||
@@ -25,6 +25,7 @@ export const TUI_AGENT_DISPLAY_NAMES: Record<TuiAgent, string> = {
|
||||
omp: 'OMP',
|
||||
'prime-agent': 'Prime Agent',
|
||||
qoder: 'Qoder CLI',
|
||||
'qoder-cn': 'Qoder CLI China',
|
||||
gemini: 'Gemini',
|
||||
antigravity: 'Antigravity',
|
||||
aider: 'Aider',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const YOLO_TUI_AGENT_ARGS: Partial<Record<TuiAgent, string>> = {
|
||||
openclaude: '--dangerously-skip-permissions',
|
||||
codex: '--dangerously-bypass-approvals-and-sandbox',
|
||||
qoder: '--dangerously-skip-permissions',
|
||||
'qoder-cn': '--dangerously-skip-permissions',
|
||||
gemini: '--yolo',
|
||||
antigravity: '--dangerously-skip-permissions',
|
||||
aider: '--yes-always',
|
||||
|
||||
@@ -29,6 +29,7 @@ const EXPECTED_REST_SIGNALS: Record<TuiAgent, TuiAgentRestSignal> = {
|
||||
grok: 'title',
|
||||
muse: 'ready-body',
|
||||
qoder: 'ready-body',
|
||||
'qoder-cn': 'ready-body',
|
||||
codebuddy: 'none',
|
||||
autohand: 'none',
|
||||
ante: 'none',
|
||||
|
||||
@@ -20,7 +20,8 @@ export type TuiAgentRestSignal = 'hook-done' | 'synthetic-title' | 'title' | 're
|
||||
const IDENTITY_REST_SIGNALS: Partial<Record<TuiAgent, TuiAgentRestSignal>> = {
|
||||
dsh: 'hook-done',
|
||||
muse: 'ready-body',
|
||||
qoder: 'ready-body'
|
||||
qoder: 'ready-body',
|
||||
'qoder-cn': 'ready-body'
|
||||
}
|
||||
|
||||
// Why derived, not declared per agent: the title tables are the evidence, so a second
|
||||
|
||||
@@ -18,6 +18,7 @@ export const TUI_AGENT_AUTO_PICK_ORDER = [
|
||||
'muse',
|
||||
'dsh',
|
||||
'qoder',
|
||||
'qoder-cn',
|
||||
'zcode',
|
||||
'pi',
|
||||
'omp',
|
||||
|
||||
@@ -13,6 +13,7 @@ export type TuiAgent =
|
||||
| 'pi' // Pi (pi.dev)
|
||||
| 'omp' // OMP (omp.sh)
|
||||
| 'qoder' // Qoder CLI
|
||||
| 'qoder-cn' // Qoder CLI China
|
||||
| 'gemini' // Gemini CLI
|
||||
| 'antigravity' // Google Antigravity CLI
|
||||
| 'aider' // Aider
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { expect, test } from 'vitest'
|
||||
import { EnsureAgentSessionParams } from '../../../src/shared/rpc-contract/agent-session-params'
|
||||
import { normalizeAgentStatusPayload } from '../../../src/shared/agent-status-types'
|
||||
import { importReleaseCheckoutModule, materializeReleaseCheckout } from './release-checkout'
|
||||
|
||||
test.each(['v1.4.205', 'v1.4.211'])(
|
||||
'release %s preserves new provider status tags but refuses unsupported resume',
|
||||
async (ref) => {
|
||||
const checkout = await materializeReleaseCheckout(ref)
|
||||
const baselineStatus = await importReleaseCheckoutModule(
|
||||
checkout,
|
||||
'src/shared/agent-status-types.ts'
|
||||
)
|
||||
const normalize = baselineStatus.normalizeAgentStatusPayload
|
||||
if (typeof normalize !== 'function') {
|
||||
throw new Error('Pinned release has no status normalizer')
|
||||
}
|
||||
const baselineResume = await importReleaseCheckoutModule(
|
||||
checkout,
|
||||
'src/shared/rpc-contract/agent-session-params.ts'
|
||||
)
|
||||
const schema = baselineResume.EnsureAgentSessionParams
|
||||
if (
|
||||
!schema ||
|
||||
typeof schema !== 'object' ||
|
||||
!('safeParse' in schema) ||
|
||||
typeof schema.safeParse !== 'function'
|
||||
) {
|
||||
throw new Error('Pinned release has no resume request parser')
|
||||
}
|
||||
for (const agent of ['qoder-cn', 'qwen-code'] as const) {
|
||||
const status = { state: 'working', agentType: agent }
|
||||
expect(normalize(status)).toMatchObject(status)
|
||||
expect(normalizeAgentStatusPayload(status)).toMatchObject(status)
|
||||
const request = {
|
||||
kind: 'explicit',
|
||||
worktree: 'folder:/task-owned/folder',
|
||||
agent,
|
||||
providerSession: { key: 'session_id', id: 'test-session' }
|
||||
}
|
||||
expect(EnsureAgentSessionParams.safeParse(request).success).toBe(true)
|
||||
expect(schema.safeParse(request)).toHaveProperty('success', false)
|
||||
}
|
||||
},
|
||||
120_000
|
||||
)
|
||||
Reference in New Issue
Block a user