@yeaft/webchat-agent 0.1.250 → 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 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.250",
3
+ "version": "0.1.251",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",