@yeaft/webchat-agent 0.1.860 → 0.1.864

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.
@@ -37,7 +37,7 @@ import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
37
37
  import { loadMcpServers, updateMcpConfig } from '../mcp.js';
38
38
  import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../yeaft/config-api.js';
39
39
  import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
40
- import { handleYeaftGroupChat, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListGroups, handleYeaftCreateGroup, handleYeaftRenameGroup, handleYeaftUpdateGroup, handleYeaftUpdateGroupConfig, handleYeaftArchiveGroup, handleYeaftDeleteGroup, handleYeaftAddMember, handleYeaftRemoveMember, handleYeaftSetDefaultVp, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, broadcastLanguageChange } from '../yeaft/web-bridge.js';
40
+ import { handleYeaftGroupChat, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListGroups, handleYeaftCreateGroup, handleYeaftRenameGroup, handleYeaftUpdateGroup, handleYeaftUpdateGroupConfig, handleYeaftArchiveGroup, handleYeaftDeleteGroup, handleYeaftAddMember, handleYeaftRemoveMember, handleYeaftSetDefaultVp, handleYeaftChatSend, handleYeaftListChats, handleYeaftCreateChat, handleYeaftRenameChat, handleYeaftArchiveChat, handleYeaftDeleteChat, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, broadcastLanguageChange } from '../yeaft/web-bridge.js';
41
41
 
