qwen-code/packages/web-shell/client/constants/localCommands.ts
Shaojin Wen 9a63c03224
feat(web-shell): add a Scheduled Tasks management page (#6348)
* feat(web-shell): add scheduled tasks management page

Add a "Scheduled tasks" page to the Web Shell for managing durable cron tasks against the current workspace.

- Sidebar entry opens a full-pane page (replaces the chat area, not a modal) listing tasks with enable/disable toggle, delete, run-now, and human-readable schedules.
- "New scheduled task" opens a modal with a schedule builder (daily / weekdays / weekly / hourly / every-N-minutes / custom cron) and a live preview.
- "Create via chat" returns to the chat and primes the composer so the agent creates the task through its cron_create tool.
- Daemon CRUD routes (GET/POST/PATCH/DELETE /scheduled-tasks) read/write the existing per-project scheduled_tasks.json; task firing stays with the session-side scheduler.
- Extend DurableCronTask with optional name/enabled (backward compatible); the scheduler skips tasks with enabled:false.
- Add /scheduled-tasks to the vite dev-server proxy allowlist so the page works under npm run dev:daemon.

* chore(web-shell): address review feedback on scheduled tasks

- cron_list: surface name/enabled so the agent can tell a disabled durable task from an active one (a disabled task no longer looks identical to an active one).
- core: export only the tasks-file functions the daemon route actually uses (drop unused addCronTask / getCronFilePath / CRON_TASKS_DISPLAY_PATH from the public barrel).
- CronScheduler: warn when a durable reload fails and the prior view is kept, since a just-disabled or -deleted task can keep firing until the next successful reload.
- Extract the schedule helpers (buildCron / describeCron / parseHhmm / describeLastRun) into a pure module and add unit tests for them.
- Add route tests for PATCH cron/prompt/recurring, empty-patch rejection, and POST field-length / boolean-type validation.

* chore(web-shell): address second review round on scheduled tasks

- Log CRUD errors server-side (writeStderrLine) in each route catch block, matching the other daemon routes.
- Share one id generator (generateCronTaskId in cronTasksFile) between the scheduler and the daemon route instead of duplicating it.
- describeCron: recognize cron day-of-week 7 as an alternate notation for Sunday.
- Reset the builder time to :00 when switching to the hourly frequency (its time picker is hidden, so it no longer silently carries the daily minute).
- Tests: cron_list name/disabled output; route Feb-30 impossible-cron and corrupt-file 500 read-failure; describeCron dow=7.

* chore(web-shell): address third review round on scheduled tasks

- Run now: report sendPrompt rejections via the toast/error path instead of dropping the promise.
- Block chat interaction while the full-pane Scheduled Tasks view is open, so the covered composer can't receive keystrokes/Escape.
- Guard reload() with a request-sequence id so a slow load can't overwrite a newer list after a mutation.
- Re-enabling a task that had genuinely fired resumes from now instead of catching up work paused while it was disabled.
- Restrict "every N minutes" to divisors of 60 (a non-divisor */N fires more often than the label claims).
- Show a Repeats / Runs once label on each card so tool-created one-shots aren't mistaken for repeating schedules.
- Return generic 500 client messages (no internal file path); the detail is logged server-side.
- Tests: SDK scheduled-task methods (method/URL/id-encoding/headers/errors); route re-enable behavior both ways.

* chore(web-shell): address fourth review round (minor suggestions)

- Route error logs interpolate the actual task id instead of the literal ":id".
- cron_list returnDisplay includes the task name (matching llmContent) so terminal /cron list shows UI-assigned names.
- Truncate the delete-confirm label so an unnamed task's long prompt doesn't blow up the confirm() dialog.
- Cap the create-form prompt textarea at MAX_PROMPT_LENGTH and drop the dead typeof-window guard.
- Test generateCronTaskId (format + near-uniqueness).

* chore(web-shell): address fifth review round on scheduled tasks

- Re-enable now resumes any recurring task from now (stamp on every false→true), not only ones that had already fired — a task disabled before its first run no longer catch-up-fires the slot it was paused through.
- describeCron applies the same divisor-of-60 check as buildCron, so a hand-edited/persisted */45 falls back to the raw expression instead of a misleading "every 45 minutes".
- Strengthen the corrupt-file route test to assert the generic client message and no leaked file path.
- Tests: recurring-disabled-before-first-run and one-shot re-enable; describeCron non-divisor fallback.

* test(cli): cover legacy scheduled-task normalization on GET

Seed a pre-fields task (no name/enabled) directly to disk and assert the GET response normalizes it to name:null / enabled:true, guarding backward compatibility with existing scheduled_tasks.json files.

* fix(core): cap durable cron loads against a durable-only budget

The daemon route accepts up to MAX_JOBS durable tasks on disk, but the scheduler previously capped durable loads against its combined job map (session-only + durable). A session holding session-only cron jobs could push the map to MAX_JOBS and make loadFileTasks silently skip durable tasks the route had already accepted — a create that returned 201 would then never fire.

Cap durable installs against a durable-only count instead, and share one MAX_JOBS constant between the scheduler and the daemon route, so a successful create is always loadable. Adds a scheduler test that 40 session-only jobs no longer crowd out 20 durable loads.
2026-07-06 03:47:17 +00:00

225 lines
7.6 KiB
TypeScript

import type { CommandInfo } from '../adapters/types';
import type { useI18n } from '../i18n';
type Translate = ReturnType<typeof useI18n>['t'];
/**
* Commands that should always appear in the slash-command completion menu,
* regardless of what ACP sends (ACP filters most BUILT_IN commands to
* 'interactive' mode only). These are merged with ACP-provided commands,
* with ACP taking precedence on duplicates.
*/
export function getLocalCommands(t: Translate): CommandInfo[] {
const commands: CommandInfo[] = [
{ name: 'help', description: t('local.help') },
{
name: 'theme',
description: t('local.theme'),
argumentHint: 'light|dark',
subcommands: ['light', 'dark'],
},
{
name: 'language',
description: t('local.language'),
argumentHint: 'ui [en|zh-CN]',
subcommands: ['ui'],
},
{ name: 'plan', description: t('local.plan'), argumentHint: '<prompt>' },
{
name: 'btw',
description: t('local.btw'),
argumentHint: '<your question>',
},
{
name: 'copy',
description: t('local.copy'),
argumentHint: '[code|<lang>|latex|inline-latex] [index]',
},
{ name: 'delete', description: t('local.delete') },
{ name: 'release', description: t('local.release') },
{ name: 'auth', description: t('local.auth') },
{
name: 'approval-mode',
description: t('local.approvalMode'),
argumentHint: '<mode>',
},
{
name: 'model',
description: t('local.model'),
argumentHint: '[--fast|--voice|--vision] [<model>]',
},
{
name: 'mcp',
description: t('local.mcp'),
argumentHint: 'desc|nodesc|schema',
},
{ name: 'skills', description: t('local.skills') },
{ name: 'status', description: t('local.status') },
{
name: 'stats',
description: t('local.stats'),
argumentHint: '[model|tools]',
subcommands: ['model', 'tools'],
},
{ name: 'tools', description: t('local.tools'), argumentHint: '[desc]' },
{
name: 'memory',
description: t('local.memory'),
},
{
name: 'context',
description: t('local.context'),
argumentHint: '[detail]',
},
{
name: 'agents',
description: t('local.agents'),
argumentHint: 'manage|create',
},
{
name: 'goal',
description: t('local.goal'),
argumentHint: '[<condition> | clear]',
},
{ name: 'tasks', description: t('local.tasks') },
{ name: 'recap', description: t('local.recap') },
{ name: 'rewind', description: t('local.rewind') },
{
name: 'branch',
description: t('local.branch'),
argumentHint: '[<name>]',
},
{
name: 'fork',
description: t('local.fork'),
argumentHint: '<directive>',
},
{ name: 'clear', description: t('local.clear') },
{ name: 'new', description: t('local.new') },
{ name: 'reset', description: t('local.reset') },
{
name: 'rename',
description: t('local.rename'),
argumentHint: '[--auto] [<name>]',
},
{
name: 'resume',
description: t('local.resume'),
argumentHint: '<session-id>',
},
{ name: 'settings', description: t('local.settings') },
{ name: 'schedule', description: t('local.schedule') },
{
name: 'extensions',
description: t('local.extensions'),
argumentHint: 'manage|install <source>',
subcommands: ['manage', 'install'],
},
];
return commands.map((command) => ({
...command,
source: 'builtin-command',
}));
}
/**
* Built-in commands the daemon advertises but that are NOT part of
* getLocalCommands (they are feature/extension-gated, so we must not pin them
* into the always-on fallback list). The daemon fills their descriptions in the
* daemon *process* language, which is independent of the web-shell UI language,
* so without this the slash menu ends up a mix of languages. We re-localize
* these built-ins by name to the current UI language.
*
* Maps command name -> i18n key. Guarded by source === 'builtin-command', so a
* user's custom command that happens to share a built-in name keeps its own
* description.
*/
const BUILTIN_COMMAND_DESCRIPTION_KEYS: Record<string, string> = {
bug: 'local.bug',
compress: 'local.compress',
'compress-fast': 'local.compressFast',
config: 'local.config',
diff: 'local.diff',
directory: 'local.directory',
docs: 'local.docs',
doctor: 'local.doctor',
dream: 'local.dream',
effort: 'local.effort',
export: 'local.export',
forget: 'local.forget',
hooks: 'local.hooks',
'import-config': 'local.importConfig',
init: 'local.init',
insight: 'local.insight',
lsp: 'local.lsp',
remember: 'local.remember',
summary: 'local.summary',
workflows: 'local.workflows',
};
/**
* Skills whose author-written descriptions ship in English — the Qwen Code
* bundled skills plus this repo's `.qwen/skills` project skills. We re-localize
* their menu descriptions by name so a zh-CN slash menu isn't a mix of languages.
* Keyed by skill name because a skill only carries a reliable `source` once a
* session exists; the skill-tagging step keys off `connection.skills` instead, so
* this works on the welcome screen too. Display-only — the model still receives
* the daemon's canonical (English) description. Skills not listed here (a user's
* own skills, newly added ones) fall back to their authored description.
*/
const SKILL_DESCRIPTION_KEYS: Record<string, string> = {
// Bundled with Qwen Code (packages/core/src/skills/bundled).
batch: 'skilldesc.batch',
dataviz: 'skilldesc.dataviz',
'extension-creator': 'skilldesc.extensionCreator',
loop: 'skilldesc.loop',
'new-app': 'skilldesc.newApp',
'qc-helper': 'skilldesc.qcHelper',
review: 'skilldesc.review',
simplify: 'skilldesc.simplify',
stuck: 'skilldesc.stuck',
// This repo's project skills (.qwen/skills).
'agent-reproduce-align': 'skilldesc.agentReproduceAlign',
'agent-reproduce-feature': 'skilldesc.agentReproduceFeature',
bugfix: 'skilldesc.bugfix',
codegraph: 'skilldesc.codegraph',
'create-issue': 'skilldesc.createIssue',
'desktop-pet': 'skilldesc.desktopPet',
'docs-audit-and-refresh': 'skilldesc.docsAuditAndRefresh',
'docs-update-from-diff': 'skilldesc.docsUpdateFromDiff',
'e2e-testing': 'skilldesc.e2eTesting',
'feat-dev': 'skilldesc.featDev',
'memory-leak-debug': 'skilldesc.memoryLeakDebug',
'openwork-desktop-sync': 'skilldesc.openworkDesktopSync',
'prepare-pr': 'skilldesc.preparePr',
'qwen-code-claw': 'skilldesc.qwenCodeClaw',
'structured-debugging': 'skilldesc.structuredDebugging',
'terminal-capture': 'skilldesc.terminalCapture',
'tmux-real-user-testing': 'skilldesc.tmuxRealUserTesting',
triage: 'skilldesc.triage',
};
/**
* i18n key for a known skill's localized menu description, or undefined for a
* skill we don't ship a translation for (leave its authored description).
*/
export function skillDescriptionKey(name: string): string | undefined {
return SKILL_DESCRIPTION_KEYS[name];
}
/**
* Re-localize built-in command descriptions by name so the slash menu matches
* the web-shell UI language even when the daemon advertises them in its own
* process language. Guarded by source === 'builtin-command' so custom commands
* keep their own description. (Skills are localized in the skill-tagging step.)
*/
export function localizeBuiltinDescriptions(
commands: CommandInfo[],
t: Translate,
): CommandInfo[] {
return commands.map((command) => {
if (command.source !== 'builtin-command') return command;
const key = BUILTIN_COMMAND_DESCRIPTION_KEYS[command.name];
return key ? { ...command, description: t(key) } : command;
});
}