@yeaft/webchat-agent 0.1.860 → 0.1.863
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/connection/message-router.js +21 -1
- package/conversation.js +57 -2
- package/package.json +1 -1
- package/providers/acp-client.js +135 -0
- package/providers/base.js +17 -0
- package/providers/claude-code.js +12 -1
- package/providers/copilot-models.js +17 -0
- package/providers/copilot.js +484 -185
- package/yeaft/chats/chat-store.js +224 -0
- package/yeaft/conversation/persist.js +98 -0
- package/yeaft/dream-v2/apply.js +8 -0
- package/yeaft/dream-v2/prompts/index.js +3 -0
- package/yeaft/dream-v2/triage.js +16 -1
- package/yeaft/engine.js +24 -8
- package/yeaft/groups/pre-flow.js +13 -4
- package/yeaft/llm/models-dev.js +7 -1
- package/yeaft/memory/segment.js +1 -1
- package/yeaft/memory/store-v2.js +41 -2
- package/yeaft/web-bridge.js +326 -3
|
@@ -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
|
@@ -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
|
package/providers/claude-code.js
CHANGED
|
@@ -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';
|