@kafca/agentdock 0.1.28 → 0.1.30
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/README.md +5 -0
- package/dist/renderer/assets/{Badge-B-Ayib4h.js → Badge-DlVJI1Ys.js} +1 -1
- package/dist/renderer/assets/{Chat-CNEOxtyz.js → Chat-oa8OjaHQ.js} +1 -1
- package/dist/renderer/assets/{ChatMarkdown-BTd1QBj9.js → ChatMarkdown-C-N8KS5C.js} +1 -1
- package/dist/renderer/assets/{Config-ByKhcLa6.js → Config-D9sL_d4D.js} +1 -1
- package/dist/renderer/assets/{CronList-0mi8dcC5.js → CronList-Z5JU3DHk.js} +1 -1
- package/dist/renderer/assets/{Dashboard-C8-tB_qg.js → Dashboard-D3BwbG43.js} +2 -2
- package/dist/renderer/assets/{EmptyState-TR-9nr5e.js → EmptyState-QOj765Ul.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-DKAxFOXc.js → KnowledgeDetail-J7n01mPu.js} +1 -1
- package/dist/renderer/assets/{KnowledgeHome-CM-1IDr4.js → KnowledgeHome-BFnH0NVk.js} +1 -1
- package/dist/renderer/assets/{Logs-DI5RFPri.js → Logs-DfSUifXc.js} +1 -1
- package/dist/renderer/assets/{Modal-CQpa4NPP.js → Modal-QjEGhyru.js} +1 -1
- package/dist/renderer/assets/{Page-BHXEAwzQ.js → Page-qdHFSw7x.js} +1 -1
- package/dist/renderer/assets/{ProjectDetail-Dn5elnHx.js → ProjectDetail-DS_rfvhh.js} +1 -1
- package/dist/renderer/assets/{ProjectList-D6yvEgb5.js → ProjectList-COtyZd7H.js} +1 -1
- package/dist/renderer/assets/{Select-BbkcXnXL.js → Select-BQ8mncVu.js} +1 -1
- package/dist/renderer/assets/{SessionChat-Vkf-7gvX.js → SessionChat-DjVnjuQy.js} +1 -1
- package/dist/renderer/assets/{SessionList-c5encjWd.js → SessionList-88dgGZJ8.js} +1 -1
- package/dist/renderer/assets/ThreadChat-BCdbTxBo.js +23 -0
- package/dist/renderer/assets/{Workspace-BC7hWqhP.js → Workspace-D_hzSJjX.js} +1 -1
- package/dist/renderer/assets/{arrow-left-I_Y1kdN9.js → arrow-left-BMpknulW.js} +1 -1
- package/dist/renderer/assets/{book-open-B3anUYvp.js → book-open-CiaOeQbU.js} +1 -1
- package/dist/renderer/assets/{check-BPNCDKgS.js → check-CgwYH4GF.js} +1 -1
- package/dist/renderer/assets/{chevron-down-BaxUcOIV.js → chevron-down-BzD2S1OO.js} +1 -1
- package/dist/renderer/assets/{circle-DJQiypFS.js → circle-BDHD7g7l.js} +1 -1
- package/dist/renderer/assets/{desktop-Cv51clcN.js → desktop-BiR5LPL5.js} +1 -1
- package/dist/renderer/assets/{funnel-DqKBtiqE.js → funnel-Bruf5FYA.js} +1 -1
- package/dist/renderer/assets/{heart-CmoNjvMO.js → heart-D7Mz04B0.js} +1 -1
- package/dist/renderer/assets/{index-DXJl2HOK.js → index-CL1e8m3U.js} +2 -2
- package/dist/renderer/assets/{index-BfXqhQ1Z.js → index-gZIRr4_o.js} +1 -1
- package/dist/renderer/assets/{pencil-CLUBszbw.js → pencil-C6gzDMse.js} +1 -1
- package/dist/renderer/assets/{play-l2RPZuAi.js → play-DqTXX-Pr.js} +1 -1
- package/dist/renderer/assets/{plug-CeUPIjRt.js → plug-ykmbeaO3.js} +1 -1
- package/dist/renderer/assets/{plus-BjkefS2P.js → plus-DHE60vMX.js} +1 -1
- package/dist/renderer/assets/projects-Bx5p10th.js +1 -0
- package/dist/renderer/assets/{save-D0yYt9tz.js → save-CGIvrdQI.js} +1 -1
- package/dist/renderer/assets/{search-B7s473Y4.js → search-D7IFAwyE.js} +1 -1
- package/dist/renderer/assets/{server-Dz_h8X_8.js → server-aOsirp7I.js} +1 -1
- package/dist/renderer/assets/{session-utils-BvNRTRXA.js → session-utils-Dxfp4ohQ.js} +1 -1
- package/dist/renderer/assets/{sessions-KPJsgKbQ.js → sessions-Db1XPbG2.js} +1 -1
- package/dist/renderer/assets/{shield-check-Wc5wfUUs.js → shield-check-ChXeQK-d.js} +1 -1
- package/dist/renderer/assets/{trash-2-7INdaB9w.js → trash-2-CfN-c1zw.js} +1 -1
- package/dist/renderer/assets/{user-CLbCgWNR.js → user-C7OHaBp4.js} +1 -1
- package/dist/renderer/assets/{wifi-off-BfDVPXbO.js → wifi-off-CpaJigye.js} +1 -1
- package/dist/renderer/index.html +1 -1
- package/dist-electron/electron/agent-runtime-detector.test.js +20 -0
- package/dist-electron/electron/lac-cli.test.js +2 -13
- package/dist-electron/electron/local-core-refactor.test.js +424 -64
- package/dist-electron/electron/workspace-task-store.test.js +118 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +17 -10
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-progress.js +245 -9
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +24 -11
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-store.js +36 -11
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-transport.js +2 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +90 -7
- package/dist-electron/services/local-ai-core/src/channel/lark/runtime-state.js +35 -40
- package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +43 -18
- package/dist-electron/services/local-ai-core/src/cli/lac.js +1 -17
- package/dist-electron/services/local-ai-core/src/router/workspace-router.js +2 -2
- package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +1 -1
- package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +31 -1
- package/dist-electron/services/local-ai-core/src/scheduler/local-schedule-adapter.js +4 -1
- package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js +4 -1
- package/dist-electron/src/pages/Threads/thread-chat-message-blocks.js +4 -114
- package/dist-electron/src/pages/Threads/thread-chat-model.js +14 -23
- package/dist-electron/src/pages/Threads/thread-chat-page-state.js +1 -1
- package/dist-electron/src/pages/Threads/thread-chat-permission.test.js +58 -31
- package/package.json +1 -1
- package/dist/renderer/assets/ThreadChat-DY4wzomi.js +0 -23
- package/dist/renderer/assets/projects-De73QY_o.js +0 -1
|
@@ -41,6 +41,8 @@ class LocalCoreAcpStore {
|
|
|
41
41
|
role TEXT NOT NULL,
|
|
42
42
|
content TEXT NOT NULL,
|
|
43
43
|
tool_call_json TEXT,
|
|
44
|
+
bridge_kind TEXT,
|
|
45
|
+
bridge_status TEXT,
|
|
44
46
|
timestamp TEXT NOT NULL,
|
|
45
47
|
kind TEXT NOT NULL,
|
|
46
48
|
seq INTEGER NOT NULL,
|
|
@@ -222,6 +224,8 @@ class LocalCoreAcpStore {
|
|
|
222
224
|
CREATE INDEX IF NOT EXISTS idx_audit_events_type_created ON audit_events (type, created_at DESC);
|
|
223
225
|
`);
|
|
224
226
|
this.ensureColumn('messages', 'tool_call_json', 'TEXT');
|
|
227
|
+
this.ensureColumn('messages', 'bridge_kind', 'TEXT');
|
|
228
|
+
this.ensureColumn('messages', 'bridge_status', 'TEXT');
|
|
225
229
|
this.ensureColumn('scheduled_jobs', 'execution_mode', "TEXT NOT NULL DEFAULT 'same-thread'");
|
|
226
230
|
this.ensureColumn('threads', 'agent_mode', "TEXT NOT NULL DEFAULT 'default'");
|
|
227
231
|
}
|
|
@@ -284,7 +288,7 @@ class LocalCoreAcpStore {
|
|
|
284
288
|
throw new Error(`Thread not found: ${threadId}`);
|
|
285
289
|
}
|
|
286
290
|
const messages = this.db.prepare(`
|
|
287
|
-
SELECT id, thread_id, role, content, tool_call_json, timestamp, kind, seq
|
|
291
|
+
SELECT id, thread_id, role, content, tool_call_json, bridge_kind, bridge_status, timestamp, kind, seq
|
|
288
292
|
FROM messages
|
|
289
293
|
WHERE thread_id = ?
|
|
290
294
|
ORDER BY seq ASC
|
|
@@ -305,6 +309,8 @@ class LocalCoreAcpStore {
|
|
|
305
309
|
role: message.role,
|
|
306
310
|
content: message.content,
|
|
307
311
|
toolCall: parseJson(message.tool_call_json || 'null', null) || undefined,
|
|
312
|
+
bridgeKind: normalizeBridgeKind(message.bridge_kind),
|
|
313
|
+
bridgeStatus: normalizeBridgeStatus(message.bridge_status),
|
|
308
314
|
timestamp: message.timestamp,
|
|
309
315
|
kind: message.kind,
|
|
310
316
|
})),
|
|
@@ -319,7 +325,7 @@ class LocalCoreAcpStore {
|
|
|
319
325
|
deleteThread(threadId) {
|
|
320
326
|
this.db.prepare('DELETE FROM threads WHERE id = ?').run(threadId);
|
|
321
327
|
}
|
|
322
|
-
appendMessage(threadId, role, content, kind, toolCall) {
|
|
328
|
+
appendMessage(threadId, role, content, kind, toolCall, bridgeKind, bridgeStatus) {
|
|
323
329
|
const timestamp = new Date().toISOString();
|
|
324
330
|
const nextSequenceRow = this.db.prepare('SELECT COALESCE(MAX(seq), -1) + 1 AS next_seq FROM messages WHERE thread_id = ?').get(threadId);
|
|
325
331
|
const nextSeq = Number(nextSequenceRow?.next_seq || 0);
|
|
@@ -328,9 +334,9 @@ class LocalCoreAcpStore {
|
|
|
328
334
|
this.db.exec('BEGIN IMMEDIATE');
|
|
329
335
|
try {
|
|
330
336
|
this.db.prepare(`
|
|
331
|
-
INSERT INTO messages (id, thread_id, role, content, tool_call_json, timestamp, kind, seq)
|
|
332
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
333
|
-
`).run(id, threadId, role, content, toolCall ? JSON.stringify(toolCall) : null, timestamp, kind, nextSeq);
|
|
337
|
+
INSERT INTO messages (id, thread_id, role, content, tool_call_json, bridge_kind, bridge_status, timestamp, kind, seq)
|
|
338
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
339
|
+
`).run(id, threadId, role, content, toolCall ? JSON.stringify(toolCall) : null, bridgeKind || null, bridgeStatus || null, timestamp, kind, nextSeq);
|
|
334
340
|
this.db.prepare(`
|
|
335
341
|
UPDATE threads
|
|
336
342
|
SET updated_at = ?, history_count = history_count + 1, excerpt = ?
|
|
@@ -344,7 +350,7 @@ class LocalCoreAcpStore {
|
|
|
344
350
|
}
|
|
345
351
|
return { id, timestamp };
|
|
346
352
|
}
|
|
347
|
-
upsertMessage(threadId, id, role, content, kind, toolCall) {
|
|
353
|
+
upsertMessage(threadId, id, role, content, kind, toolCall, bridgeKind, bridgeStatus) {
|
|
348
354
|
const timestamp = new Date().toISOString();
|
|
349
355
|
const excerpt = (0, workspace_thread_mappers_js_1.normalizeMessageContent)(content);
|
|
350
356
|
const existing = this.db.prepare('SELECT id FROM messages WHERE id = ? AND thread_id = ?').get(id, threadId);
|
|
@@ -353,17 +359,17 @@ class LocalCoreAcpStore {
|
|
|
353
359
|
if (existing) {
|
|
354
360
|
this.db.prepare(`
|
|
355
361
|
UPDATE messages
|
|
356
|
-
SET content = ?, tool_call_json = ?, timestamp = ?, kind = ?
|
|
362
|
+
SET content = ?, tool_call_json = ?, bridge_kind = ?, bridge_status = ?, timestamp = ?, kind = ?
|
|
357
363
|
WHERE id = ? AND thread_id = ?
|
|
358
|
-
`).run(content, toolCall ? JSON.stringify(toolCall) : null, timestamp, kind, id, threadId);
|
|
364
|
+
`).run(content, toolCall ? JSON.stringify(toolCall) : null, bridgeKind || null, bridgeStatus || null, timestamp, kind, id, threadId);
|
|
359
365
|
}
|
|
360
366
|
else {
|
|
361
367
|
const nextSequenceRow = this.db.prepare('SELECT COALESCE(MAX(seq), -1) + 1 AS next_seq FROM messages WHERE thread_id = ?').get(threadId);
|
|
362
368
|
const nextSeq = Number(nextSequenceRow?.next_seq || 0);
|
|
363
369
|
this.db.prepare(`
|
|
364
|
-
INSERT INTO messages (id, thread_id, role, content, tool_call_json, timestamp, kind, seq)
|
|
365
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
366
|
-
`).run(id, threadId, role, content, toolCall ? JSON.stringify(toolCall) : null, timestamp, kind, nextSeq);
|
|
370
|
+
INSERT INTO messages (id, thread_id, role, content, tool_call_json, bridge_kind, bridge_status, timestamp, kind, seq)
|
|
371
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
372
|
+
`).run(id, threadId, role, content, toolCall ? JSON.stringify(toolCall) : null, bridgeKind || null, bridgeStatus || null, timestamp, kind, nextSeq);
|
|
367
373
|
this.db.prepare(`
|
|
368
374
|
UPDATE threads
|
|
369
375
|
SET history_count = history_count + 1
|
|
@@ -830,6 +836,9 @@ class LocalCoreAcpStore {
|
|
|
830
836
|
return row ? this.toScheduledJob(row) : undefined;
|
|
831
837
|
}
|
|
832
838
|
createScheduledJob(input) {
|
|
839
|
+
if (!input.platform || !input.route) {
|
|
840
|
+
throw new Error('Scheduled job creation requires a resolved platform and route.');
|
|
841
|
+
}
|
|
833
842
|
let id = (0, job_id_js_1.createScheduledJobId)();
|
|
834
843
|
for (let attempt = 0; attempt < 5 && this.getScheduledJob(id); attempt += 1) {
|
|
835
844
|
id = (0, job_id_js_1.createScheduledJobId)();
|
|
@@ -1301,6 +1310,22 @@ function parseJson(value, fallback) {
|
|
|
1301
1310
|
return fallback;
|
|
1302
1311
|
}
|
|
1303
1312
|
}
|
|
1313
|
+
function normalizeBridgeKind(value) {
|
|
1314
|
+
switch (value) {
|
|
1315
|
+
case 'assistant':
|
|
1316
|
+
case 'thought':
|
|
1317
|
+
case 'plan':
|
|
1318
|
+
case 'tool':
|
|
1319
|
+
case 'status':
|
|
1320
|
+
case 'permission':
|
|
1321
|
+
return value;
|
|
1322
|
+
default:
|
|
1323
|
+
return undefined;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
function normalizeBridgeStatus(value) {
|
|
1327
|
+
return value === 'awaiting_input' ? value : undefined;
|
|
1328
|
+
}
|
|
1304
1329
|
function defaultPermissions() {
|
|
1305
1330
|
return {
|
|
1306
1331
|
'workspace.read': 'allow',
|
|
@@ -35,9 +35,11 @@ class LocalCoreAcpTransport {
|
|
|
35
35
|
loadReplayMode: false,
|
|
36
36
|
pendingPermissionByRun: new Map(),
|
|
37
37
|
schedulerJobCreatedByRun: new Map(),
|
|
38
|
+
pendingRawAssistantProgressChunks: [],
|
|
38
39
|
closed: false,
|
|
39
40
|
closeReason: null,
|
|
40
41
|
promptPromise: null,
|
|
42
|
+
launchPermissionMode: '',
|
|
41
43
|
};
|
|
42
44
|
child.stdout.setEncoding('utf8');
|
|
43
45
|
child.stdout.on('data', (chunk) => this.handleStdout(session, chunk));
|
|
@@ -11,6 +11,11 @@ class LocalCoreAcpTurnCoordinator {
|
|
|
11
11
|
constructor(options) {
|
|
12
12
|
this.options = options;
|
|
13
13
|
}
|
|
14
|
+
closePendingThoughtSegment(session) {
|
|
15
|
+
const currentTurn = session.currentTurn;
|
|
16
|
+
if (currentTurn)
|
|
17
|
+
(0, local_core_acp_progress_js_1.closeThoughtSegment)(currentTurn);
|
|
18
|
+
}
|
|
14
19
|
flushPendingToolCall(session) {
|
|
15
20
|
const currentTurn = session.currentTurn;
|
|
16
21
|
const currentRunId = session.currentRunId;
|
|
@@ -97,6 +102,7 @@ class LocalCoreAcpTurnCoordinator {
|
|
|
97
102
|
}
|
|
98
103
|
const options = (0, local_core_acp_permission_lifecycle_js_1.parsePermissionOptions)(payload.params?.options);
|
|
99
104
|
const toolTitle = (0, workspace_acp_permissions_js_1.formatToolCallContent)(payload.params?.toolCall);
|
|
105
|
+
this.closePendingThoughtSegment(session);
|
|
100
106
|
if ((this.options.getThreadAgentMode?.(session.threadId) || local_core_slash_commands_js_1.DEFAULT_AGENT_MODE) === 'bypassPermissions') {
|
|
101
107
|
const selected = options.find((option) => option.normalizedAction === 'allow all')
|
|
102
108
|
|| options.find((option) => option.normalizedAction === 'allow')
|
|
@@ -160,17 +166,26 @@ class LocalCoreAcpTurnCoordinator {
|
|
|
160
166
|
});
|
|
161
167
|
this.options.updateRunStatus(currentRunId, session.threadId, 'awaiting_input');
|
|
162
168
|
const permissionPrompt = (0, local_core_acp_permission_lifecycle_js_1.createPermissionPrompt)(toolTitle);
|
|
163
|
-
this.options.appendMessage(session.threadId, 'assistant', permissionPrompt, 'progress');
|
|
169
|
+
this.options.appendMessage(session.threadId, 'assistant', permissionPrompt, 'progress', undefined, 'permission', 'awaiting_input');
|
|
164
170
|
this.options.emitBridge({
|
|
165
171
|
type: 'buttons',
|
|
166
172
|
sessionKey: session.bridgeSessionKey,
|
|
167
173
|
replyCtx: currentRunId,
|
|
174
|
+
bridgeKind: 'permission',
|
|
175
|
+
bridgeStatus: 'awaiting_input',
|
|
168
176
|
content: permissionPrompt,
|
|
169
177
|
buttonRows,
|
|
170
178
|
});
|
|
171
179
|
}
|
|
172
180
|
handleAgentNotification(session, payload) {
|
|
173
|
-
if (session.loadReplayMode
|
|
181
|
+
if (session.loadReplayMode) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (payload.method === '_claude/sdkMessage') {
|
|
185
|
+
this.handleClaudeSdkMessage(session, payload.params?.message);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (payload.method !== 'session/update') {
|
|
174
189
|
return;
|
|
175
190
|
}
|
|
176
191
|
const update = payload.params?.update;
|
|
@@ -181,10 +196,25 @@ class LocalCoreAcpTurnCoordinator {
|
|
|
181
196
|
}
|
|
182
197
|
switch (String(update.sessionUpdate || '')) {
|
|
183
198
|
case 'agent_message_chunk': {
|
|
199
|
+
this.closePendingThoughtSegment(session);
|
|
184
200
|
this.flushPendingToolCall(session);
|
|
185
201
|
if (update.content?.type !== 'text') {
|
|
186
202
|
return;
|
|
187
203
|
}
|
|
204
|
+
if (isToolScopedAssistantUpdate(update) || consumeRawAssistantProgressChunk(session, String(update.content.text || ''))) {
|
|
205
|
+
const content = String(update.content.text || '').trim();
|
|
206
|
+
if (content) {
|
|
207
|
+
this.options.appendMessage(session.threadId, 'assistant', content, 'progress', undefined, 'tool');
|
|
208
|
+
this.options.emitBridge({
|
|
209
|
+
type: 'reply',
|
|
210
|
+
sessionKey: session.bridgeSessionKey,
|
|
211
|
+
replyCtx: currentRunId,
|
|
212
|
+
bridgeKind: 'tool',
|
|
213
|
+
content,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
188
218
|
const projection = (0, local_core_acp_progress_js_1.applyAssistantMessageChunk)(currentTurn, String(update.content.text || ''));
|
|
189
219
|
if (!projection) {
|
|
190
220
|
return;
|
|
@@ -194,12 +224,12 @@ class LocalCoreAcpTurnCoordinator {
|
|
|
194
224
|
sessionKey: session.bridgeSessionKey,
|
|
195
225
|
replyCtx: currentRunId,
|
|
196
226
|
previewHandle: projection.previewHandle,
|
|
227
|
+
bridgeKind: projection.bridgeKind,
|
|
197
228
|
content: projection.content,
|
|
198
229
|
});
|
|
199
230
|
return;
|
|
200
231
|
}
|
|
201
232
|
case 'agent_thought_chunk': {
|
|
202
|
-
this.flushPendingToolCall(session);
|
|
203
233
|
if (update.content?.type !== 'text') {
|
|
204
234
|
return;
|
|
205
235
|
}
|
|
@@ -208,23 +238,31 @@ class LocalCoreAcpTurnCoordinator {
|
|
|
208
238
|
return;
|
|
209
239
|
}
|
|
210
240
|
if (this.options.upsertMessage) {
|
|
211
|
-
this.options.upsertMessage(session.threadId, projection.messageId, 'assistant', projection.content, 'progress');
|
|
241
|
+
this.options.upsertMessage(session.threadId, projection.messageId, 'assistant', projection.content, 'progress', undefined, projection.bridgeKind);
|
|
212
242
|
}
|
|
213
243
|
this.options.emitBridge({
|
|
214
244
|
type: projection.bridgeType,
|
|
215
245
|
sessionKey: session.bridgeSessionKey,
|
|
216
246
|
replyCtx: currentRunId,
|
|
217
247
|
previewHandle: projection.previewHandle,
|
|
248
|
+
bridgeKind: projection.bridgeKind,
|
|
218
249
|
content: projection.content,
|
|
219
250
|
});
|
|
220
251
|
return;
|
|
221
252
|
}
|
|
222
253
|
case 'tool_call': {
|
|
254
|
+
this.closePendingThoughtSegment(session);
|
|
223
255
|
const toolCall = (0, local_core_acp_progress_js_1.registerPendingToolCall)({ currentTurn, runId: currentRunId, update });
|
|
256
|
+
(0, local_core_acp_progress_js_1.recordToolObservation)(currentTurn, {
|
|
257
|
+
name: toolCall.title,
|
|
258
|
+
title: toolCall.title,
|
|
259
|
+
input: toolCall.input,
|
|
260
|
+
});
|
|
224
261
|
(0, local_core_acp_progress_js_1.syncLegacyPendingToolCall)(currentTurn, toolCall);
|
|
225
262
|
return;
|
|
226
263
|
}
|
|
227
264
|
case 'tool_call_update': {
|
|
265
|
+
this.closePendingThoughtSegment(session);
|
|
228
266
|
const title = String(update.title || 'Tool update').trim();
|
|
229
267
|
const status = String(update.status || '').trim();
|
|
230
268
|
const content = (0, local_core_acp_progress_js_1.extractToolUpdateContent)(update.content);
|
|
@@ -276,11 +314,19 @@ class LocalCoreAcpTurnCoordinator {
|
|
|
276
314
|
detail: displayTitle,
|
|
277
315
|
content,
|
|
278
316
|
});
|
|
317
|
+
(0, local_core_acp_progress_js_1.recordToolObservation)(currentTurn, {
|
|
318
|
+
name: toolName || displayTitle || title,
|
|
319
|
+
title: displayTitle || title,
|
|
320
|
+
status,
|
|
321
|
+
input: updateInput === undefined ? toolCall?.input : updateInput,
|
|
322
|
+
outputText: content,
|
|
323
|
+
});
|
|
279
324
|
this.emitProgress(session, currentRunId, (0, local_core_acp_progress_js_1.formatToolProgressMessage)({ toolName, title: displayTitle, status, content }), messageId, toolCallPayload);
|
|
280
325
|
(0, local_core_acp_progress_js_1.syncLegacyPendingToolCall)(currentTurn, (0, local_core_acp_progress_js_1.resolveFallbackToolCall)(currentTurn));
|
|
281
326
|
return;
|
|
282
327
|
}
|
|
283
328
|
case 'plan': {
|
|
329
|
+
this.closePendingThoughtSegment(session);
|
|
284
330
|
this.flushPendingToolCall(session);
|
|
285
331
|
const entries = Array.isArray(update.entries) ? update.entries : [];
|
|
286
332
|
if (entries.length === 0) {
|
|
@@ -290,11 +336,12 @@ class LocalCoreAcpTurnCoordinator {
|
|
|
290
336
|
if (!content) {
|
|
291
337
|
return;
|
|
292
338
|
}
|
|
293
|
-
this.options.appendMessage(session.threadId, 'assistant', content, 'progress');
|
|
339
|
+
this.options.appendMessage(session.threadId, 'assistant', content, 'progress', undefined, 'plan');
|
|
294
340
|
this.options.emitBridge({
|
|
295
341
|
type: 'reply',
|
|
296
342
|
sessionKey: session.bridgeSessionKey,
|
|
297
343
|
replyCtx: currentRunId,
|
|
344
|
+
bridgeKind: 'plan',
|
|
298
345
|
content,
|
|
299
346
|
});
|
|
300
347
|
return;
|
|
@@ -303,24 +350,60 @@ class LocalCoreAcpTurnCoordinator {
|
|
|
303
350
|
return;
|
|
304
351
|
}
|
|
305
352
|
}
|
|
353
|
+
handleClaudeSdkMessage(session, message) {
|
|
354
|
+
if (!session.currentTurn || message?.type !== 'system' || message?.subtype !== 'local_command_output') {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const content = typeof message.content === 'string' ? message.content : '';
|
|
358
|
+
if (!content) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
session.pendingRawAssistantProgressChunks = [
|
|
362
|
+
...(session.pendingRawAssistantProgressChunks || []),
|
|
363
|
+
content,
|
|
364
|
+
];
|
|
365
|
+
}
|
|
306
366
|
emitProgress(session, currentRunId, content, messageId, toolCall) {
|
|
307
367
|
if (messageId && this.options.upsertMessage) {
|
|
308
|
-
this.options.upsertMessage(session.threadId, messageId, 'assistant', content, 'progress', toolCall);
|
|
368
|
+
this.options.upsertMessage(session.threadId, messageId, 'assistant', content, 'progress', toolCall, toolCall ? 'tool' : undefined);
|
|
309
369
|
}
|
|
310
370
|
else {
|
|
311
|
-
this.options.appendMessage(session.threadId, 'assistant', content, 'progress', toolCall);
|
|
371
|
+
this.options.appendMessage(session.threadId, 'assistant', content, 'progress', toolCall, toolCall ? 'tool' : undefined);
|
|
312
372
|
}
|
|
313
373
|
this.options.emitBridge({
|
|
314
374
|
type: 'reply',
|
|
315
375
|
sessionKey: session.bridgeSessionKey,
|
|
316
376
|
replyCtx: currentRunId,
|
|
317
377
|
messageId,
|
|
378
|
+
bridgeKind: toolCall ? 'tool' : undefined,
|
|
318
379
|
content,
|
|
319
380
|
toolCall,
|
|
320
381
|
});
|
|
321
382
|
}
|
|
322
383
|
}
|
|
323
384
|
exports.LocalCoreAcpTurnCoordinator = LocalCoreAcpTurnCoordinator;
|
|
385
|
+
function isToolScopedAssistantUpdate(update) {
|
|
386
|
+
const meta = update._meta;
|
|
387
|
+
if (!meta || typeof meta !== 'object') {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
const claudeCode = meta.claudeCode;
|
|
391
|
+
if (!claudeCode || typeof claudeCode !== 'object') {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
const parentToolUseId = claudeCode.parentToolUseId;
|
|
395
|
+
return typeof parentToolUseId === 'string' && parentToolUseId.trim().length > 0;
|
|
396
|
+
}
|
|
397
|
+
function consumeRawAssistantProgressChunk(session, text) {
|
|
398
|
+
const pending = session.pendingRawAssistantProgressChunks || [];
|
|
399
|
+
const index = pending.findIndex((candidate) => candidate === text);
|
|
400
|
+
if (index < 0) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
pending.splice(index, 1);
|
|
404
|
+
session.pendingRawAssistantProgressChunks = pending;
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
324
407
|
function createToolCallPayload(input) {
|
|
325
408
|
const status = input.status.trim() || 'running';
|
|
326
409
|
return {
|
|
@@ -31,31 +31,25 @@ function createLarkTurnState(sessionKey, sourceMessageId) {
|
|
|
31
31
|
return turn;
|
|
32
32
|
}
|
|
33
33
|
function foldToolResultForLark(content) {
|
|
34
|
-
|
|
35
|
-
if (!normalized.startsWith('🔧 '))
|
|
36
|
-
return normalized;
|
|
37
|
-
const parts = normalized.split(' - ');
|
|
38
|
-
if (parts.length <= 2)
|
|
39
|
-
return normalized;
|
|
40
|
-
return parts.slice(0, 2).join(' - ');
|
|
34
|
+
return String(content || '').trim();
|
|
41
35
|
}
|
|
42
36
|
function summarizeToolCallForLark(event, content) {
|
|
43
37
|
const toolCall = event.toolCall;
|
|
44
38
|
if (toolCall) {
|
|
45
39
|
const name = String(toolCall.name || '').trim() || summarizeToolContentForLark(content);
|
|
46
40
|
const input = formatToolInputForLark(toolCall.input);
|
|
47
|
-
return [
|
|
41
|
+
return [`🔧 ${name}`, input ? `参数:${input}` : '']
|
|
48
42
|
.filter(Boolean)
|
|
49
43
|
.join('\n\n');
|
|
50
44
|
}
|
|
51
45
|
return summarizeToolContentForLark(content);
|
|
52
46
|
}
|
|
53
|
-
function isPendingToolProgressForLark(
|
|
54
|
-
|
|
55
|
-
return normalized.startsWith('🔧 ') && normalized.includes(' - pending');
|
|
47
|
+
function isPendingToolProgressForLark(event) {
|
|
48
|
+
return event.toolCall?.status.trim().toLowerCase() === 'pending';
|
|
56
49
|
}
|
|
57
50
|
function consumeLarkBridgeEvent(turn, event, options) {
|
|
58
51
|
const content = foldToolResultForLark(String(event.content || '').trim());
|
|
52
|
+
const bridgeKind = resolveBridgeEventKind(event);
|
|
59
53
|
if (event.type === 'typing_start') {
|
|
60
54
|
turn.messageId = undefined;
|
|
61
55
|
turn.finalMessageId = undefined;
|
|
@@ -80,10 +74,10 @@ function consumeLarkBridgeEvent(turn, event, options) {
|
|
|
80
74
|
return;
|
|
81
75
|
}
|
|
82
76
|
if (event.type === 'preview_start' || event.type === 'update_message') {
|
|
83
|
-
if (
|
|
77
|
+
if (bridgeKind === 'thought') {
|
|
84
78
|
turn.pendingThoughtKey = event.previewHandle || 'thinking-preview';
|
|
85
79
|
turn.pendingThoughtText = content;
|
|
86
|
-
pushUniqueLarkTurnLine(turn.thinkingSteps, content
|
|
80
|
+
pushUniqueLarkTurnLine(turn.thinkingSteps, content);
|
|
87
81
|
return;
|
|
88
82
|
}
|
|
89
83
|
turn.previewText = content;
|
|
@@ -106,17 +100,17 @@ function consumeLarkBridgeEvent(turn, event, options) {
|
|
|
106
100
|
if (!content) {
|
|
107
101
|
return;
|
|
108
102
|
}
|
|
109
|
-
if (
|
|
103
|
+
if (bridgeKind === 'thought' || bridgeKind === 'plan') {
|
|
110
104
|
turn.pendingThoughtKey = progressKey('thinking', event);
|
|
111
105
|
turn.pendingThoughtText = content;
|
|
112
|
-
pushUniqueLarkTurnLine(turn.thinkingSteps, content
|
|
106
|
+
pushUniqueLarkTurnLine(turn.thinkingSteps, content);
|
|
113
107
|
return;
|
|
114
108
|
}
|
|
115
|
-
if (
|
|
109
|
+
if (bridgeKind === 'tool') {
|
|
116
110
|
pushUniqueLarkTurnLine(turn.toolCalls, content.slice(3).trim());
|
|
117
111
|
return;
|
|
118
112
|
}
|
|
119
|
-
if (
|
|
113
|
+
if (bridgeKind === 'status') {
|
|
120
114
|
pushUniqueLarkTurnLine(turn.statusLines, content.slice(3).trim());
|
|
121
115
|
return;
|
|
122
116
|
}
|
|
@@ -125,12 +119,13 @@ function consumeLarkBridgeEvent(turn, event, options) {
|
|
|
125
119
|
}
|
|
126
120
|
function renderLarkBridgeEventMessage(turn, event) {
|
|
127
121
|
const content = foldToolResultForLark(String(event.content || '').trim());
|
|
122
|
+
const bridgeKind = resolveBridgeEventKind(event);
|
|
128
123
|
if (event.type === 'preview_start' || event.type === 'update_message') {
|
|
129
|
-
if (
|
|
124
|
+
if (bridgeKind === 'thought') {
|
|
130
125
|
return { key: 'noop', text: '', buttonRows: [], isFinal: false };
|
|
131
126
|
}
|
|
132
|
-
if (
|
|
133
|
-
if (isPendingToolProgressForLark(
|
|
127
|
+
if (bridgeKind === 'tool') {
|
|
128
|
+
if (isPendingToolProgressForLark(event)) {
|
|
134
129
|
return { key: 'noop', text: '', buttonRows: [], isFinal: false };
|
|
135
130
|
}
|
|
136
131
|
return renderToolMessage(progressKey('tool', event), summarizeToolCallForLark(event, content));
|
|
@@ -138,19 +133,19 @@ function renderLarkBridgeEventMessage(turn, event) {
|
|
|
138
133
|
return { key: 'noop', text: '', buttonRows: [], isFinal: false };
|
|
139
134
|
}
|
|
140
135
|
if (event.type === 'status') {
|
|
141
|
-
return renderProgressMessage(progressKey('status', event),
|
|
136
|
+
return renderProgressMessage(progressKey('status', event), `⏳ ${content}`);
|
|
142
137
|
}
|
|
143
138
|
if (event.type === 'reply') {
|
|
144
|
-
if (
|
|
145
|
-
return
|
|
139
|
+
if (bridgeKind === 'thought' || bridgeKind === 'plan') {
|
|
140
|
+
return renderProgressMessage(progressKey(bridgeKind, event), renderProcessText(bridgeKind, content));
|
|
146
141
|
}
|
|
147
|
-
if (
|
|
148
|
-
if (isPendingToolProgressForLark(
|
|
142
|
+
if (bridgeKind === 'tool') {
|
|
143
|
+
if (isPendingToolProgressForLark(event)) {
|
|
149
144
|
return { key: 'noop', text: '', buttonRows: [], isFinal: false };
|
|
150
145
|
}
|
|
151
146
|
return renderToolMessage(progressKey('tool', event), summarizeToolCallForLark(event, content));
|
|
152
147
|
}
|
|
153
|
-
if (
|
|
148
|
+
if (bridgeKind === 'status') {
|
|
154
149
|
return renderProgressMessage(progressKey('status', event), content);
|
|
155
150
|
}
|
|
156
151
|
return {
|
|
@@ -186,14 +181,14 @@ function takePendingLarkThoughtRender(turn) {
|
|
|
186
181
|
}
|
|
187
182
|
turn.thoughtSegmentSequence += 1;
|
|
188
183
|
const baseKey = turn.pendingThoughtKey || 'thinking-preview';
|
|
189
|
-
const rendered = renderProgressMessage(`thinking:${baseKey}:${turn.thoughtSegmentSequence}`, text);
|
|
184
|
+
const rendered = renderProgressMessage(`thinking:${baseKey}:${turn.thoughtSegmentSequence}`, renderProcessText('thought', text));
|
|
190
185
|
turn.pendingThoughtKey = undefined;
|
|
191
186
|
turn.pendingThoughtText = undefined;
|
|
192
187
|
return rendered;
|
|
193
188
|
}
|
|
194
189
|
function getLarkRenderedMessageId(turn, rendered) {
|
|
195
190
|
if (rendered.isFinal) {
|
|
196
|
-
return
|
|
191
|
+
return undefined;
|
|
197
192
|
}
|
|
198
193
|
return turn.progressMessageIds[rendered.key];
|
|
199
194
|
}
|
|
@@ -229,21 +224,21 @@ function progressKey(prefix, event) {
|
|
|
229
224
|
return `${prefix}:${content.slice(0, 120)}`;
|
|
230
225
|
}
|
|
231
226
|
function isThoughtBridgeEvent(event) {
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
const kind = resolveBridgeEventKind(event);
|
|
228
|
+
return kind === 'thought' || kind === 'plan';
|
|
229
|
+
}
|
|
230
|
+
function resolveBridgeEventKind(event) {
|
|
231
|
+
if (event.bridgeKind) {
|
|
232
|
+
return event.bridgeKind;
|
|
234
233
|
}
|
|
235
|
-
return
|
|
234
|
+
return event.type === 'status' ? 'status' : 'assistant';
|
|
235
|
+
}
|
|
236
|
+
function renderProcessText(kind, content) {
|
|
237
|
+
const text = content.trim();
|
|
238
|
+
return kind === 'plan' ? `计划\n${text}` : text;
|
|
236
239
|
}
|
|
237
240
|
function summarizeToolContentForLark(content) {
|
|
238
|
-
|
|
239
|
-
if (!normalized.startsWith('🔧 ')) {
|
|
240
|
-
return normalized;
|
|
241
|
-
}
|
|
242
|
-
const withoutOutput = normalized.split(' - ')[0]?.trim() || normalized;
|
|
243
|
-
return withoutOutput
|
|
244
|
-
.replace(/\s+-\s*(running|pending|completed|failed|error|cancelled|canceled)\s*$/i, '')
|
|
245
|
-
.replace(/:\s*(running|pending|completed|failed|error|cancelled|canceled)\s*$/i, '')
|
|
246
|
-
.trim();
|
|
241
|
+
return String(content || '').trim();
|
|
247
242
|
}
|
|
248
243
|
function formatToolInputForLark(input) {
|
|
249
244
|
if (input == null) {
|
package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js
CHANGED
|
@@ -130,16 +130,16 @@ function truncateTextByUtf8Bytes(text, maxBytes) {
|
|
|
130
130
|
}
|
|
131
131
|
return `${result.trim()}${suffix}`;
|
|
132
132
|
}
|
|
133
|
-
function
|
|
134
|
-
const
|
|
135
|
-
if (!
|
|
136
|
-
return
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
function renderBridgeContentForWeixin(event) {
|
|
134
|
+
const toolCall = event.toolCall;
|
|
135
|
+
if (!toolCall) {
|
|
136
|
+
return String(event.content || '').trim();
|
|
137
|
+
}
|
|
138
|
+
const name = String(toolCall.name || '').trim() || 'Tool update';
|
|
139
|
+
const status = String(toolCall.status || '').trim();
|
|
140
|
+
if (name === 'Tool update' && status === 'completed')
|
|
139
141
|
return '';
|
|
140
|
-
|
|
141
|
-
return normalized;
|
|
142
|
-
return parts.slice(0, 2).join(' - ');
|
|
142
|
+
return [name, status].filter(Boolean).join(' - ');
|
|
143
143
|
}
|
|
144
144
|
// ==================== Gateway Class ====================
|
|
145
145
|
class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
|
|
@@ -1058,6 +1058,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
|
|
|
1058
1058
|
previewText: '',
|
|
1059
1059
|
finalText: '',
|
|
1060
1060
|
thinkingSteps: [],
|
|
1061
|
+
pendingThoughtText: undefined,
|
|
1061
1062
|
statusLines: [],
|
|
1062
1063
|
buttonRows: [],
|
|
1063
1064
|
lastSentAt: 0,
|
|
@@ -1070,12 +1071,14 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
|
|
|
1070
1071
|
return this.outboundTurns.get(sessionKey) || this.createTurnState(sessionKey);
|
|
1071
1072
|
}
|
|
1072
1073
|
consumeBridgeEvent(turn, event) {
|
|
1073
|
-
const content =
|
|
1074
|
+
const content = renderBridgeContentForWeixin(event);
|
|
1075
|
+
const bridgeKind = this.resolveBridgeEventKind(event);
|
|
1074
1076
|
if (event.type === 'typing_start') {
|
|
1075
1077
|
turn.processing = true;
|
|
1076
1078
|
turn.previewText = '';
|
|
1077
1079
|
turn.finalText = '';
|
|
1078
1080
|
turn.thinkingSteps = [];
|
|
1081
|
+
turn.pendingThoughtText = undefined;
|
|
1079
1082
|
turn.statusLines = [];
|
|
1080
1083
|
turn.buttonRows = [];
|
|
1081
1084
|
return;
|
|
@@ -1085,9 +1088,16 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
|
|
|
1085
1088
|
return;
|
|
1086
1089
|
}
|
|
1087
1090
|
if (event.type === 'preview_start' || event.type === 'update_message') {
|
|
1091
|
+
if (bridgeKind === 'thought') {
|
|
1092
|
+
turn.pendingThoughtText = content;
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1088
1095
|
turn.previewText = content;
|
|
1089
1096
|
return;
|
|
1090
1097
|
}
|
|
1098
|
+
if (bridgeKind !== 'thought') {
|
|
1099
|
+
this.flushPendingThought(turn);
|
|
1100
|
+
}
|
|
1091
1101
|
if (event.type === 'status') {
|
|
1092
1102
|
if (content) {
|
|
1093
1103
|
this.pushUnique(turn.statusLines, content);
|
|
@@ -1110,8 +1120,12 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
|
|
|
1110
1120
|
}
|
|
1111
1121
|
if (!content)
|
|
1112
1122
|
return;
|
|
1113
|
-
if (
|
|
1114
|
-
this.pushUnique(turn.thinkingSteps, content
|
|
1123
|
+
if (bridgeKind === 'thought' || bridgeKind === 'plan') {
|
|
1124
|
+
this.pushUnique(turn.thinkingSteps, content);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (bridgeKind === 'tool' || bridgeKind === 'status') {
|
|
1128
|
+
this.pushUnique(turn.statusLines, content);
|
|
1115
1129
|
return;
|
|
1116
1130
|
}
|
|
1117
1131
|
turn.finalText = content;
|
|
@@ -1120,7 +1134,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
|
|
|
1120
1134
|
renderTurnText(turn) {
|
|
1121
1135
|
const sections = [];
|
|
1122
1136
|
if (turn.thinkingSteps.length > 0) {
|
|
1123
|
-
sections.push(
|
|
1137
|
+
sections.push(`**中间过程**\n${turn.thinkingSteps.map((step) => `• ${step.replace(/\s+/g, ' ').trim()}`).join('\n')}`);
|
|
1124
1138
|
}
|
|
1125
1139
|
if (turn.finalText) {
|
|
1126
1140
|
sections.push(turn.finalText);
|
|
@@ -1128,7 +1142,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
|
|
|
1128
1142
|
else if (turn.previewText) {
|
|
1129
1143
|
sections.push(turn.previewText);
|
|
1130
1144
|
}
|
|
1131
|
-
else if (turn.
|
|
1145
|
+
else if (turn.statusLines.length > 0) {
|
|
1132
1146
|
sections.push(`**处理中**\n${turn.statusLines.slice(-3).map((l) => `• ${l.replace(/\s+/g, ' ').trim()}`).join('\n')}`);
|
|
1133
1147
|
}
|
|
1134
1148
|
else if (turn.processing) {
|
|
@@ -1139,21 +1153,32 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
|
|
|
1139
1153
|
}
|
|
1140
1154
|
return sections.join('\n\n').trim();
|
|
1141
1155
|
}
|
|
1156
|
+
flushPendingThought(turn) {
|
|
1157
|
+
const text = String(turn.pendingThoughtText || '').trim();
|
|
1158
|
+
if (!text)
|
|
1159
|
+
return;
|
|
1160
|
+
this.pushUnique(turn.thinkingSteps, text);
|
|
1161
|
+
turn.pendingThoughtText = undefined;
|
|
1162
|
+
}
|
|
1142
1163
|
isTerminalBridgeMessage(event, rendered) {
|
|
1143
1164
|
if (event.type === 'buttons')
|
|
1144
1165
|
return true;
|
|
1145
1166
|
if (event.type !== 'reply')
|
|
1146
1167
|
return false;
|
|
1147
|
-
const
|
|
1148
|
-
if (
|
|
1168
|
+
const bridgeKind = this.resolveBridgeEventKind(event);
|
|
1169
|
+
if (bridgeKind === 'tool' || bridgeKind === 'thought' || bridgeKind === 'plan' || bridgeKind === 'status')
|
|
1149
1170
|
return false;
|
|
1150
1171
|
const normalized = rendered.trim();
|
|
1151
1172
|
if (!normalized)
|
|
1152
1173
|
return false;
|
|
1153
|
-
if (normalized.startsWith('🔧 ') || normalized.startsWith('💭 '))
|
|
1154
|
-
return false;
|
|
1155
1174
|
return true;
|
|
1156
1175
|
}
|
|
1176
|
+
resolveBridgeEventKind(event) {
|
|
1177
|
+
if (event.bridgeKind) {
|
|
1178
|
+
return event.bridgeKind;
|
|
1179
|
+
}
|
|
1180
|
+
return event.type === 'status' ? 'status' : 'assistant';
|
|
1181
|
+
}
|
|
1157
1182
|
// ==================== Private: Helpers ====================
|
|
1158
1183
|
pushUnique(target, value) {
|
|
1159
1184
|
const normalized = value.trim();
|