42
42
  export async function handleMessage(msg) {
43
43
  switch (msg.type) {
@@ -547,6 +547,26 @@ export async function handleMessage(msg) {
547
547
  handleYeaftSetDefaultVp(msg);
548
548
  break;
549
549
 
550
+ // Yeaft Chat Mode (1:1 single-VP) — separate from group fan-out.
551
+ case 'yeaft_chat_send':
552
+ await handleYeaftChatSend(msg);
553
+ break;
554
+ case 'yeaft_list_chats':
555
+ await handleYeaftListChats(msg);
556
+ break;
557
+ case 'yeaft_create_chat':
558
+ await handleYeaftCreateChat(msg);
559
+ break;
560
+ case 'yeaft_rename_chat':
561
+ await handleYeaftRenameChat(msg);
562
+ break;
563
+ case 'yeaft_archive_chat':
564
+ await handleYeaftArchiveChat(msg);
565
+ break;
566
+ case 'yeaft_delete_chat':
567
+ await handleYeaftDeleteChat(msg);
568
+ break;
569
+
550
570
  // wave-6b: manual dream trigger from VP detail page
551
571
  case 'yeaft_dream_trigger':
552
572
  case 'unify_dream_trigger':
package/conversation.js CHANGED
@@ -287,6 +287,11 @@ export function parseSlashCommand(message) {
287
287
  export async function sendConversationList() {
288
288
  const list = [];
289
289
  for (const [id, state] of ctx.conversations) {
290
+ let providerCaps = state.capabilities;
291
+ if (!providerCaps) {
292
+ try { providerCaps = getProvider(state.providerName || DEFAULT_PROVIDER).capabilities || null; }
293
+ catch { providerCaps = null; }
294
+ }
290
295
  const entry = {
291
296
  id,
292
297
  workDir: state.workDir,
@@ -295,7 +300,8 @@ export async function sendConversationList() {
295
300
  processing: !!state.turnActive,
296
301
  userId: state.userId,
297
302
  username: state.username,
298
- provider: state.providerName || DEFAULT_PROVIDER
303
+ provider: state.providerName || DEFAULT_PROVIDER,
304
+ capabilities: providerCaps || undefined,
299
305
  };
300
306
  list.push(entry);
301
307
  }
@@ -395,6 +401,9 @@ export async function createConversation(msg) {
395
401
  state.disallowedTools = disallowedTools || null;
396
402
  }
397
403
 
404
+ let createdCaps = null;
405
+ try { createdCaps = getProvider(provider).capabilities || null; } catch { /* noop */ }
406
+
398
407
  ctx.sendToServer({
399
408
  type: 'conversation_created',
400
409
  conversationId,
@@ -402,6 +411,7 @@ export async function createConversation(msg) {
402
411
  userId,
403
412
  username,
404
413
  provider,
414
+ capabilities: createdCaps || undefined,
405
415
  disallowedTools: disallowedTools || null
406
416
  });
407
417
 
@@ -508,6 +518,9 @@ export async function resumeConversation(msg) {
508
518
  state.disallowedTools = disallowedTools || null;
509
519
  }
510
520
 
521
+ let resumedCaps = null;
522
+ try { resumedCaps = getProvider(provider).capabilities || null; } catch { /* noop */ }
523
+
511
524
  ctx.sendToServer({
512
525
  type: 'conversation_resumed',
513
526
  conversationId,
@@ -516,7 +529,8 @@ export async function resumeConversation(msg) {
516
529
  historyMessages,
517
530
  userId,
518
531
  username,
519
- provider
532
+ provider,
533
+ capabilities: resumedCaps || undefined
520
534
  });
521
535
 
522
536
  // 立即发送 agent 级别的 MCP servers 列表
@@ -766,6 +780,23 @@ export async function handleUserInput(msg) {
766
780
  });
767
781
  }
768
782
  if (workDir) state.workDir = workDir;
783
+
784
+ // /clear for capable providers — reset session in-place without spawning new turn
785
+ if (slashCommand.type === 'slash' && slashCommand.command === '/clear') {
786
+ if (typeof driver.clear === 'function' && driver.capabilities?.clear) {
787
+ try { await driver.clear(state); } catch (err) {
788
+ console.warn(`[${conversationId}] driver.clear failed:`, err?.message || err);
789
+ }
790
+ }
791
+ ctx.sendToServer({
792
+ type: 'turn_completed',
793
+ conversationId,
794
+ claudeSessionId: state.sessionId || state.claudeSessionId,
795
+ workDir: state.workDir
796
+ });
797
+ return;
798
+ }
799
+
769
800
  sendOutput(conversationId, { type: 'user', message: { role: 'user', content: prompt } });
770
801
  state.turnActive = true;
771
802
  sendConversationList();
@@ -931,6 +962,30 @@ export function handleAskUserQuestion(conversationId, input, toolCtx) {
931
962
  * 则按 conversationId 查找该 conversation 下唯一的 pending question 并 resolve。
932
963
  */
933
964
  export function handleAskUserAnswer(msg) {
965
+ // Provider-driven permission round-trip (e.g. Copilot ACP
966
+ // session/request_permission): if any registered driver owns this
967
+ // requestId, route the answer back to its `respondToPermissionRequest`
968
+ // instead of through the generic AskUserQuestion path. Dispatch by
969
+ // capability + ownership, not by string prefix, so future providers
970
+ // (hermes-agent etc.) plug in without touching this code.
971
+ if (msg.conversationId && msg.requestId) {
972
+ const state = ctx.conversations.get(msg.conversationId);
973
+ if (state?.pendingPermissions?.has(msg.requestId)) {
974
+ try {
975
+ const driver = getProvider(state.providerName || DEFAULT_PROVIDER);
976
+ if (typeof driver.respondToPermissionRequest === 'function') {
977
+ const ans = msg.answers || {};
978
+ const optionId = typeof ans === 'string' ? ans
979
+ : ans.optionId || ans.option || Object.values(ans)[0];
980
+ driver.respondToPermissionRequest(state, msg.requestId, optionId);
981
+ return;
982
+ }
983
+ } catch (err) {
984
+ console.warn('[AskUser] provider perm routing failed:', err?.message || err);
985
+ }
986
+ }
987
+ }
988
+
934
989
  let pending = ctx.pendingUserQuestions.get(msg.requestId);
935
990
  let matchedRequestId = msg.requestId;
936
991
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.860",
3
+ "version": "0.1.864",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Tiny JSON-RPC 2.0 client over stdio for Agent Client Protocol (ACP).
3
+ *
4
+ * ACP uses newline-delimited JSON (one JSON object per line on each direction).
5
+ * This client:
6
+ * - sends `request(method, params)` → Promise<result>
7
+ * - sends `notify(method, params)` (no response expected)
8
+ * - dispatches incoming notifications via onNotification(method, params)
9
+ * - dispatches incoming requests (server → client) via onRequest(method, params)
10
+ * which must return a value (resolved as result) or throw to send an error
11
+ *
12
+ * Designed to be reusable by any ACP-style backend (Copilot today,
13
+ * hermes-agent later).
14
+ */
15
+
16
+ export class AcpClient {
17
+ /**
18
+ * @param {object} opts
19
+ * @param {NodeJS.WritableStream} opts.stdin process.stdin of the child
20
+ * @param {NodeJS.ReadableStream} opts.stdout process.stdout of the child
21
+ * @param {(method:string, params:any)=>void} [opts.onNotification]
22
+ * @param {(method:string, params:any)=>Promise<any>|any} [opts.onRequest]
23
+ * @param {(err:Error)=>void} [opts.onError] transport-level error sink
24
+ */
25
+ constructor({ stdin, stdout, onNotification, onRequest, onError }) {
26
+ this._stdin = stdin;
27
+ this._stdout = stdout;
28
+ this._onNotification = onNotification || (() => {});
29
+ this._onRequest = onRequest || (() => { throw new Error('no onRequest handler'); });
30
+ this._onError = onError || (() => {});
31
+ this._nextId = 1;
32
+ this._pending = new Map(); // id → { resolve, reject }
33
+ this._buf = '';
34
+ this._closed = false;
35
+
36
+ stdout.on('data', (chunk) => this._onData(chunk));
37
+ stdout.on('error', (err) => this._onError(err));
38
+ stdout.on('close', () => this._handleClose());
39
+ }
40
+
41
+ /** Send a JSONRPC request and await its response. */
42
+ request(method, params) {
43
+ if (this._closed) return Promise.reject(new Error('acp client closed'));
44
+ const id = this._nextId++;
45
+ const payload = { jsonrpc: '2.0', id, method, params };
46
+ return new Promise((resolve, reject) => {
47
+ this._pending.set(id, { resolve, reject });
48
+ try {
49
+ this._stdin.write(JSON.stringify(payload) + '\n');
50
+ } catch (err) {
51
+ this._pending.delete(id);
52
+ reject(err);
53
+ }
54
+ });
55
+ }
56
+
57
+ /** Send a JSONRPC notification (no id, no response). */
58
+ notify(method, params) {
59
+ if (this._closed) return;
60
+ const payload = { jsonrpc: '2.0', method, params };
61
+ try { this._stdin.write(JSON.stringify(payload) + '\n'); }
62
+ catch (err) { this._onError(err); }
63
+ }
64
+
65
+ /** Mark closed; reject all pending requests. */
66
+ close(reason) {
67
+ if (this._closed) return;
68
+ this._closed = true;
69
+ const err = new Error(reason || 'acp client closed');
70
+ for (const { reject } of this._pending.values()) {
71
+ try { reject(err); } catch { /* noop */ }
72
+ }
73
+ this._pending.clear();
74
+ }
75
+
76
+ _handleClose() {
77
+ this.close('child stdout closed');
78
+ }
79
+
80
+ _onData(chunk) {
81
+ this._buf += chunk.toString('utf8');
82
+ let idx;
83
+ while ((idx = this._buf.indexOf('\n')) >= 0) {
84
+ const line = this._buf.slice(0, idx).trim();
85
+ this._buf = this._buf.slice(idx + 1);
86
+ if (!line) continue;
87
+ let msg;
88
+ try { msg = JSON.parse(line); }
89
+ catch (err) { this._onError(new Error(`acp: unparsable line: ${line.slice(0, 200)}`)); continue; }
90
+ this._dispatch(msg);
91
+ }
92
+ }
93
+
94
+ _dispatch(msg) {
95
+ // Response (has id and result/error, no method)
96
+ if (msg.id != null && (Object.prototype.hasOwnProperty.call(msg, 'result') || Object.prototype.hasOwnProperty.call(msg, 'error'))) {
97
+ const slot = this._pending.get(msg.id);
98
+ if (!slot) return; // stale
99
+ this._pending.delete(msg.id);
100
+ if (msg.error) slot.reject(Object.assign(new Error(msg.error.message || 'acp error'), { code: msg.error.code, data: msg.error.data }));
101
+ else slot.resolve(msg.result);
102
+ return;
103
+ }
104
+ // Request from server (has id and method) — synchronous response required
105
+ if (msg.id != null && typeof msg.method === 'string') {
106
+ Promise.resolve()
107
+ .then(() => this._onRequest(msg.method, msg.params))
108
+ .then((result) => {
109
+ this._safeWrite({ jsonrpc: '2.0', id: msg.id, result: result === undefined ? null : result });
110
+ })
111
+ .catch((err) => {
112
+ this._safeWrite({
113
+ jsonrpc: '2.0',
114
+ id: msg.id,
115
+ error: { code: err?.code || -32603, message: err?.message || String(err) },
116
+ });
117
+ });
118
+ return;
119
+ }
120
+ // Notification (method, no id)
121
+ if (typeof msg.method === 'string') {
122
+ try { this._onNotification(msg.method, msg.params); }
123
+ catch (err) { this._onError(err); }
124
+ return;
125
+ }
126
+ this._onError(new Error('acp: unknown message shape'));
127
+ }
128
+
129
+ _safeWrite(payload) {
130
+ try { this._stdin.write(JSON.stringify(payload) + '\n'); }
131
+ catch (err) { this._onError(err); }
132
+ }
133
+ }
134
+
135
+ export default AcpClient;
package/providers/base.js CHANGED
@@ -15,9 +15,16 @@
15
15
  *
16
16
  * @typedef {Object} ChatProvider
17
17
  * @property {string} name
18
+ * @property {ProviderCapabilities} capabilities
19
+ * Static feature flags the provider supports. UI uses these to decide which
20
+ * header buttons / panels to show, rather than string-matching provider names.
18
21
  * @property {(opts: StartOpts) => Promise<Object>} start
19
22
  * @property {(state: Object, prompt: string, opts?: Object) => Promise<void>} sendInput
20
23
  * @property {(state: Object) => void} abort
24
+ * @property {(state: Object) => Promise<void>} [clear]
25
+ * Optional. Reset in-flight conversation state (e.g. start a new ACP session
26
+ * under the same conversationId). If not provided, the frontend falls back to
27
+ * a client-side message wipe only.
21
28
  * @property {() => Promise<FolderInfo[]>} listFolders
22
29
  * Return the list of work-directories that this provider has sessions for.
23
30
  * @property {(workDir: string) => Promise<SessionInfo[]>} listSessions
@@ -26,6 +33,16 @@
26
33
  * Return the resumable transcript as an array of `claude_output`-compatible
27
34
  * envelopes (the same shape the live stream would have produced).
28
35
  *
36
+ * @typedef {Object} ProviderCapabilities
37
+ * @property {boolean} [compact] provider supports /compact (auto + manual)
38
+ * @property {boolean} [clear] provider supports in-place /clear
39
+ * @property {boolean} [expert] provider supports the expert panel / subagent injection
40
+ * @property {boolean} [mcp] provider exposes MCP server toggles per conversation
41
+ * @property {boolean} [subagents] provider drives subagent watcher events
42
+ * @property {boolean} [attachments] provider accepts file / image attachments in prompts
43
+ * @property {boolean} [askUser] provider supports the round-trip ask-user permission prompt
44
+ * @property {boolean} [modelPicker] provider supports switching model from the UI
45
+ *
29
46
  * @typedef {Object} StartOpts
30
47
  * @property {string} conversationId
31
48
  * @property {string} workDir
@@ -10,6 +10,17 @@ import {
10
10
 
11
11
  export const name = 'claude-code';
12
12
 
13
+ export const capabilities = Object.freeze({
14
+ compact: true,
15
+ clear: true,
16
+ expert: true,
17
+ mcp: true,
18
+ subagents: true,
19
+ attachments: true,
20
+ askUser: true,
21
+ modelPicker: true,
22
+ });
23
+
13
24
  /**
14
25
  * Start (or resume) a Claude Code CLI session.
15
26
  * Returns the same state object that startClaudeQuery stores in ctx.conversations.
@@ -81,4 +92,4 @@ export async function loadHistory(workDir, sessionId, limit = 500) {
81
92
  return loadSessionHistory(workDir, sessionId, limit);
82
93
  }
83
94
 
84
- export default { name, start, sendInput, abort, listFolders, listSessions, loadHistory };
95
+ export default { name, capabilities, start, sendInput, abort, listFolders, listSessions, loadHistory };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Hard-coded known Copilot CLI model IDs. The CLI accepts `--model <id>` but
3
+ * doesn't expose a list over ACP today, so we ship a curated set that maps
4
+ * to what `copilot /model` shows in interactive mode. Add to this list as
5
+ * Copilot publishes new models — no other code change required.
6
+ */
7
+ export const COPILOT_MODELS = Object.freeze([
8
+ { id: 'gpt-5', label: 'GPT-5' },
9
+ { id: 'gpt-5-mini', label: 'GPT-5 Mini' },
10
+ { id: 'claude-sonnet-4', label: 'Claude Sonnet 4' },
11
+ { id: 'claude-sonnet-4.5', label: 'Claude Sonnet 4.5' },
12
+ { id: 'claude-opus-4', label: 'Claude Opus 4' },
13
+ { id: 'gpt-4o', label: 'GPT-4o' },
14
+ { id: 'o1', label: 'o1' },
15
+ ]);
16
+
17
+ export const DEFAULT_COPILOT_MODEL = 'claude-sonnet-4.5';