diff --git a/soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/WebController.java b/soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/WebController.java
index 14b6e8549f1c997d692ba1eb5a6f270fab56d93b..bbf8c5a0611d1b7277d048e2fa6dea7fca901c88 100644
--- a/soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/WebController.java
+++ b/soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/WebController.java
@@ -1218,6 +1218,11 @@ public class WebController {
item.put("name", definition.getName());
item.put("description", definition.getDescription());
item.put("type", "subagent");
+ // Try to read hint from the agent's markdown file
+ String hint = readHintFromAgent(definition.getName());
+ if (hint != null && !hint.isEmpty()) {
+ item.put("hint", hint);
+ }
data.add(item);
}
@@ -1252,6 +1257,55 @@ public class WebController {
return Result.succeed(data);
}
+ /**
+ * 从智能体的 markdown 文件中读取 hint 字段。
+ */
+ private String readHintFromAgent(String name) {
+ if (name == null || name.isEmpty()) return "";
+ try {
+ // Check user scope first, then workspace
+ Path userRoot = Paths.get(AgentFlags.getUserHome(), AgentFlags.getHarnessAgents()).toAbsolutePath().normalize();
+ Path userFile = userRoot.resolve(name + ".md").normalize();
+ if (userFile.startsWith(userRoot) && Files.exists(userFile) && !Files.isDirectory(userFile)) {
+ String md = new String(Files.readAllBytes(userFile));
+ String hint = extractFrontMatterValue(md, "hint");
+ if (hint != null && !hint.isEmpty()) return hint;
+ }
+ Path workspaceRoot = Paths.get(engine().getWorkspace(), AgentFlags.getHarnessAgents()).toAbsolutePath().normalize();
+ Path workspaceFile = workspaceRoot.resolve(name + ".md").normalize();
+ if (workspaceFile.startsWith(workspaceRoot) && Files.exists(workspaceFile) && !Files.isDirectory(workspaceFile)) {
+ String md = new String(Files.readAllBytes(workspaceFile));
+ String hint = extractFrontMatterValue(md, "hint");
+ if (hint != null && !hint.isEmpty()) return hint;
+ }
+ } catch (Exception e) {
+ // Silently ignore read errors
+ }
+ return "";
+ }
+
+ /**
+ * Extract a specific key's value from markdown front matter.
+ */
+ private String extractFrontMatterValue(String markdown, String key) {
+ if (markdown == null) return "";
+ String normalized = markdown.replace("\r\n", "\n").replace('\r', '\n');
+ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile(
+ "^" + key + "\\s*:\\s*(.*)$",
+ java.util.regex.Pattern.MULTILINE
+ ).matcher(normalized);
+ if (matcher.find()) {
+ String value = matcher.group(1).trim();
+ if (value.startsWith("\"") && value.endsWith("\"") && value.length() >= 2) {
+ value = value.substring(1, value.length() - 1);
+ } else if (value.startsWith("'") && value.endsWith("'") && value.length() >= 2) {
+ value = value.substring(1, value.length() - 1);
+ }
+ return value;
+ }
+ return "";
+ }
+
/**
* 聊天输入入口:解析请求参数后路由到 WebGate 处理。
*
接收用户输入的文本消息、附件文件、模型选择、推理选项和会话标识,
diff --git a/soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/settings/AgentSettingsController.java b/soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/settings/AgentSettingsController.java
index 03acf80d6d9eb922c456846d6d86aa578e9c1540..2a24908440b144efe0e737c90a75cbc18e6eb1f8 100644
--- a/soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/settings/AgentSettingsController.java
+++ b/soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/settings/AgentSettingsController.java
@@ -48,6 +48,7 @@ public class AgentSettingsController extends BaseSettingsController {
private static final int MAX_FILE_SIZE = 512 * 1024;
private static final String BUILTIN_RESOURCE_BASE = "META-INF/solon/ai/harness/";
private static final String[] BUILTIN_NAMES = {"general", "explore", "bash", "plan", "git-summary"};
+ private static final Pattern HINT_PATTERN = Pattern.compile("^hint\\s*:\\s*(.*)$", Pattern.MULTILINE);
private static final Pattern TOP_LEVEL_KEY_PATTERN = Pattern.compile("^[A-Za-z_][A-Za-z0-9_-]*\\s*:.*$");
private static final Logger LOG = LoggerFactory.getLogger(AgentSettingsController.class);
@@ -109,7 +110,7 @@ public class AgentSettingsController extends BaseSettingsController {
String markdown = new String(Files.readAllBytes(file), StandardCharsets.UTF_8);
try {
AgentDefinition definition = parseAndValidate(markdown, name);
- Map data = toItem(definition, normalizeScope(scope), false);
+ Map data = toItem(definition, normalizeScope(scope), false, markdown);
data.put("systemPrompt", definition.getSystemPrompt());
data.put("editable", true);
return Result.succeed(data);
@@ -119,6 +120,7 @@ public class AgentSettingsController extends BaseSettingsController {
data.put("description", "");
data.put("tools", Collections.emptyList());
data.put("systemPrompt", extractSystemPrompt(markdown));
+ data.put("hint", extractFrontMatterValue(markdown, "hint"));
data.put("scope", normalizeScope(scope));
data.put("editable", true);
data.put("valid", false);
@@ -238,7 +240,9 @@ public class AgentSettingsController extends BaseSettingsController {
String description = root.get("description").getString();
String systemPrompt = root.get("systemPrompt").getString();
String model = root.get("model").getString();
+ String hint = root.get("hint").getString();
if (model != null) model = model.trim();
+ if (hint != null) hint = hint.trim();
List tools = readStringList(root.get("tools"));
String originalName = root.get("originalName").getString();
String originalScopeValue = root.get("originalScope").getString();
@@ -280,7 +284,7 @@ public class AgentSettingsController extends BaseSettingsController {
}
}
- String markdown = buildMarkdown(name, description, tools, model, systemPrompt, originalMarkdown);
+ String markdown = buildMarkdown(name, description, tools, model, systemPrompt, originalMarkdown, hint);
if (markdown.getBytes(StandardCharsets.UTF_8).length > MAX_FILE_SIZE) return Result.failure("智能体文件最大允许 512 KB");
parseAndValidate(markdown, name);
@@ -336,13 +340,14 @@ public class AgentSettingsController extends BaseSettingsController {
if (Files.size(file) > MAX_FILE_SIZE) throw new IllegalArgumentException("文件超过 512 KB");
String markdown = new String(Files.readAllBytes(file), StandardCharsets.UTF_8);
AgentDefinition definition = parseAndValidate(markdown, name);
- item = toItem(definition, scope, false);
+ item = toItem(definition, scope, false, markdown);
item.put("sourceScope", scope);
item.put("editable", true);
} catch (Exception e) {
item = new LinkedHashMap<>();
item.put("name", name);
item.put("description", e.getMessage());
+ item.put("hint", "");
item.put("scope", scope);
item.put("sourceScope", scope);
item.put("builtin", false);
@@ -368,6 +373,10 @@ public class AgentSettingsController extends BaseSettingsController {
}
private Map toItem(AgentDefinition definition, String scope, boolean builtin) {
+ return toItem(definition, scope, builtin, null);
+ }
+
+ private Map toItem(AgentDefinition definition, String scope, boolean builtin, String markdown) {
Map item = new LinkedHashMap<>();
item.put("name", definition.getName());
item.put("description", definition.getDescription());
@@ -379,9 +388,35 @@ public class AgentSettingsController extends BaseSettingsController {
item.put("scope", scope);
item.put("builtin", builtin);
item.put("valid", true);
+ if (markdown != null) {
+ item.put("hint", extractFrontMatterValue(markdown, "hint"));
+ }
return item;
}
+ /**
+ * Extract a specific key's value from markdown front matter.
+ */
+ private String extractFrontMatterValue(String markdown, String key) {
+ if (markdown == null) return "";
+ String normalized = markdown.replace("\r\n", "\n").replace('\r', '\n');
+ java.util.regex.Matcher matcher = Pattern.compile(
+ "^" + key + "\\s*:\\s*(.*)$",
+ Pattern.MULTILINE
+ ).matcher(normalized);
+ if (matcher.find()) {
+ String value = matcher.group(1).trim();
+ // Remove surrounding quotes if present
+ if (value.startsWith("\"") && value.endsWith("\"") && value.length() >= 2) {
+ value = value.substring(1, value.length() - 1);
+ } else if (value.startsWith("'") && value.endsWith("'") && value.length() >= 2) {
+ value = value.substring(1, value.length() - 1);
+ }
+ return value;
+ }
+ return "";
+ }
+
private AgentDefinition findBuiltin(String name) {
return loadBuiltinDefinitions().get(name);
}
@@ -423,9 +458,15 @@ public class AgentSettingsController extends BaseSettingsController {
private String buildMarkdown(String name, String description, List tools,
String model, String systemPrompt, String originalMarkdown) {
+ return buildMarkdown(name, description, tools, model, systemPrompt, originalMarkdown, null);
+ }
+
+ private String buildMarkdown(String name, String description, List tools,
+ String model, String systemPrompt, String originalMarkdown, String hint) {
// model != null 表示前端明确接管模型设置(含清空);null 表示保留旧 front matter 中的 model
boolean takeoverModel = model != null;
- List preservedLines = extractPreservedFrontMatter(originalMarkdown, takeoverModel);
+ boolean takeoverHint = hint != null;
+ List preservedLines = extractPreservedFrontMatter(originalMarkdown, takeoverModel, takeoverHint);
StringBuilder markdown = new StringBuilder();
markdown.append("---\n");
markdown.append("name: ").append(ONode.ofBean(name).toJson()).append('\n');
@@ -434,6 +475,9 @@ public class AgentSettingsController extends BaseSettingsController {
if (!Assert.isEmpty(model)) {
markdown.append("model: ").append(ONode.ofBean(model).toJson()).append('\n');
}
+ if (!Assert.isEmpty(hint)) {
+ markdown.append("hint: ").append(ONode.ofBean(hint).toJson()).append('\n');
+ }
for (String line : preservedLines) {
markdown.append(line).append('\n');
}
@@ -479,10 +523,14 @@ public class AgentSettingsController extends BaseSettingsController {
}
private List extractPreservedFrontMatter(String markdown) {
- return extractPreservedFrontMatter(markdown, true);
+ return extractPreservedFrontMatter(markdown, true, true);
}
private List extractPreservedFrontMatter(String markdown, boolean takeoverModel) {
+ return extractPreservedFrontMatter(markdown, takeoverModel, true);
+ }
+
+ private List extractPreservedFrontMatter(String markdown, boolean takeoverModel, boolean takeoverHint) {
List result = new ArrayList<>();
if (Assert.isEmpty(markdown)) return result;
String normalized = markdown.replace("\r\n", "\n").replace('\r', '\n');
@@ -497,7 +545,8 @@ public class AgentSettingsController extends BaseSettingsController {
if (topLevel) {
String key = line.substring(0, line.indexOf(':')).trim();
skip = "name".equals(key) || "description".equals(key)
- || "tools".equals(key) || (takeoverModel && "model".equals(key));
+ || "tools".equals(key) || (takeoverModel && "model".equals(key))
+ || (takeoverHint && "hint".equals(key));
}
if (!skip) result.add(line);
}
diff --git a/soloncode-cli/src/main/resources/static/css/app.css b/soloncode-cli/src/main/resources/static/css/app.css
index c05118ead2024c10f572c51e96f6299984f1e732..64175c8f7a57b840fe31e3c9814676675f3ca046 100644
--- a/soloncode-cli/src/main/resources/static/css/app.css
+++ b/soloncode-cli/src/main/resources/static/css/app.css
@@ -341,7 +341,8 @@ body {
.newchat-avatar img { width: 100px; height: 100px; object-fit: contain; border-radius: 24px; }
@keyframes blob-spin { to { transform: rotate(360deg); } }
.newchat-title { font-size: var(--fs-4xl); font-weight: 700; margin-bottom: 10px; }
-.newchat-sub { color: var(--text-secondary); font-size: var(--fs-lg); margin-bottom: 36px; }
+.newchat-sub { color: var(--text-secondary); font-size: var(--fs-lg); margin-bottom: 12px; }
+.newchat-hint { color: var(--text-primary); font-size: var(--fs); margin-bottom: 36px; text-align: center; max-width: 520px; line-height: 1.6; }
.newchat-input-box { width: 100%; max-width: 780px; background: var(--bg-input-box); border: 1px solid var(--border-input); border-radius: 16px; box-shadow: var(--shadow-lg); display: flex; flex-direction: column; transition: all 0.25s; }
.newchat-input-box:focus-within { border-color: var(--accent); box-shadow: var(--shadow-lg), 0 0 0 4px rgba(79,110,247,0.08); }
.newchat-input-box textarea { border: none; outline: none; resize: none; background: transparent; padding: 18px 20px 8px; font-family: var(--font-sans); font-size: var(--fs-lg); color: var(--text-primary); min-height: calc(54px * var(--font-scale)); max-height: calc(140px * var(--font-scale)); line-height: 1.6; }
@@ -2715,7 +2716,8 @@ body.memory-active .main-header { display: none; }
.input-wrap { padding: 8px 12px 16px; }
.newchat-title { font-size: calc(22px * var(--font-scale)); }
.onboarding-home-title { font-size: calc(22px * var(--font-scale)); }
- .newchat-sub { font-size: var(--fs-base); margin-bottom: 24px; }
+ .newchat-sub { font-size: var(--fs-base); margin-bottom: 12px; }
+ .newchat-hint { font-size: var(--fs-sm); margin-bottom: 24px; }
.newchat-avatar { width: 80px; height: 80px; }
.newchat-avatar img { width: 80px; height: 80px; }
.main-header { padding: 12px 16px 10px; }
diff --git a/soloncode-cli/src/main/resources/static/i18n/en.json b/soloncode-cli/src/main/resources/static/i18n/en.json
index 702ca344bd28fbc2123ee0e4000a704e48df81f4..5cb7854efa9ff52e2704f2c026e63ee8101a6141 100644
--- a/soloncode-cli/src/main/resources/static/i18n/en.json
+++ b/soloncode-cli/src/main/resources/static/i18n/en.json
@@ -50,6 +50,9 @@
"agents.modelFollowDefault": "Follow chat",
"agents.modelSelect": "Select run model",
"agents.modelHint": "Leave empty to run this agent with the chat's selected model.",
+ "agents.hint": "Hint",
+ "agents.hintPlaceholder": "Optional. A short description of the agent's purpose, shown in the selector dropdown",
+ "agents.hintHint": "When set, this text will appear as a hint in the agent selector to help users understand the agent's purpose.",
"agents.systemPrompt": "System Prompt",
"agents.systemPromptRequired": "Please enter a system prompt",
"agents.title": "Agents",
diff --git a/soloncode-cli/src/main/resources/static/i18n/zh-CN.json b/soloncode-cli/src/main/resources/static/i18n/zh-CN.json
index 92552e9125f0b31d8bd6706d19f0c57a550b155d..051fb5adb0251c6e2ac15e7893ebddf022b27edf 100644
--- a/soloncode-cli/src/main/resources/static/i18n/zh-CN.json
+++ b/soloncode-cli/src/main/resources/static/i18n/zh-CN.json
@@ -50,6 +50,9 @@
"agents.modelFollowDefault": "跟随对话",
"agents.modelSelect": "选择运行模型",
"agents.modelHint": "留空时该智能体跟随对话选择的模型运行。",
+ "agents.hint": "提示(hint)",
+ "agents.hintPlaceholder": "选填,简短描述该智能体的用途,在选择器下拉列表中显示",
+ "agents.hintHint": "设置后,在智能体选择器中会显示为提示文本,帮助用户了解该智能体的用途。",
"agents.systemPrompt": "系统提示词",
"agents.systemPromptRequired": "请填写系统提示词",
"agents.title": "智能体",
diff --git a/soloncode-cli/src/main/resources/static/js/app-history.js b/soloncode-cli/src/main/resources/static/js/app-history.js
index 27e10339785e8356b33363ac41efbf567fb46610..c00b2b971c9a1a32268a82d47f6f28d94361726e 100644
--- a/soloncode-cli/src/main/resources/static/js/app-history.js
+++ b/soloncode-cli/src/main/resources/static/js/app-history.js
@@ -569,6 +569,9 @@ function loadCommands() {
commandList = resp.data || [];
commandsLoaded = true;
if (typeof renderAgentUI === 'function') renderAgentUI();
+ if (typeof updateAgentHint === 'function') {
+ updateAgentHint(getSelectedAgent());
+ }
} catch (e) {}
});
}
@@ -1716,10 +1719,47 @@ function selectAgent(agentName) {
var sid = getSessionKey();
sessionAgentMap[sid] = agentName || '';
renderAgentUI();
+ updateAgentHint(agentName);
// 选择时调后端记住(与模型选择器保持一致)
postAgentSelect({ sessionId: sid, agentName: agentName || '' });
}
+function updateAgentHint(agentName) {
+ var $hint = $('#newChatHint');
+ var $avatar = $('.newchat-avatar');
+ var $title = $('#newChatTitle');
+ var $sub = $('#newChatSub');
+ if (!agentName) {
+ $avatar.show();
+ $title.show();
+ $sub.show();
+ $hint.hide();
+ return;
+ }
+ // 从 commandList 中查找 hint
+ var hint = '';
+ for (var i = 0; i < commandList.length; i++) {
+ var item = commandList[i];
+ if (item.type === 'subagent' && item.name === agentName && item.hint) {
+ hint = item.hint;
+ break;
+ }
+ }
+ if (hint) {
+ $avatar.hide();
+ $title.hide();
+ $sub.hide();
+ // 渲染 markdown
+ var html = marked.parse(hint);
+ $hint.html(html).show();
+ } else {
+ $avatar.show();
+ $title.show();
+ $sub.show();
+ $hint.hide();
+ }
+}
+
function initAgentSelector(selectorId, currentId, dropdownId) {
var $selector = $('#' + selectorId);
var $current = $('#' + currentId);
diff --git a/soloncode-cli/src/main/resources/static/js/app-settings-agents.js b/soloncode-cli/src/main/resources/static/js/app-settings-agents.js
index 6b0afabc08ca6e37bd3e8197f97c8618dfe11ad2..fb6624dd49295cde219e70fbce66d997922b946a 100644
--- a/soloncode-cli/src/main/resources/static/js/app-settings-agents.js
+++ b/soloncode-cli/src/main/resources/static/js/app-settings-agents.js
@@ -277,6 +277,7 @@
sourceScope = null;
$('#agentsName').val('').prop('readOnly', false).removeClass('readonly-gray');
$('#agentsDescription').val('').prop('readOnly', false).removeClass('readonly-gray');
+ $('#agentsHint').val('').prop('readOnly', false).removeClass('readonly-gray');
$('#agentsSystemPrompt').val('').prop('readOnly', false).removeClass('readonly-gray');
$('#agentsToolsBtn').prop('disabled', false);
$('#agentsToolsBtn').removeClass('is-open');
@@ -305,6 +306,7 @@
showFormView(data.valid === false ? I18n.t('agents.repairConfig') : I18n.t('agents.formTitle.edit'), true);
$('#agentsName').val(name).prop('readOnly', true).addClass('readonly-gray');
$('#agentsDescription').val(data.description || '').prop('readOnly', false).removeClass('readonly-gray');
+ $('#agentsHint').val(data.hint || '').prop('readOnly', false).removeClass('readonly-gray');
$('#agentsSystemPrompt').val(data.systemPrompt || '').prop('readOnly', false).removeClass('readonly-gray');
$('#agentsToolsBtn').prop('disabled', false);
$('#agentsToolsBtn').removeClass('is-open');
@@ -327,7 +329,7 @@
showFormView(I18n.t('agents.copyAgent'), false);
$('#agentsFormActions').hide();
$('#agentsName').prop('readOnly', false).removeClass('readonly-gray').focus().select();
- $('#agentsDescription, #agentsSystemPrompt').prop('readOnly', false).removeClass('readonly-gray');
+ $('#agentsDescription, #agentsHint, #agentsSystemPrompt').prop('readOnly', false).removeClass('readonly-gray');
$('#agentsToolsBtn').prop('disabled', false);
$('#agentsToolsBtn').removeClass('is-open');
$('#agentsToolsSelector').hide().removeClass('is-open');
@@ -413,12 +415,13 @@
var name = $('#agentsName').val().trim();
var scope = $('#agentsScope').val() || 'user';
var description = $('#agentsDescription').val().trim();
+ var hint = $('#agentsHint').val().trim();
var systemPrompt = $('#agentsSystemPrompt').val().trim();
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(name)) { showToast(I18n.t('agents.nameInvalid'), 'error'); return; }
if (!description) { showToast(I18n.t('agents.descriptionRequired'), 'error'); return; }
if (!systemPrompt) { showToast(I18n.t('agents.systemPromptRequired'), 'error'); return; }
var model = ($('#agentsModel').val() || '').trim();
- var body = { name: name, scope: scope, description: description, tools: selectedTools, model: model, systemPrompt: systemPrompt };
+ var body = { name: name, scope: scope, description: description, tools: selectedTools, model: model, hint: hint, systemPrompt: systemPrompt };
if (sourceName && sourceScope) { body.sourceName = sourceName; body.sourceScope = sourceScope; body.sourceBuiltin = builtinSource; }
var isEdit = !!editName && !builtinSource;
if (isEdit) { body.originalName = editName; body.originalScope = editScope; }
diff --git a/soloncode-cli/src/main/resources/static/web.html b/soloncode-cli/src/main/resources/static/web.html
index df1603685758c0526d8f31eb32f35e9da48c81cc..5fab675442ba06b1bc51291d06b9510a111b115b 100644
--- a/soloncode-cli/src/main/resources/static/web.html
+++ b/soloncode-cli/src/main/resources/static/web.html
@@ -100,7 +100,8 @@
你好,我是 SolonCode !
- 需要什么帮助吗?
+ 需要什么帮助吗?
+