mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-14 19:24:54 +00:00
* 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.
225 lines
7.6 KiB
TypeScript
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;
|
|
});
|
|
}
|