mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat: Enhance CLI output handling with structured Intermediate Representation (IR)
- Introduced `CliOutputUnit` and `IOutputParser` interfaces for unified output processing. - Implemented `PlainTextParser` and `JsonLinesParser` for parsing raw CLI output into structured units. - Updated `executeCliTool` to utilize output parsers and handle structured output. - Added `flattenOutputUnits` utility for extracting clean output from structured data. - Enhanced `ConversationTurn` and `ExecutionRecord` interfaces to include structured output. - Created comprehensive documentation for CLI Output Converter usage and integration. - Improved error handling and type mapping for various output formats.
This commit is contained in:
@@ -203,6 +203,168 @@
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
/* Tool Tags - displayed in tool cards */
|
||||
.tool-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.tool-tag {
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 500;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
color: hsl(var(--primary));
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Tags Input - used in config modal */
|
||||
.tags-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Unified tag input - tags and input in one container */
|
||||
.tags-unified-input {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
min-height: 2.5rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
cursor: text;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.tags-unified-input:focus-within {
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.1);
|
||||
}
|
||||
|
||||
.tag-inline-input {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--foreground));
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.tag-inline-input::placeholder {
|
||||
color: hsl(var(--muted-foreground) / 0.6);
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
min-height: 1.75rem;
|
||||
padding: 0.25rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: hsl(var(--primary) / 0.15);
|
||||
color: hsl(var(--primary));
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.tag-remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: hsl(var(--primary) / 0.6);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1;
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.tag-remove:hover {
|
||||
background: hsl(var(--destructive) / 0.2);
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
/* Predefined Tags Row - prominent quick add buttons */
|
||||
.predefined-tags-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.predefined-tag-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.375rem 0.625rem;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.predefined-tag-btn:hover {
|
||||
background: hsl(var(--primary) / 0.15);
|
||||
color: hsl(var(--primary));
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.predefined-tag-btn i {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Legacy predefined tags */
|
||||
.predefined-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.predefined-tag {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
color: hsl(var(--muted-foreground));
|
||||
border: 1px dashed hsl(var(--border));
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.predefined-tag:hover {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
color: hsl(var(--primary));
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.tool-item-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -549,6 +549,77 @@
|
||||
color: hsl(200 70% 70%);
|
||||
}
|
||||
|
||||
/* ===== Backend ChunkType Badges (CliOutputUnit.type) ===== */
|
||||
|
||||
/* Thought/Thinking Message (from JSONL parser) */
|
||||
.cli-stream-line.formatted.thought {
|
||||
background: hsl(280 50% 20% / 0.3);
|
||||
border-left: 3px solid hsl(280 70% 65%);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cli-msg-badge.cli-msg-thought {
|
||||
background: hsl(280 70% 65% / 0.2);
|
||||
color: hsl(280 70% 75%);
|
||||
}
|
||||
|
||||
/* Code Block Message */
|
||||
.cli-stream-line.formatted.code {
|
||||
background: hsl(220 40% 18% / 0.4);
|
||||
border-left: 3px solid hsl(220 60% 55%);
|
||||
font-family: var(--font-mono, 'Consolas', 'Monaco', 'Courier New', monospace);
|
||||
}
|
||||
|
||||
.cli-msg-badge.cli-msg-code {
|
||||
background: hsl(220 60% 55% / 0.25);
|
||||
color: hsl(220 60% 70%);
|
||||
}
|
||||
|
||||
/* File Diff Message */
|
||||
.cli-stream-line.formatted.file_diff {
|
||||
background: hsl(35 50% 18% / 0.4);
|
||||
border-left: 3px solid hsl(35 80% 55%);
|
||||
}
|
||||
|
||||
.cli-msg-badge.cli-msg-file_diff {
|
||||
background: hsl(35 80% 55% / 0.25);
|
||||
color: hsl(35 80% 65%);
|
||||
}
|
||||
|
||||
/* Progress Message */
|
||||
.cli-stream-line.formatted.progress {
|
||||
background: hsl(190 40% 18% / 0.3);
|
||||
border-left: 3px solid hsl(190 70% 50%);
|
||||
}
|
||||
|
||||
.cli-msg-badge.cli-msg-progress {
|
||||
background: hsl(190 70% 50% / 0.2);
|
||||
color: hsl(190 70% 65%);
|
||||
}
|
||||
|
||||
/* Metadata Message */
|
||||
.cli-stream-line.formatted.metadata {
|
||||
background: hsl(250 30% 18% / 0.3);
|
||||
border-left: 3px solid hsl(250 50% 60%);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.cli-msg-badge.cli-msg-metadata {
|
||||
background: hsl(250 50% 60% / 0.2);
|
||||
color: hsl(250 50% 75%);
|
||||
}
|
||||
|
||||
/* Stderr Message (Error) */
|
||||
.cli-stream-line.formatted.stderr {
|
||||
background: hsl(0 50% 20% / 0.4);
|
||||
border-left: 3px solid hsl(0 70% 55%);
|
||||
}
|
||||
|
||||
.cli-msg-badge.cli-msg-stderr {
|
||||
background: hsl(0 70% 55% / 0.25);
|
||||
color: hsl(0 70% 70%);
|
||||
}
|
||||
|
||||
/* Inline Code */
|
||||
.cli-inline-code {
|
||||
padding: 1px 5px;
|
||||
|
||||
@@ -346,16 +346,50 @@ function renderFormattedLine(line, searchFilter) {
|
||||
// Format inline code
|
||||
content = content.replace(/`([^`]+)`/g, '<code class="cli-inline-code">$1</code>');
|
||||
|
||||
// Build type badge if has prefix
|
||||
const typeBadge = parsed.hasPrefix ?
|
||||
`<span class="cli-msg-badge cli-msg-${parsed.type}">
|
||||
// Type badge icons for backend chunkType (CliOutputUnit.type)
|
||||
const CHUNK_TYPE_ICONS = {
|
||||
thought: 'brain',
|
||||
code: 'code',
|
||||
file_diff: 'git-compare',
|
||||
progress: 'loader',
|
||||
system: 'settings',
|
||||
stderr: 'alert-circle',
|
||||
metadata: 'info'
|
||||
};
|
||||
|
||||
// Type badge labels for backend chunkType
|
||||
const CHUNK_TYPE_LABELS = {
|
||||
thought: 'Thinking',
|
||||
code: 'Code',
|
||||
file_diff: 'Diff',
|
||||
progress: 'Progress',
|
||||
system: 'System',
|
||||
stderr: 'Error',
|
||||
metadata: 'Info'
|
||||
};
|
||||
|
||||
// Build type badge - prioritize content prefix, then fall back to chunkType
|
||||
let typeBadge = '';
|
||||
let lineClass = '';
|
||||
|
||||
if (parsed.hasPrefix) {
|
||||
// Content has Chinese prefix like [系统], [思考], etc.
|
||||
typeBadge = `<span class="cli-msg-badge cli-msg-${parsed.type}">
|
||||
<i data-lucide="${MESSAGE_TYPE_ICONS[parsed.type] || 'circle'}"></i>
|
||||
<span>${parsed.label}</span>
|
||||
</span>` : '';
|
||||
|
||||
// Determine line class based on original type and parsed type
|
||||
const lineClass = parsed.hasPrefix ? `cli-stream-line formatted ${parsed.type}` :
|
||||
`cli-stream-line ${line.type}`;
|
||||
</span>`;
|
||||
lineClass = `cli-stream-line formatted ${parsed.type}`;
|
||||
} else if (line.type && line.type !== 'stdout' && CHUNK_TYPE_LABELS[line.type]) {
|
||||
// No content prefix, but backend sent a meaningful chunkType
|
||||
typeBadge = `<span class="cli-msg-badge cli-msg-${line.type}">
|
||||
<i data-lucide="${CHUNK_TYPE_ICONS[line.type] || 'circle'}"></i>
|
||||
<span>${CHUNK_TYPE_LABELS[line.type]}</span>
|
||||
</span>`;
|
||||
lineClass = `cli-stream-line formatted ${line.type}`;
|
||||
} else {
|
||||
// Plain stdout, no badge
|
||||
lineClass = `cli-stream-line ${line.type || 'stdout'}`;
|
||||
}
|
||||
|
||||
return `<div class="${lineClass}">${typeBadge}<span class="cli-msg-content">${content}</span></div>`;
|
||||
}
|
||||
|
||||
@@ -1721,6 +1721,28 @@ const i18n = {
|
||||
'apiSettings.modelIdExists': 'Model ID already exists',
|
||||
'apiSettings.useModelTreeToManage': 'Use the model tree to manage individual models',
|
||||
|
||||
// CLI Settings
|
||||
'apiSettings.cliSettings': 'CLI Settings',
|
||||
'apiSettings.addCliSettings': 'Add CLI Settings',
|
||||
'apiSettings.editCliSettings': 'Edit CLI Settings',
|
||||
'apiSettings.noCliSettings': 'No CLI settings configured',
|
||||
'apiSettings.noCliSettingsSelected': 'No CLI Settings Selected',
|
||||
'apiSettings.cliSettingsHint': 'Select a CLI settings endpoint or create a new one',
|
||||
'apiSettings.cliProviderHint': 'Select an Anthropic provider to use its API key and base URL',
|
||||
'apiSettings.noAnthropicProviders': 'No Anthropic providers configured. Please add one in the Providers tab first.',
|
||||
'apiSettings.selectProviderFirst': 'Select a provider first',
|
||||
'apiSettings.providerRequired': 'Provider is required',
|
||||
'apiSettings.modelRequired': 'Model is required',
|
||||
'apiSettings.providerNotFound': 'Provider not found',
|
||||
'apiSettings.settingsSaved': 'Settings saved successfully',
|
||||
'apiSettings.settingsDeleted': 'Settings deleted successfully',
|
||||
'apiSettings.confirmDeleteSettings': 'Are you sure you want to delete this CLI settings?',
|
||||
'apiSettings.endpointName': 'Endpoint Name',
|
||||
'apiSettings.envSettings': 'Environment Settings',
|
||||
'apiSettings.settingsFilePath': 'Settings File Path',
|
||||
'apiSettings.nameRequired': 'Name is required',
|
||||
'apiSettings.status': 'Status',
|
||||
|
||||
// Common
|
||||
'common.cancel': 'Cancel',
|
||||
'common.optional': '(Optional)',
|
||||
@@ -3777,6 +3799,29 @@ const i18n = {
|
||||
'apiSettings.modelIdExists': '模型 ID 已存在',
|
||||
'apiSettings.useModelTreeToManage': '使用模型树管理各个模型',
|
||||
|
||||
// CLI Settings
|
||||
'apiSettings.cliSettings': 'CLI 配置',
|
||||
'apiSettings.addCliSettings': '添加 CLI 配置',
|
||||
'apiSettings.editCliSettings': '编辑 CLI 配置',
|
||||
'apiSettings.noCliSettings': '未配置 CLI 设置',
|
||||
'apiSettings.noCliSettingsSelected': '未选择 CLI 配置',
|
||||
'apiSettings.cliSettingsHint': '选择一个 CLI 配置端点或创建新的',
|
||||
'apiSettings.cliProviderHint': '选择一个 Anthropic 供应商以使用其 API 密钥和基础 URL',
|
||||
'apiSettings.noAnthropicProviders': '未配置 Anthropic 供应商。请先在供应商标签页中添加。',
|
||||
'apiSettings.selectProviderFirst': '请先选择供应商',
|
||||
'apiSettings.providerRequired': '供应商为必填项',
|
||||
'apiSettings.modelRequired': '模型为必填项',
|
||||
'apiSettings.providerNotFound': '未找到供应商',
|
||||
'apiSettings.settingsSaved': '设置保存成功',
|
||||
'apiSettings.settingsDeleted': '设置删除成功',
|
||||
'apiSettings.confirmDeleteSettings': '确定要删除此 CLI 配置吗?',
|
||||
'apiSettings.endpointName': '端点名称',
|
||||
'apiSettings.envSettings': '环境变量设置',
|
||||
'apiSettings.settingsFilePath': '配置文件路径',
|
||||
'apiSettings.nameRequired': '名称为必填项',
|
||||
'apiSettings.tokenRequired': 'API 令牌为必填项',
|
||||
'apiSettings.status': '状态',
|
||||
|
||||
// Common
|
||||
'common.cancel': '取消',
|
||||
'common.optional': '(可选)',
|
||||
|
||||
@@ -3737,23 +3737,94 @@ function renderCliSettingsEmptyState() {
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available Anthropic providers
|
||||
*/
|
||||
function getAvailableAnthropicProviders() {
|
||||
if (!apiSettingsData || !apiSettingsData.providers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return apiSettingsData.providers.filter(function(p) {
|
||||
return p.type === 'anthropic' && p.enabled;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build provider options HTML for CLI Settings
|
||||
*/
|
||||
function buildCliProviderOptions(selectedProviderId) {
|
||||
var providers = getAvailableAnthropicProviders();
|
||||
var optionsHtml = '<option value="">' + t('apiSettings.selectProvider') + '</option>';
|
||||
|
||||
providers.forEach(function(provider) {
|
||||
var isSelected = provider.id === selectedProviderId ? ' selected' : '';
|
||||
optionsHtml += '<option value="' + escapeHtml(provider.id) + '"' + isSelected + '>' + escapeHtml(provider.name) + '</option>';
|
||||
});
|
||||
|
||||
return optionsHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build model options HTML for CLI Settings based on selected provider
|
||||
*/
|
||||
function buildCliModelOptions(providerId, selectedModel) {
|
||||
var providers = getAvailableAnthropicProviders();
|
||||
var provider = providers.find(function(p) { return p.id === providerId; });
|
||||
|
||||
if (!provider || !provider.llmModels || provider.llmModels.length === 0) {
|
||||
return '<option value="">' + t('apiSettings.selectProviderFirst') + '</option>';
|
||||
}
|
||||
|
||||
var optionsHtml = '';
|
||||
provider.llmModels.forEach(function(model) {
|
||||
var isSelected = model.id === selectedModel ? ' selected' : '';
|
||||
optionsHtml += '<option value="' + escapeHtml(model.id) + '"' + isSelected + '>' + escapeHtml(model.name || model.id) + '</option>';
|
||||
});
|
||||
|
||||
return optionsHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update CLI Settings model dropdown when provider changes
|
||||
*/
|
||||
function onCliProviderChange() {
|
||||
var providerId = document.getElementById('cli-settings-provider').value;
|
||||
var modelSelect = document.getElementById('cli-settings-model');
|
||||
|
||||
if (modelSelect) {
|
||||
modelSelect.innerHTML = buildCliModelOptions(providerId, '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Add CLI Settings Modal
|
||||
*/
|
||||
function showAddCliSettingsModal(existingEndpoint) {
|
||||
var isEdit = !!existingEndpoint;
|
||||
var settings = existingEndpoint ? existingEndpoint.settings : { env: {}, model: 'sonnet' };
|
||||
var env = settings.env || {};
|
||||
var settings = existingEndpoint ? existingEndpoint.settings : { env: {}, model: '' };
|
||||
var selectedProviderId = settings.providerId || '';
|
||||
var providerOptionsHtml = buildCliProviderOptions(selectedProviderId);
|
||||
var modelOptionsHtml = buildCliModelOptions(selectedProviderId, settings.model);
|
||||
|
||||
// Check if any Anthropic providers are configured
|
||||
var hasProviders = getAvailableAnthropicProviders().length > 0;
|
||||
var noProvidersWarning = !hasProviders ?
|
||||
'<div class="info-message" style="margin-bottom: 1rem;">' +
|
||||
'<i data-lucide="alert-circle"></i>' +
|
||||
'<span>' + t('apiSettings.noAnthropicProviders') + '</span>' +
|
||||
'</div>' : '';
|
||||
|
||||
var modalHtml =
|
||||
'<div class="modal-overlay" onclick="closeModal(event)">' +
|
||||
'<div class="modal" onclick="event.stopPropagation()">' +
|
||||
'<div class="modal-header">' +
|
||||
'<h2>' + (isEdit ? t('apiSettings.editCliSettings') : t('apiSettings.addCliSettings')) + '</h2>' +
|
||||
'<button class="modal-close" onclick="closeCliSettingsModal()">×</button>' +
|
||||
'<div class="generic-modal-overlay active" id="cliSettingsModal">' +
|
||||
'<div class="generic-modal">' +
|
||||
'<div class="generic-modal-header">' +
|
||||
'<h3 class="generic-modal-title">' + (isEdit ? t('apiSettings.editCliSettings') : t('apiSettings.addCliSettings')) + '</h3>' +
|
||||
'<button class="generic-modal-close" onclick="closeCliSettingsModal()">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="modal-body">' +
|
||||
'<form id="cli-settings-form">' +
|
||||
'<div class="generic-modal-body">' +
|
||||
noProvidersWarning +
|
||||
'<form id="cli-settings-form" class="api-settings-form">' +
|
||||
(isEdit ? '<input type="hidden" id="cli-settings-id" value="' + existingEndpoint.id + '">' : '') +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-name">' + t('apiSettings.endpointName') + ' *</label>' +
|
||||
@@ -3764,20 +3835,17 @@ function showAddCliSettingsModal(existingEndpoint) {
|
||||
'<input type="text" id="cli-settings-description" class="cli-input" value="' + escapeHtml(existingEndpoint ? (existingEndpoint.description || '') : '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-model">' + t('apiSettings.model') + '</label>' +
|
||||
'<select id="cli-settings-model" class="cli-select">' +
|
||||
'<option value="opus"' + (settings.model === 'opus' ? ' selected' : '') + '>Claude Opus</option>' +
|
||||
'<option value="sonnet"' + (settings.model === 'sonnet' ? ' selected' : '') + '>Claude Sonnet</option>' +
|
||||
'<option value="haiku"' + (settings.model === 'haiku' ? ' selected' : '') + '>Claude Haiku</option>' +
|
||||
'<label for="cli-settings-provider">' + t('apiSettings.provider') + ' *</label>' +
|
||||
'<select id="cli-settings-provider" class="cli-input" onchange="onCliProviderChange()" required>' +
|
||||
providerOptionsHtml +
|
||||
'</select>' +
|
||||
'<small class="form-hint">' + t('apiSettings.cliProviderHint') + '</small>' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-token">ANTHROPIC_AUTH_TOKEN *</label>' +
|
||||
'<input type="password" id="cli-settings-token" class="cli-input" value="' + escapeHtml(env.ANTHROPIC_AUTH_TOKEN || '') + '" placeholder="sk-..." required />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-base-url">ANTHROPIC_BASE_URL</label>' +
|
||||
'<input type="text" id="cli-settings-base-url" class="cli-input" value="' + escapeHtml(env.ANTHROPIC_BASE_URL || '') + '" placeholder="https://api.anthropic.com/v1" />' +
|
||||
'<label for="cli-settings-model">' + t('apiSettings.model') + ' *</label>' +
|
||||
'<select id="cli-settings-model" class="cli-input" required>' +
|
||||
modelOptionsHtml +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label class="checkbox-label">' +
|
||||
@@ -3786,22 +3854,17 @@ function showAddCliSettingsModal(existingEndpoint) {
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'</form>' +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-secondary" onclick="closeCliSettingsModal()">' + t('common.cancel') + '</button>' +
|
||||
'<button class="btn btn-primary" onclick="submitCliSettings()"' + (!hasProviders ? ' disabled' : '') + '>' + (isEdit ? t('common.save') : t('common.create')) + '</button>' +
|
||||
'</div>' +
|
||||
'<div class="modal-footer">' +
|
||||
'<button class="btn btn-ghost" onclick="closeCliSettingsModal()">' + t('common.cancel') + '</button>' +
|
||||
'<button class="btn btn-primary" onclick="submitCliSettings()">' + (isEdit ? t('common.save') : t('common.create')) + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Append modal to body
|
||||
var modalsContainer = document.getElementById('modals');
|
||||
if (!modalsContainer) {
|
||||
modalsContainer = document.createElement('div');
|
||||
modalsContainer.id = 'modals';
|
||||
document.body.appendChild(modalsContainer);
|
||||
}
|
||||
modalsContainer.innerHTML = modalHtml;
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3821,10 +3884,8 @@ function editCliSettings(endpointId) {
|
||||
* Close CLI Settings Modal
|
||||
*/
|
||||
function closeCliSettingsModal() {
|
||||
var modalsContainer = document.getElementById('modals');
|
||||
if (modalsContainer) {
|
||||
modalsContainer.innerHTML = '';
|
||||
}
|
||||
var modal = document.getElementById('cliSettingsModal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3833,9 +3894,8 @@ function closeCliSettingsModal() {
|
||||
async function submitCliSettings() {
|
||||
var name = document.getElementById('cli-settings-name').value.trim();
|
||||
var description = document.getElementById('cli-settings-description').value.trim();
|
||||
var providerId = document.getElementById('cli-settings-provider').value;
|
||||
var model = document.getElementById('cli-settings-model').value;
|
||||
var token = document.getElementById('cli-settings-token').value.trim();
|
||||
var baseUrl = document.getElementById('cli-settings-base-url').value.trim();
|
||||
var enabled = document.getElementById('cli-settings-enabled').checked;
|
||||
var idInput = document.getElementById('cli-settings-id');
|
||||
var id = idInput ? idInput.value : null;
|
||||
@@ -3845,26 +3905,42 @@ async function submitCliSettings() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
showRefreshToast(t('apiSettings.tokenRequired'), 'error');
|
||||
if (!providerId) {
|
||||
showRefreshToast(t('apiSettings.providerRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
showRefreshToast(t('apiSettings.modelRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get provider configuration
|
||||
var providers = getAvailableAnthropicProviders();
|
||||
var provider = providers.find(function(p) { return p.id === providerId; });
|
||||
|
||||
if (!provider) {
|
||||
showRefreshToast(t('apiSettings.providerNotFound'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build settings from provider
|
||||
var data = {
|
||||
name: name,
|
||||
description: description,
|
||||
enabled: enabled,
|
||||
settings: {
|
||||
env: {
|
||||
ANTHROPIC_AUTH_TOKEN: token,
|
||||
ANTHROPIC_AUTH_TOKEN: provider.apiKey || '',
|
||||
DISABLE_AUTOUPDATER: '1'
|
||||
},
|
||||
model: model
|
||||
model: model,
|
||||
providerId: providerId // Store for editing
|
||||
}
|
||||
};
|
||||
|
||||
if (baseUrl) {
|
||||
data.settings.env.ANTHROPIC_BASE_URL = baseUrl;
|
||||
if (provider.apiBase) {
|
||||
data.settings.env.ANTHROPIC_BASE_URL = provider.apiBase;
|
||||
}
|
||||
|
||||
if (id) {
|
||||
@@ -3889,6 +3965,7 @@ window.showAddCliSettingsModal = showAddCliSettingsModal;
|
||||
window.editCliSettings = editCliSettings;
|
||||
window.closeCliSettingsModal = closeCliSettingsModal;
|
||||
window.submitCliSettings = submitCliSettings;
|
||||
window.onCliProviderChange = onCliProviderChange;
|
||||
|
||||
|
||||
// ========== Utility Functions ==========
|
||||
|
||||
@@ -330,6 +330,25 @@ function buildToolConfigModalContent(tool, config, models, status) {
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Tags Section - Unified input with inline tags
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>Tags <span class="text-muted">(optional labels)</span></h4>' +
|
||||
'<div class="tags-unified-input" id="tagsUnifiedInput">' +
|
||||
(config.tags || []).map(function(tag) {
|
||||
return '<span class="tag-item">' + escapeHtml(tag) + '<button type="button" class="tag-remove" data-tag="' + escapeHtml(tag) + '">×</button></span>';
|
||||
}).join('') +
|
||||
'<input type="text" id="tagInput" class="tag-inline-input" placeholder="输入标签按 Enter 添加" />' +
|
||||
'</div>' +
|
||||
'<div class="predefined-tags-row">' +
|
||||
'<button type="button" class="predefined-tag-btn" data-tag="分析"><i data-lucide="search" class="w-3 h-3"></i> 分析</button>' +
|
||||
'<button type="button" class="predefined-tag-btn" data-tag="编码"><i data-lucide="code" class="w-3 h-3"></i> 编码</button>' +
|
||||
'<button type="button" class="predefined-tag-btn" data-tag="Debug"><i data-lucide="bug" class="w-3 h-3"></i> Debug</button>' +
|
||||
'<button type="button" class="predefined-tag-btn" data-tag="重构"><i data-lucide="refresh-cw" class="w-3 h-3"></i> 重构</button>' +
|
||||
'<button type="button" class="predefined-tag-btn" data-tag="测试"><i data-lucide="check-square" class="w-3 h-3"></i> 测试</button>' +
|
||||
'<button type="button" class="predefined-tag-btn" data-tag="文档"><i data-lucide="file-text" class="w-3 h-3"></i> 文档</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Footer
|
||||
'<div class="tool-config-footer">' +
|
||||
'<button class="btn btn-outline" onclick="closeModal()">' + t('common.cancel') + '</button>' +
|
||||
@@ -341,6 +360,79 @@ function buildToolConfigModalContent(tool, config, models, status) {
|
||||
}
|
||||
|
||||
function initToolConfigModalEvents(tool, currentConfig, models) {
|
||||
// Local tags state (copy from config)
|
||||
var currentTags = (currentConfig.tags || []).slice();
|
||||
|
||||
// Helper to render tags inline with input
|
||||
function renderTags() {
|
||||
var container = document.getElementById('tagsUnifiedInput');
|
||||
var input = document.getElementById('tagInput');
|
||||
if (!container) return;
|
||||
|
||||
// Remove existing tag items but keep the input
|
||||
container.querySelectorAll('.tag-item').forEach(function(el) { el.remove(); });
|
||||
|
||||
// Insert tags before the input
|
||||
currentTags.forEach(function(tag) {
|
||||
var tagEl = document.createElement('span');
|
||||
tagEl.className = 'tag-item';
|
||||
tagEl.innerHTML = escapeHtml(tag) + '<button type="button" class="tag-remove" data-tag="' + escapeHtml(tag) + '">×</button>';
|
||||
container.insertBefore(tagEl, input);
|
||||
});
|
||||
|
||||
// Re-attach remove handlers
|
||||
container.querySelectorAll('.tag-remove').forEach(function(btn) {
|
||||
btn.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var tagToRemove = this.getAttribute('data-tag');
|
||||
currentTags = currentTags.filter(function(t) { return t !== tagToRemove; });
|
||||
renderTags();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Click on unified input container focuses the input
|
||||
var unifiedInput = document.getElementById('tagsUnifiedInput');
|
||||
if (unifiedInput) {
|
||||
unifiedInput.onclick = function(e) {
|
||||
if (e.target === this) {
|
||||
document.getElementById('tagInput').focus();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Tag input handler
|
||||
var tagInput = document.getElementById('tagInput');
|
||||
if (tagInput) {
|
||||
tagInput.onkeydown = function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
var newTag = this.value.trim();
|
||||
if (newTag && currentTags.indexOf(newTag) === -1) {
|
||||
currentTags.push(newTag);
|
||||
renderTags();
|
||||
}
|
||||
this.value = '';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Predefined tag click handlers
|
||||
document.querySelectorAll('.predefined-tag-btn').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
var tag = this.getAttribute('data-tag');
|
||||
if (tag && currentTags.indexOf(tag) === -1) {
|
||||
currentTags.push(tag);
|
||||
renderTags();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Initialize tags display
|
||||
renderTags();
|
||||
// Initialize lucide icons for predefined buttons
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
// Toggle Enable/Disable
|
||||
var toggleBtn = document.getElementById('toggleEnableBtn');
|
||||
if (toggleBtn) {
|
||||
@@ -426,10 +518,15 @@ function initToolConfigModalEvents(tool, currentConfig, models) {
|
||||
try {
|
||||
await updateCliToolConfig(tool, {
|
||||
primaryModel: primaryModel,
|
||||
secondaryModel: secondaryModel
|
||||
secondaryModel: secondaryModel,
|
||||
tags: currentTags
|
||||
});
|
||||
// Reload config to reflect changes
|
||||
await loadCliToolConfig();
|
||||
showRefreshToast('Configuration saved', 'success');
|
||||
closeModal();
|
||||
renderToolsSection();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
} catch (err) {
|
||||
showRefreshToast('Failed to save: ' + err.message, 'error');
|
||||
}
|
||||
@@ -554,35 +651,42 @@ function renderToolsSection() {
|
||||
var toolDescriptions = {
|
||||
gemini: t('cli.geminiDesc'),
|
||||
qwen: t('cli.qwenDesc'),
|
||||
codex: t('cli.codexDesc')
|
||||
codex: t('cli.codexDesc'),
|
||||
claude: t('cli.claudeDesc') || 'Anthropic Claude Code CLI for AI-assisted development',
|
||||
opencode: t('cli.opencodeDesc') || 'OpenCode CLI - Multi-provider AI coding assistant'
|
||||
};
|
||||
|
||||
var tools = ['gemini', 'qwen', 'codex'];
|
||||
var tools = ['gemini', 'qwen', 'codex', 'claude', 'opencode'];
|
||||
var available = Object.values(cliToolStatus).filter(function(t) { return t.available; }).length;
|
||||
|
||||
var toolsHtml = tools.map(function(tool) {
|
||||
var status = cliToolStatus[tool] || {};
|
||||
var isAvailable = status.available;
|
||||
var isDefault = defaultCliTool === tool;
|
||||
var toolConfig = cliToolConfig && cliToolConfig.tools ? cliToolConfig.tools[tool] : null;
|
||||
var tags = toolConfig && toolConfig.tags ? toolConfig.tags : [];
|
||||
|
||||
// Build tags HTML
|
||||
var tagsHtml = tags.length > 0
|
||||
? '<div class="tool-tags">' + tags.map(function(tag) {
|
||||
return '<span class="tool-tag">' + escapeHtml(tag) + '</span>';
|
||||
}).join('') + '</div>'
|
||||
: '';
|
||||
|
||||
return '<div class="tool-item clickable ' + (isAvailable ? 'available' : 'unavailable') + '" onclick="showToolConfigModal(\'' + tool + '\')">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (isAvailable ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">' + tool.charAt(0).toUpperCase() + tool.slice(1) +
|
||||
(isDefault ? '<span class="tool-default-badge">' + t('cli.default') + '</span>' : '') +
|
||||
'<i data-lucide="settings" class="w-3 h-3 tool-config-icon"></i>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-desc">' + toolDescriptions[tool] + '</div>' +
|
||||
tagsHtml +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
(isAvailable
|
||||
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> ' + t('cli.ready') + '</span>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> ' + t('cli.notInstalled') + '</span>') +
|
||||
(isAvailable && !isDefault
|
||||
? '<button class="btn-sm btn-outline" onclick="event.stopPropagation(); setDefaultCliTool(\'' + tool + '\')"><i data-lucide="star" class="w-3 h-3"></i> ' + t('cli.setDefault') + '</button>'
|
||||
: '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
Reference in New Issue
Block a user