codemini-cli 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,6 +26,7 @@ import {
26
26
  estimateMessagesTokens,
27
27
  parseCompactArgs
28
28
  } from './context-compact.js';
29
+ import { buildSystemPromptWithReplyLanguage } from './reply-language.js';
29
30
  import { buildSystemPromptWithSoul } from './soul.js';
30
31
 
31
32
  function toOpenAIMessages(sessionMessages) {
@@ -60,6 +61,46 @@ function nowStamp() {
60
61
  return new Date().toISOString().replace(/[:.]/g, '-');
61
62
  }
62
63
 
64
+ function prioritizeByPreferredOrder(items, preferredOrder) {
65
+ const source = Array.isArray(items) ? items : [];
66
+ const priorities = new Map((Array.isArray(preferredOrder) ? preferredOrder : []).map((value, index) => [value, index]));
67
+ return [...source].sort((left, right) => {
68
+ const leftRank = priorities.has(left) ? priorities.get(left) : Number.MAX_SAFE_INTEGER;
69
+ const rightRank = priorities.has(right) ? priorities.get(right) : Number.MAX_SAFE_INTEGER;
70
+ if (leftRank !== rightRank) return leftRank - rightRank;
71
+ return source.indexOf(left) - source.indexOf(right);
72
+ });
73
+ }
74
+
75
+ function describeConfigKey(key, mode = 'set') {
76
+ const labelMap = {
77
+ 'gateway.base_url': 'gateway base URL',
78
+ 'gateway.api_key': 'gateway API key',
79
+ 'gateway.timeout_ms': 'gateway timeout in milliseconds',
80
+ 'gateway.max_retries': 'gateway retry count',
81
+ 'model.name': 'active model name',
82
+ 'model.max_context_tokens': 'model context token limit',
83
+ 'ui.reply_language': 'reply language',
84
+ 'execution.mode': 'execution mode',
85
+ 'execution.always_allow_tools': 'always-allowed tools',
86
+ 'execution.max_steps': 'maximum tool steps',
87
+ 'context.preflight_trigger_pct': 'preflight compact threshold',
88
+ 'context.hard_limit_pct': 'hard compact threshold',
89
+ 'context.tool_result_max_chars': 'tool result character limit',
90
+ 'context.read_file_default_lines': 'default read_file line window',
91
+ 'context.read_file_max_chars': 'read_file character limit',
92
+ 'sessions.max_sessions': 'stored session limit',
93
+ 'sessions.retention_days': 'session retention days',
94
+ 'shell.default': 'default shell',
95
+ 'shell.timeout_ms': 'shell timeout in milliseconds',
96
+ 'context.max_tokens': 'context token budget',
97
+ 'policy.safe_mode': 'safe mode switch',
98
+ 'policy.allow_dangerous_commands': 'dangerous command allowance'
99
+ };
100
+ const label = labelMap[key] || key;
101
+ return mode === 'get' ? `show the ${label}` : `set the ${label}`;
102
+ }
103
+
63
104
  const SUB_AGENT_ROLES = ['planner', 'coder', 'reviewer', 'tester'];
64
105
  const SUB_AGENT_CONTEXT_MAX_MESSAGES = 4;
65
106
  const SUB_AGENT_CONTEXT_MAX_CHARS = 1200;
@@ -76,6 +117,8 @@ function getSubAgentRolePrompt(role) {
76
117
  'You are a review sub-agent. Focus on bugs, regressions, edge cases, and missing tests.',
77
118
  'Start with the focused files or directories handed to you. Do not roam unrelated parts of the repo unless the handed-off evidence is insufficient.',
78
119
  'Use this exact output structure:',
120
+ 'Acceptance Status:',
121
+ '- <met|unmet|unverified> :: <acceptance checklist item or "none">',
79
122
  'Findings:',
80
123
  '- <bug, regression, risk, or "none">',
81
124
  'Verified:',
@@ -92,6 +135,8 @@ function getSubAgentRolePrompt(role) {
92
135
  'Prefer running concrete verification commands over only suggesting them.',
93
136
  'Start with the focused files or directories handed to you. Verify those artifacts first before scanning the wider repo.',
94
137
  'Use this exact output structure:',
138
+ 'Acceptance Status:',
139
+ '- <met|unmet|unverified> :: <acceptance checklist item or "none">',
95
140
  'Verified:',
96
141
  '- <commands run and evidence>',
97
142
  'Not Verified:',
@@ -277,6 +322,114 @@ function buildFocusedTaskNote(role, focusPaths) {
277
322
  return '';
278
323
  }
279
324
 
325
+ function normalizeGoalClauseText(value) {
326
+ return String(value || '')
327
+ .replace(/^[\s\-*0-9.)、,,:;]+/g, '')
328
+ .replace(/\s+/g, ' ')
329
+ .trim();
330
+ }
331
+
332
+ function sentenceCaseRequirement(value) {
333
+ const text = normalizeGoalClauseText(value);
334
+ if (!text) return '';
335
+ return text.charAt(0).toUpperCase() + text.slice(1);
336
+ }
337
+
338
+ function deriveGoalRequirements(goal) {
339
+ const rawGoal = String(goal || '').trim();
340
+ if (!rawGoal) return [];
341
+
342
+ const normalized = rawGoal
343
+ .replace(/\r\n?/g, '\n')
344
+ .replace(/[;。]/g, ',')
345
+ .replace(/\band then\b/gi, ',')
346
+ .replace(/\bthen\b/gi, ',')
347
+ .replace(/\bplus\b/gi, ',')
348
+ .replace(/\s+(?:and|并且|而且|以及)\s+/gi, ', ')
349
+ .replace(/\n+/g, ', ');
350
+
351
+ const roughParts = normalized
352
+ .split(/\s*,\s*/)
353
+ .map((part) => normalizeGoalClauseText(part))
354
+ .filter(Boolean);
355
+
356
+ const requirements = [];
357
+ const seen = new Set();
358
+
359
+ for (const part of roughParts) {
360
+ const lowered = part.toLowerCase();
361
+ if (/\btrim\b/.test(lowered) && !/\bwhitespace\b/.test(lowered)) {
362
+ const label = 'Trim whitespace in the returned greeting';
363
+ if (!seen.has(label)) {
364
+ seen.add(label);
365
+ requirements.push(label);
366
+ }
367
+ continue;
368
+ }
369
+ if (/\btrim\b/.test(lowered) && /\bwhitespace\b/.test(lowered)) {
370
+ const label = 'Trim whitespace in the returned greeting';
371
+ if (!seen.has(label)) {
372
+ seen.add(label);
373
+ requirements.push(label);
374
+ }
375
+ continue;
376
+ }
377
+ if (/(exclamation mark|感叹号|!)/i.test(part)) {
378
+ const label = 'Preserve the exclamation mark';
379
+ if (!seen.has(label)) {
380
+ seen.add(label);
381
+ requirements.push(label);
382
+ }
383
+ continue;
384
+ }
385
+ const label = sentenceCaseRequirement(part);
386
+ if (!label || seen.has(label)) continue;
387
+ seen.add(label);
388
+ requirements.push(label);
389
+ }
390
+
391
+ if (requirements.length === 0) {
392
+ return [sentenceCaseRequirement(rawGoal)].filter(Boolean);
393
+ }
394
+ return requirements.slice(0, 6);
395
+ }
396
+
397
+ function isLightweightAutoPlanGoal(goal, requirements = []) {
398
+ const text = String(goal || '').trim();
399
+ if (!text) return false;
400
+ if (requirements.length !== 1) return false;
401
+ if (text.length > 140) return false;
402
+ if (/\b(plan|spec|design|architecture|roadmap|strategy|migration|refactor)\b/i.test(text)) return false;
403
+ if (/\b(ensure|verify|review|test|validate|make sure|confirm)\b/i.test(text)) return false;
404
+ if (/[;。]/.test(text)) return false;
405
+ return /\b(add|update|fix|rename|trim|export|create|remove|change|implement)\b/i.test(text);
406
+ }
407
+
408
+ function buildGoalRequirementPacket(goal, role) {
409
+ const rawGoal = trimInlineText(goal, 800);
410
+ if (!rawGoal) return '';
411
+ const requirements = deriveGoalRequirements(goal);
412
+ const lines = ['Original goal:', rawGoal];
413
+ if (requirements.length > 0) {
414
+ lines.push('Acceptance checklist:');
415
+ for (const requirement of requirements) {
416
+ lines.push(`- ${requirement}`);
417
+ }
418
+ }
419
+ if (role === 'reviewer') {
420
+ lines.push('Review against the original goal, not just local code quality.');
421
+ lines.push('Check each acceptance item explicitly before deciding there are no findings.');
422
+ lines.push('If any requested behavior is missing, incorrect, or only partially implemented, report it in Findings.');
423
+ } else if (role === 'tester') {
424
+ lines.push('Verify the implementation against the original goal, not just syntax or smoke checks.');
425
+ lines.push('Check each acceptance item explicitly before calling the work verified.');
426
+ lines.push('If any requested behavior is unverified or contradicted by evidence, list it under Not Verified or Failures.');
427
+ } else if (role === 'coder') {
428
+ lines.push('Implement against the acceptance checklist, not only the broad wording of the goal.');
429
+ }
430
+ return lines.join('\n');
431
+ }
432
+
280
433
  async function pathExists(targetPath) {
281
434
  try {
282
435
  await fs.access(targetPath);
@@ -466,7 +619,16 @@ function normalizeAutoPlan(parsed, goal) {
466
619
 
467
620
  function enforceAutoPlanGuardrailSteps(plan, goal) {
468
621
  const source = Array.isArray(plan?.steps) ? plan.steps : [];
622
+ const requirements = deriveGoalRequirements(goal);
623
+ const lightweightGoal = isLightweightAutoPlanGoal(goal, requirements);
469
624
  const implementationSteps = source.filter((step) => step.role !== 'reviewer' && step.role !== 'tester');
625
+ const primaryImplementationStep =
626
+ implementationSteps.find((step) => step.role === 'coder') ||
627
+ implementationSteps[0] || {
628
+ title: 'Implement requested change',
629
+ role: 'coder',
630
+ task: `Implement the requested change for: ${goal}`
631
+ };
470
632
  const reviewerStep = source.find((step) => step.role === 'reviewer') || {
471
633
  title: 'Review implementation',
472
634
  role: 'reviewer',
@@ -478,6 +640,13 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
478
640
  task: `Test and verify the completed work for: ${goal}. Start with the artifacts produced by earlier implementation steps, run the most relevant checks available, report concrete evidence, and call out anything still unverified.`
479
641
  };
480
642
 
643
+ if (lightweightGoal) {
644
+ return {
645
+ summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
646
+ steps: [primaryImplementationStep, testerStep]
647
+ };
648
+ }
649
+
481
650
  return {
482
651
  summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
483
652
  steps: [...implementationSteps.slice(0, 6), reviewerStep, testerStep]
@@ -487,11 +656,67 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
487
656
  function looksLikeSuccessfulStepOutput(text = '') {
488
657
  const value = String(text || '').trim();
489
658
  if (!value) return false;
490
- if (/(^|\n)\s*(error|failures?)\s*:\s*(?!none\b)/i.test(value)) return false;
491
- if (/(^|\n)\s*next action\s*:\s*-\s*retry\b/i.test(value)) return false;
659
+ const failureBullet = extractSectionFirstBullet(value, 'Failures');
660
+ const errorBullet = extractSectionFirstBullet(value, 'Error');
661
+ const nextActionBullet = extractSectionFirstBullet(value, 'Next Action');
662
+ const acceptanceFailures = extractAcceptanceStatusItems(value).filter((item) => item.status !== 'met');
663
+ if (errorBullet && !/^none\b/i.test(errorBullet)) return false;
664
+ if (failureBullet && !/^none\b/i.test(failureBullet)) return false;
665
+ if (acceptanceFailures.length > 0) return false;
666
+ if (nextActionBullet && /^retry\b/i.test(nextActionBullet)) return false;
492
667
  return true;
493
668
  }
494
669
 
670
+ function stepOutputHasFailureSignals(role, text = '') {
671
+ const value = String(text || '').trim();
672
+ if (!value) return true;
673
+ const errorBullet = extractSectionFirstBullet(value, 'Error');
674
+ const failureBullet = extractSectionFirstBullet(value, 'Failures');
675
+ const findingsBullet = extractSectionFirstBullet(value, 'Findings');
676
+ const nextActionBullet = extractSectionFirstBullet(value, 'Next Action');
677
+ const acceptanceFailures = extractAcceptanceStatusItems(value).filter((item) => item.status !== 'met');
678
+ if (errorBullet && !/^none\b/i.test(errorBullet)) return true;
679
+ if (failureBullet && !/^none\b/i.test(failureBullet)) return true;
680
+ if (acceptanceFailures.length > 0) return true;
681
+ if (role === 'reviewer' && findingsBullet && !/^none\b/i.test(findingsBullet)) return true;
682
+ if (nextActionBullet && /^(fix|retry|correct|repair)\b/i.test(nextActionBullet)) return true;
683
+ return false;
684
+ }
685
+
686
+ function extractSectionFirstBullet(text = '', heading = '') {
687
+ const escaped = String(heading || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
688
+ const match = String(text || '').match(new RegExp(String.raw`(^|\n)\s*${escaped}\s*:\s*(?:\n|\r\n?)+\s*-\s*([^\n\r]+)`, 'i'));
689
+ return String(match?.[2] || '').trim();
690
+ }
691
+
692
+ function extractSectionBullets(text = '', heading = '') {
693
+ const escaped = String(heading || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
694
+ const value = String(text || '');
695
+ const headingMatch = value.match(new RegExp(String.raw`(^|\n)\s*${escaped}\s*:\s*(?:\n|\r\n?)`, 'i'));
696
+ if (!headingMatch || headingMatch.index == null) return [];
697
+ const start = headingMatch.index + headingMatch[0].length;
698
+ const after = value.slice(start);
699
+ const nextHeading = after.search(/\n\s*[A-Za-z][A-Za-z ]+\s*:\s*(?:\n|\r\n?)/);
700
+ const section = nextHeading === -1 ? after : after.slice(0, nextHeading);
701
+ return section
702
+ .split(/\r?\n/)
703
+ .map((line) => line.match(/^\s*-\s*(.+)$/)?.[1]?.trim() || '')
704
+ .filter(Boolean);
705
+ }
706
+
707
+ function extractAcceptanceStatusItems(text = '') {
708
+ return extractSectionBullets(text, 'Acceptance Status')
709
+ .map((item) => {
710
+ const match = String(item).match(/^(met|unmet|unverified)\s*::\s*(.+)$/i);
711
+ if (!match) return null;
712
+ return {
713
+ status: String(match[1] || '').toLowerCase(),
714
+ label: String(match[2] || '').trim()
715
+ };
716
+ })
717
+ .filter(Boolean);
718
+ }
719
+
495
720
  function buildAutoPlanSystemSummary(auto) {
496
721
  const statusTitle =
497
722
  auto.failedCount > 0 ? 'Auto plan finished with failures' : auto.warningCount > 0 ? 'Auto plan finished with warnings' : 'Auto plan finished';
@@ -579,6 +804,10 @@ async function buildAutoPlanFinalSummary({
579
804
  }
580
805
  const fallbackSummary = fallbackParts.join(' ');
581
806
 
807
+ if (runItems.some((item) => item.failed || item.error)) {
808
+ return fallbackSummary;
809
+ }
810
+
582
811
  try {
583
812
  const result = await createChatCompletion({
584
813
  baseUrl: config.gateway.base_url,
@@ -1033,6 +1262,7 @@ async function askModel({
1033
1262
  async function runSubAgentTask({
1034
1263
  role,
1035
1264
  task,
1265
+ goal = '',
1036
1266
  priorSteps = [],
1037
1267
  parentSession,
1038
1268
  config,
@@ -1047,8 +1277,18 @@ async function runSubAgentTask({
1047
1277
  const handoffPacket = buildStepArtifactPacket(priorSteps, role);
1048
1278
  const handoffFocusPaths = collectStepArtifacts(priorSteps, role)?.focusPaths || [];
1049
1279
  const focusedTaskNote = buildFocusedTaskNote(role, handoffFocusPaths);
1280
+ const goalRequirementPacket = buildGoalRequirementPacket(goal, role);
1050
1281
  const verificationPacket = role === 'tester' ? await buildTesterVerificationPacket(handoffFocusPaths) : '';
1051
- const scopedTask = [contextPacket, evidencePacket, handoffPacket, verificationPacket, focusedTaskNote, 'Task:', task]
1282
+ const scopedTask = [
1283
+ contextPacket,
1284
+ goalRequirementPacket,
1285
+ evidencePacket,
1286
+ handoffPacket,
1287
+ verificationPacket,
1288
+ focusedTaskNote,
1289
+ 'Task:',
1290
+ task
1291
+ ]
1052
1292
  .filter(Boolean)
1053
1293
  .join('\n\n');
1054
1294
  let blockedCount = 0;
@@ -1105,6 +1345,7 @@ async function buildAutoPlanAndRun({
1105
1345
  onAgentEvent,
1106
1346
  sessionId
1107
1347
  }) {
1348
+ const requirementPacket = buildGoalRequirementPacket(goal, 'planner');
1108
1349
  const plannerPrompt =
1109
1350
  'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester","task":"..."}]}. No markdown. Always include final reviewer and tester steps.';
1110
1351
  let autoPlan = {
@@ -1127,7 +1368,13 @@ async function buildAutoPlanAndRun({
1127
1368
  { role: 'system', content: `${systemPrompt}\n${plannerPrompt}` },
1128
1369
  {
1129
1370
  role: 'user',
1130
- content: `Create an execution plan and assign best sub-agent role for each step.\nGoal: ${goal}\nThe final steps must include review and testing/verification.`
1371
+ content: [
1372
+ 'Create an execution plan and assign best sub-agent role for each step.',
1373
+ requirementPacket,
1374
+ 'The final steps must include review and testing/verification unless the goal is a tiny single-change task, in which case you may keep only one implementation step plus one testing/verification step.'
1375
+ ]
1376
+ .filter(Boolean)
1377
+ .join('\n')
1131
1378
  }
1132
1379
  ],
1133
1380
  timeoutMs: config.gateway.timeout_ms || 90000,
@@ -1153,6 +1400,7 @@ async function buildAutoPlanAndRun({
1153
1400
  const stepResult = await runSubAgentTask({
1154
1401
  role: step.role,
1155
1402
  task: step.task,
1403
+ goal,
1156
1404
  priorSteps: runItems,
1157
1405
  parentSession: session,
1158
1406
  config,
@@ -1161,14 +1409,20 @@ async function buildAutoPlanAndRun({
1161
1409
  onAgentEvent
1162
1410
  });
1163
1411
  const outputLooksSuccessful = looksLikeSuccessfulStepOutput(stepResult.text);
1412
+ const outputHasFailureSignals = stepOutputHasFailureSignals(step.role, stepResult.text);
1164
1413
  const warningParts = [];
1165
1414
  if (stepResult.blockedCount > 0) warningParts.push(`${stepResult.blockedCount} blocked tool call(s)`);
1166
1415
  if (stepResult.toolErrorCount > 0) warningParts.push(`${stepResult.toolErrorCount} tool error(s)`);
1167
1416
  const warning = warningParts.length > 0 ? `sub-agent recovered after ${warningParts.join(', ')}` : '';
1168
- const failed = stepResult.hasErrorLine || (!outputLooksSuccessful && (stepResult.blockedCount > 0 || stepResult.toolErrorCount > 0));
1417
+ const failed =
1418
+ stepResult.hasErrorLine ||
1419
+ outputHasFailureSignals ||
1420
+ (!outputLooksSuccessful && (stepResult.blockedCount > 0 || stepResult.toolErrorCount > 0));
1169
1421
  let error = '';
1170
1422
  if (stepResult.hasErrorLine) {
1171
1423
  error = 'sub-agent output contains error line(s)';
1424
+ } else if (outputHasFailureSignals) {
1425
+ error = 'sub-agent output reports unmet requirements or failed verification';
1172
1426
  } else if (failed && stepResult.blockedCount > 0) {
1173
1427
  error = `sub-agent ended with ${stepResult.blockedCount} blocked tool call(s)`;
1174
1428
  } else if (failed && stepResult.toolErrorCount > 0) {
@@ -1324,11 +1578,13 @@ export async function createChatRuntime({
1324
1578
  const configKeyHints = [
1325
1579
  'gateway.base_url',
1326
1580
  'gateway.api_key',
1581
+ 'model.name',
1582
+ 'ui.reply_language',
1583
+ 'execution.mode',
1584
+ 'shell.default',
1327
1585
  'gateway.timeout_ms',
1328
1586
  'gateway.max_retries',
1329
- 'model.name',
1330
1587
  'model.max_context_tokens',
1331
- 'execution.mode',
1332
1588
  'execution.always_allow_tools',
1333
1589
  'execution.max_steps',
1334
1590
  'context.preflight_trigger_pct',
@@ -1338,13 +1594,34 @@ export async function createChatRuntime({
1338
1594
  'context.read_file_max_chars',
1339
1595
  'sessions.max_sessions',
1340
1596
  'sessions.retention_days',
1341
- 'shell.default',
1342
1597
  'shell.timeout_ms',
1343
1598
  'context.max_tokens',
1344
1599
  'policy.safe_mode',
1345
1600
  'policy.allow_dangerous_commands'
1346
1601
  ];
1347
1602
 
1603
+ const commandPriorityOrder = [
1604
+ '/help',
1605
+ '/status',
1606
+ '/config',
1607
+ '/mode',
1608
+ '/plan',
1609
+ '/tasks',
1610
+ '/history',
1611
+ '/checkpoint',
1612
+ '/agents',
1613
+ '/compact',
1614
+ '/debug',
1615
+ '/retry'
1616
+ ];
1617
+ const configSubcommandPriority = ['/config set', '/config get', '/config list', '/config reset'];
1618
+ const configSubcommandDescriptions = {
1619
+ '/config set': 'update a config value',
1620
+ '/config get': 'show a config value',
1621
+ '/config list': 'print the full config',
1622
+ '/config reset': 'reset config to defaults'
1623
+ };
1624
+
1348
1625
  const listCommandNames = () => {
1349
1626
  const builtins = [
1350
1627
  { name: 'help', description: 'show chat help' },
@@ -1423,10 +1700,23 @@ export async function createChatRuntime({
1423
1700
  '/status'
1424
1701
  ];
1425
1702
  const compactKey = (value) => String(value || '').toLowerCase().replace(/[\/\s<>?]/g, '');
1703
+ const commandDescriptions = new Map();
1704
+ const registerSuggestion = (value, description = '') => {
1705
+ commandDescriptions.set(value, description);
1706
+ return { value, description };
1707
+ };
1708
+ const materializeSuggestions = (items) =>
1709
+ (Array.isArray(items) ? items : []).map((item) => {
1710
+ if (item && typeof item === 'object' && 'value' in item) return item;
1711
+ const value = String(item || '');
1712
+ return { value, description: commandDescriptions.get(value) || '' };
1713
+ });
1426
1714
  const matchCompactTemplates = (value) => {
1427
1715
  const needle = compactKey(value);
1428
1716
  if (!needle) return [];
1429
- return slashTemplates.filter((template) => compactKey(template).startsWith(needle));
1717
+ return materializeSuggestions(
1718
+ slashTemplates.filter((template) => compactKey(template).startsWith(needle))
1719
+ );
1430
1720
  };
1431
1721
 
1432
1722
  const getCompletionOptions = (rawInput) => {
@@ -1438,26 +1728,53 @@ export async function createChatRuntime({
1438
1728
  const tokens = body.trim().split(/\s+/).filter(Boolean);
1439
1729
  const commandPart = tokens[0] || '';
1440
1730
 
1441
- const allCommands = listCommandNames().map((c) => c.name);
1731
+ const allCommandEntries = listCommandNames();
1732
+ const allCommands = allCommandEntries.map((c) => c.name);
1733
+ for (const entry of allCommandEntries) {
1734
+ registerSuggestion(`/${entry.name}`, entry.description || '');
1735
+ }
1736
+ for (const template of configTemplates) {
1737
+ registerSuggestion(template, configSubcommandDescriptions[template] || 'config command');
1738
+ }
1739
+ for (const template of historyTemplates) registerSuggestion(template, 'history command');
1740
+ for (const template of modeTemplates) registerSuggestion(template, 'switch execution mode');
1741
+ for (const template of taskTemplates) registerSuggestion(template, 'task board command');
1742
+ for (const template of checkpointTemplates) registerSuggestion(template, 'checkpoint command');
1743
+ for (const template of specTemplates) registerSuggestion(template, 'create a spec file');
1744
+ for (const template of planTemplates) registerSuggestion(template, 'planning command');
1745
+ for (const template of agentTemplates) registerSuggestion(template, 'sub-agent command');
1746
+ for (const template of debugTemplates) registerSuggestion(template, 'debug command');
1747
+ for (const template of compactTemplates) registerSuggestion(template, 'context compaction command');
1748
+ registerSuggestion('/retry', 'retry the last user request');
1749
+ registerSuggestion('/status', 'show runtime status');
1442
1750
 
1443
1751
  if (!commandPart) {
1444
- return allCommands.map((name) => `/${name}`);
1752
+ return materializeSuggestions(prioritizeByPreferredOrder(
1753
+ allCommands.map((name) => `/${name}`),
1754
+ commandPriorityOrder
1755
+ ));
1445
1756
  }
1446
1757
 
1447
1758
  if (tokens.length === 1 && !hasTrailingSpace) {
1448
- const direct = allCommands
1449
- .filter((name) => name.startsWith(commandPart))
1450
- .map((name) => `/${name}`);
1451
- if (direct.length > 0) return direct;
1759
+ const direct = prioritizeByPreferredOrder(
1760
+ allCommands
1761
+ .filter((name) => name.startsWith(commandPart))
1762
+ .map((name) => `/${name}`),
1763
+ commandPriorityOrder
1764
+ );
1765
+ if (direct.length > 0) return materializeSuggestions(direct);
1452
1766
  return matchCompactTemplates(input);
1453
1767
  }
1454
1768
 
1455
1769
  if (commandPart === 'config') {
1456
1770
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1457
1771
  const sub = tokens[1] || '';
1458
- return ['list', 'get', 'set', 'reset']
1459
- .filter((s) => s.startsWith(sub))
1460
- .map((s) => `/config ${s}`);
1772
+ return materializeSuggestions(prioritizeByPreferredOrder(
1773
+ ['set', 'get', 'list', 'reset']
1774
+ .filter((s) => s.startsWith(sub))
1775
+ .map((s) => registerSuggestion(`/config ${s}`, configSubcommandDescriptions[`/config ${s}`] || 'config command').value),
1776
+ configSubcommandPriority
1777
+ ));
1461
1778
  }
1462
1779
 
1463
1780
  const sub = tokens[1] || '';
@@ -1465,96 +1782,98 @@ export async function createChatRuntime({
1465
1782
  const keyPrefix = tokens[2] || '';
1466
1783
  return configKeyHints
1467
1784
  .filter((k) => k.startsWith(keyPrefix))
1468
- .map((k) => `/config get ${k}`);
1785
+ .map((k) => registerSuggestion(`/config get ${k}`, describeConfigKey(k, 'get')));
1469
1786
  }
1470
1787
  if (sub === 'set') {
1471
1788
  const keyPrefix = tokens[2] || '';
1472
1789
  return configKeyHints
1473
1790
  .filter((k) => k.startsWith(keyPrefix))
1474
- .map((k) => `/config set ${k} `);
1791
+ .map((k) => registerSuggestion(`/config set ${k} `, describeConfigKey(k, 'set')));
1475
1792
  }
1476
1793
 
1477
- return configTemplates;
1794
+ return materializeSuggestions(configTemplates);
1478
1795
  }
1479
1796
 
1480
1797
  if (commandPart === 'compact') {
1481
1798
  const joined = tokens.slice(1).join(' ');
1482
1799
  return compactOptions
1483
1800
  .filter((opt) => opt.includes(joined) || joined === '')
1484
- .map((opt) => `/compact ${opt}`);
1801
+ .map((opt) => registerSuggestion(`/compact ${opt}`, 'context compaction command'));
1485
1802
  }
1486
1803
 
1487
1804
  if (commandPart === 'retry') {
1488
- return ['/retry'];
1805
+ return [registerSuggestion('/retry', 'retry the last user request')];
1489
1806
  }
1490
1807
  if (commandPart === 'status') {
1491
- return ['/status'];
1808
+ return [registerSuggestion('/status', 'show runtime status')];
1492
1809
  }
1493
1810
  if (commandPart === 'mode') {
1494
1811
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1495
1812
  const sub = tokens[1] || '';
1496
1813
  return ['normal', 'auto', 'plan']
1497
1814
  .filter((m) => m.startsWith(sub))
1498
- .map((m) => `/mode ${m}`);
1815
+ .map((m) => registerSuggestion(`/mode ${m}`, 'switch execution mode'));
1499
1816
  }
1500
- return modeTemplates;
1817
+ return materializeSuggestions(modeTemplates);
1501
1818
  }
1502
1819
  if (commandPart === 'tasks') {
1503
1820
  if (tokens.length <= 2 && !hasTrailingSpace) {
1504
1821
  const sub = tokens[1] || '';
1505
1822
  return ['add', 'start', 'done', 'remove', 'rm', 'clear']
1506
1823
  .filter((s) => s.startsWith(sub))
1507
- .map((s) => `/tasks ${s}`);
1824
+ .map((s) => registerSuggestion(`/tasks ${s}`, 'task board command'));
1508
1825
  }
1509
- return taskTemplates;
1826
+ return materializeSuggestions(taskTemplates);
1510
1827
  }
1511
1828
  if (commandPart === 'checkpoint') {
1512
1829
  if (tokens.length <= 2 && !hasTrailingSpace) {
1513
1830
  const sub = tokens[1] || '';
1514
1831
  return ['create', 'list', 'load']
1515
1832
  .filter((s) => s.startsWith(sub))
1516
- .map((s) => `/checkpoint ${s}`);
1833
+ .map((s) => registerSuggestion(`/checkpoint ${s}`, 'checkpoint command'));
1517
1834
  }
1518
1835
  if (tokens[1] === 'list') {
1519
1836
  const hint = tokens[2] || '';
1520
- return ['--all'].filter((v) => v.startsWith(hint)).map((v) => `/checkpoint list ${v}`);
1837
+ return ['--all']
1838
+ .filter((v) => v.startsWith(hint))
1839
+ .map((v) => registerSuggestion(`/checkpoint list ${v}`, 'checkpoint command'));
1521
1840
  }
1522
1841
  if (tokens[1] === 'load') {
1523
1842
  if (tokens.length >= 3) {
1524
1843
  const hint = tokens[3] || '';
1525
1844
  return ['--all']
1526
1845
  .filter((v) => v.startsWith(hint))
1527
- .map((v) => `/checkpoint load ${tokens[2]} ${v}`);
1846
+ .map((v) => registerSuggestion(`/checkpoint load ${tokens[2]} ${v}`, 'checkpoint command'));
1528
1847
  }
1529
1848
  }
1530
- return checkpointTemplates;
1849
+ return materializeSuggestions(checkpointTemplates);
1531
1850
  }
1532
1851
  if (commandPart === 'spec') {
1533
- return specTemplates;
1852
+ return materializeSuggestions(specTemplates);
1534
1853
  }
1535
1854
  if (commandPart === 'plan') {
1536
1855
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1537
1856
  const sub = tokens[1] || '';
1538
1857
  return ['auto', 'from-spec']
1539
1858
  .filter((s) => s.startsWith(sub))
1540
- .map((s) => `/plan ${s}`);
1859
+ .map((s) => registerSuggestion(`/plan ${s}`, 'planning command'));
1541
1860
  }
1542
- return planTemplates;
1861
+ return materializeSuggestions(planTemplates);
1543
1862
  }
1544
1863
  if (commandPart === 'agents') {
1545
1864
  if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
1546
1865
  const sub = tokens[1] || '';
1547
1866
  return ['list', 'run']
1548
1867
  .filter((s) => s.startsWith(sub))
1549
- .map((s) => `/agents ${s}`);
1868
+ .map((s) => registerSuggestion(`/agents ${s}`, 'sub-agent command'));
1550
1869
  }
1551
1870
  if (tokens[1] === 'run') {
1552
1871
  const rolePrefix = tokens[2] || '';
1553
1872
  return ['planner', 'coder', 'reviewer', 'tester']
1554
1873
  .filter((r) => r.startsWith(rolePrefix))
1555
- .map((r) => `/agents run ${r} `);
1874
+ .map((r) => registerSuggestion(`/agents run ${r} `, 'sub-agent command'));
1556
1875
  }
1557
- return agentTemplates;
1876
+ return materializeSuggestions(agentTemplates);
1558
1877
  }
1559
1878
 
1560
1879
  if (commandPart === 'history') {
@@ -1571,12 +1890,13 @@ export async function createChatRuntime({
1571
1890
  .filter((session) => String(session.id || '').startsWith(idPrefix))
1572
1891
  .map((session) => ({
1573
1892
  value: `/history resume ${session.id}`,
1574
- display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs`
1893
+ display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs`,
1894
+ description: 'resume a saved session'
1575
1895
  }));
1576
1896
  if (dynamic.length > 0) return dynamic;
1577
- return historyTemplates;
1897
+ return materializeSuggestions(historyTemplates);
1578
1898
  }
1579
- return historyTemplates;
1899
+ return materializeSuggestions(historyTemplates);
1580
1900
  }
1581
1901
 
1582
1902
  if (commandPart === 'debug') {
@@ -1586,9 +1906,9 @@ export async function createChatRuntime({
1586
1906
  const action = tokens[2] || '';
1587
1907
  return ['on', 'off', 'status']
1588
1908
  .filter((v) => v.startsWith(action))
1589
- .map((v) => `/debug keys ${v}`);
1909
+ .map((v) => registerSuggestion(`/debug keys ${v}`, 'keyboard debug command'));
1590
1910
  }
1591
- return debugTemplates;
1911
+ return materializeSuggestions(debugTemplates);
1592
1912
  }
1593
1913
 
1594
1914
  return [];
@@ -1630,7 +1950,7 @@ export async function createChatRuntime({
1630
1950
  };
1631
1951
 
1632
1952
  const submit = async (line, onAgentEvent) => {
1633
- const activeBaseSystemPrompt = baseSystemPrompt;
1953
+ const activeBaseSystemPrompt = buildSystemPromptWithReplyLanguage(baseSystemPrompt, config);
1634
1954
  const activeReplySystemPrompt = await buildSystemPromptWithSoul(baseSystemPrompt, config);
1635
1955
  try {
1636
1956
  await appendInputHistory(line);