@yeaft/webchat-agent 0.1.249 → 0.1.251
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.
- package/claude.js +2 -1
- package/connection/message-router.js +4 -0
- package/context.js +2 -0
- package/conversation.js +147 -1
- package/crew/role-query.js +8 -9
- package/package.json +1 -1
package/claude.js
CHANGED
|
@@ -310,7 +310,8 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
|
|
|
310
310
|
ctx.sendToServer({
|
|
311
311
|
type: 'slash_commands_update',
|
|
312
312
|
conversationId,
|
|
313
|
-
slashCommands: state.slashCommands
|
|
313
|
+
slashCommands: state.slashCommands,
|
|
314
|
+
slashCommandDescriptions: ctx.slashCommandDescriptions
|
|
314
315
|
});
|
|
315
316
|
}
|
|
316
317
|
}
|
|
@@ -105,6 +105,10 @@ export async function handleMessage(msg) {
|
|
|
105
105
|
await handleBtwQuestion(msg);
|
|
106
106
|
break;
|
|
107
107
|
|
|
108
|
+
case 'stop_background_task':
|
|
109
|
+
await (await import('../conversation.js')).handleStopBackgroundTask(msg);
|
|
110
|
+
break;
|
|
111
|
+
|
|
108
112
|
case 'cancel_execution':
|
|
109
113
|
await handleCancelExecution(msg);
|
|
110
114
|
break;
|
package/context.js
CHANGED
|
@@ -14,6 +14,8 @@ export default {
|
|
|
14
14
|
agentCapabilities: [],
|
|
15
15
|
// Agent 级别的 slash commands 缓存(所有 conversation 共用)
|
|
16
16
|
slashCommands: [],
|
|
17
|
+
// Slash command 描述映射: { commandName: description } — 从 plugin commands/*.md 提取
|
|
18
|
+
slashCommandDescriptions: {},
|
|
17
19
|
// MCP servers 列表 (从 ~/.claude.json 读取): [{ name, enabled, source }]
|
|
18
20
|
mcpServers: [],
|
|
19
21
|
// 连接相关
|
package/conversation.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
1
4
|
import ctx from './context.js';
|
|
2
5
|
import { query } from './sdk/index.js';
|
|
3
6
|
import { loadSessionHistory } from './history.js';
|
|
@@ -7,6 +10,114 @@ import { crewSessions, loadCrewIndex } from './crew.js';
|
|
|
7
10
|
// 不支持的斜杠命令(真正需要交互式 CLI 的命令)
|
|
8
11
|
const UNSUPPORTED_SLASH_COMMANDS = ['/help', '/bug', '/login', '/logout', '/terminal-setup', '/vim', '/config'];
|
|
9
12
|
|
|
13
|
+
// 内置命令的描述(作为 fallback,这些命令 CLI 不提供 frontmatter 描述)
|
|
14
|
+
const BUILTIN_COMMAND_DESCRIPTIONS = {
|
|
15
|
+
compact: 'Compact conversation context',
|
|
16
|
+
context: 'Show context usage',
|
|
17
|
+
cost: 'Show token costs',
|
|
18
|
+
init: 'Reinitialize session',
|
|
19
|
+
review: 'Code review',
|
|
20
|
+
insights: 'Session insights',
|
|
21
|
+
'pr-comments': 'PR comment review',
|
|
22
|
+
'release-notes': 'Generate release notes',
|
|
23
|
+
'security-review': 'Security review',
|
|
24
|
+
heapdump: 'Heap dump (debug)',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load command descriptions from installed plugin commands/*.md files.
|
|
29
|
+
* Parses YAML frontmatter to extract name and description fields.
|
|
30
|
+
* Results are cached in ctx.slashCommandDescriptions.
|
|
31
|
+
*/
|
|
32
|
+
export function loadPluginCommandDescriptions() {
|
|
33
|
+
if (Object.keys(ctx.slashCommandDescriptions).length > 0) return; // already loaded
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const installedPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
37
|
+
const installed = JSON.parse(readFileSync(installedPath, 'utf-8'));
|
|
38
|
+
|
|
39
|
+
for (const [pluginKey, entries] of Object.entries(installed.plugins || {})) {
|
|
40
|
+
// pluginKey: "yeaft-skills@yeaft-skills-dev"
|
|
41
|
+
const pluginName = pluginKey.split('@')[0]; // "yeaft-skills"
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const commandsDir = join(entry.installPath, 'commands');
|
|
44
|
+
let files;
|
|
45
|
+
try {
|
|
46
|
+
files = readdirSync(commandsDir).filter(f => f.endsWith('.md'));
|
|
47
|
+
} catch { continue; }
|
|
48
|
+
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
try {
|
|
51
|
+
const content = readFileSync(join(commandsDir, file), 'utf-8');
|
|
52
|
+
const fm = parseFrontmatter(content);
|
|
53
|
+
if (fm.name && fm.description) {
|
|
54
|
+
// CLI uses pluginName:commandName format (e.g. "yeaft-skills:sprint")
|
|
55
|
+
const cliName = `${pluginName}:${fm.name}`;
|
|
56
|
+
// Take first line of description only
|
|
57
|
+
const desc = fm.description.split('\n')[0].trim();
|
|
58
|
+
ctx.slashCommandDescriptions[cliName] = desc;
|
|
59
|
+
}
|
|
60
|
+
} catch { /* skip unparseable files */ }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Add builtin descriptions
|
|
66
|
+
for (const [name, desc] of Object.entries(BUILTIN_COMMAND_DESCRIPTIONS)) {
|
|
67
|
+
if (!ctx.slashCommandDescriptions[name]) {
|
|
68
|
+
ctx.slashCommandDescriptions[name] = desc;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(`[Preload] Loaded ${Object.keys(ctx.slashCommandDescriptions).length} command descriptions`);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.warn('[Preload] Failed to load plugin command descriptions:', err.message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Minimal YAML frontmatter parser for commands/*.md files.
|
|
80
|
+
* Extracts name and description from --- delimited frontmatter.
|
|
81
|
+
*/
|
|
82
|
+
function parseFrontmatter(content) {
|
|
83
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
84
|
+
if (!match) return {};
|
|
85
|
+
|
|
86
|
+
const fm = {};
|
|
87
|
+
const lines = match[1].split('\n');
|
|
88
|
+
let currentKey = null;
|
|
89
|
+
let multilineValue = '';
|
|
90
|
+
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
// New key: value pair
|
|
93
|
+
const kvMatch = line.match(/^(\w+):\s*(.*)/);
|
|
94
|
+
if (kvMatch) {
|
|
95
|
+
// Save previous multiline value
|
|
96
|
+
if (currentKey && multilineValue) {
|
|
97
|
+
fm[currentKey] = multilineValue.trim();
|
|
98
|
+
}
|
|
99
|
+
currentKey = kvMatch[1];
|
|
100
|
+
const val = kvMatch[2].trim();
|
|
101
|
+
if (val === '|' || val === '>') {
|
|
102
|
+
multilineValue = '';
|
|
103
|
+
} else {
|
|
104
|
+
fm[currentKey] = val;
|
|
105
|
+
currentKey = null;
|
|
106
|
+
multilineValue = '';
|
|
107
|
+
}
|
|
108
|
+
} else if (currentKey && (line.startsWith(' ') || line.trim() === '')) {
|
|
109
|
+
// Continuation of multiline value
|
|
110
|
+
multilineValue += (multilineValue ? '\n' : '') + line.trimStart();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Save last multiline value
|
|
114
|
+
if (currentKey && multilineValue) {
|
|
115
|
+
fm[currentKey] = multilineValue.trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return fm;
|
|
119
|
+
}
|
|
120
|
+
|
|
10
121
|
/**
|
|
11
122
|
* Prestart Claude CLI process in background (fire-and-forget).
|
|
12
123
|
* When the query starts, processClaudeOutput will receive the system init message
|
|
@@ -33,6 +144,10 @@ function prestartClaude(conversationId, workDir, resumeSessionId) {
|
|
|
33
144
|
*/
|
|
34
145
|
export async function preloadSlashCommands(workDir, targetId = '__preload__') {
|
|
35
146
|
const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
|
|
147
|
+
|
|
148
|
+
// Eagerly load plugin command descriptions (cached, runs once)
|
|
149
|
+
loadPluginCommandDescriptions();
|
|
150
|
+
|
|
36
151
|
try {
|
|
37
152
|
const abortController = new AbortController();
|
|
38
153
|
// Use --print with a cheap built-in command to trigger system init.
|
|
@@ -58,7 +173,8 @@ export async function preloadSlashCommands(workDir, targetId = '__preload__') {
|
|
|
58
173
|
ctx.sendToServer({
|
|
59
174
|
type: 'slash_commands_update',
|
|
60
175
|
conversationId: targetId,
|
|
61
|
-
slashCommands
|
|
176
|
+
slashCommands,
|
|
177
|
+
slashCommandDescriptions: ctx.slashCommandDescriptions
|
|
62
178
|
});
|
|
63
179
|
console.log(`[Preload] ${targetId}: loaded ${slashCommands.length} slash commands from ${effectiveWorkDir}`);
|
|
64
180
|
}
|
|
@@ -435,6 +551,36 @@ export async function handleCancelExecution(msg) {
|
|
|
435
551
|
sendConversationList();
|
|
436
552
|
}
|
|
437
553
|
|
|
554
|
+
// 停止单个后台任务
|
|
555
|
+
// NOTE: This is a "soft stop" — it marks the task as stopped in our tracking state
|
|
556
|
+
// and notifies the frontend, but does NOT actually kill the underlying subprocess.
|
|
557
|
+
// The Claude SDK's abortController.abort() kills the entire session (all sub-tasks),
|
|
558
|
+
// not individual ones. Per-sub-agent abort is not supported by the SDK.
|
|
559
|
+
// TODO [P2]: Investigate SDK changes that might enable per-task cancellation.
|
|
560
|
+
export async function handleStopBackgroundTask(msg) {
|
|
561
|
+
const { conversationId, taskId } = msg;
|
|
562
|
+
|
|
563
|
+
const state = ctx.conversations.get(conversationId);
|
|
564
|
+
if (!state) return;
|
|
565
|
+
|
|
566
|
+
const taskInfo = state.backgroundTasks?.get(taskId);
|
|
567
|
+
if (!taskInfo) return;
|
|
568
|
+
|
|
569
|
+
// Soft stop: mark as stopped in tracking state (subprocess continues in background)
|
|
570
|
+
taskInfo.status = 'stopped';
|
|
571
|
+
taskInfo.endTime = Date.now();
|
|
572
|
+
|
|
573
|
+
console.log(`[Tasks] Stopped: ${taskId} in conversation ${conversationId}`);
|
|
574
|
+
|
|
575
|
+
// Notify frontend
|
|
576
|
+
ctx.sendToServer({
|
|
577
|
+
type: 'background_task_output',
|
|
578
|
+
conversationId,
|
|
579
|
+
taskId,
|
|
580
|
+
task: taskInfo
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
438
584
|
// 清空排队消息 — 已移至 server 端管理 (Phase 3.6)
|
|
439
585
|
// handleClearQueue 和 handleCancelQueuedMessage 不再需要
|
|
440
586
|
|
package/crew/role-query.js
CHANGED
|
@@ -160,15 +160,14 @@ async function _createRoleQueryInner(session, roleName) {
|
|
|
160
160
|
...(effectiveDisallowed.length > 0 && { disallowedTools: effectiveDisallowed })
|
|
161
161
|
};
|
|
162
162
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
163
|
+
// Intercept AskUserQuestion for all roles — forward to Web UI for interactive answering.
|
|
164
|
+
// Without this, non-DM roles' AskCard buttons stay disabled (no askRequestId).
|
|
165
|
+
queryOptions.canCallTool = async (toolName, input, toolCtx) => {
|
|
166
|
+
if (toolName === 'AskUserQuestion') {
|
|
167
|
+
return await handleAskUserQuestion(session.id, input, toolCtx);
|
|
168
|
+
}
|
|
169
|
+
return input;
|
|
170
|
+
};
|
|
172
171
|
|
|
173
172
|
if (savedSessionId) {
|
|
174
173
|
queryOptions.resume = savedSessionId;
|