@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.
Files changed (70) hide show
  1. package/README.md +5 -0
  2. package/dist/renderer/assets/{Badge-B-Ayib4h.js → Badge-DlVJI1Ys.js} +1 -1
  3. package/dist/renderer/assets/{Chat-CNEOxtyz.js → Chat-oa8OjaHQ.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-BTd1QBj9.js → ChatMarkdown-C-N8KS5C.js} +1 -1
  5. package/dist/renderer/assets/{Config-ByKhcLa6.js → Config-D9sL_d4D.js} +1 -1
  6. package/dist/renderer/assets/{CronList-0mi8dcC5.js → CronList-Z5JU3DHk.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-C8-tB_qg.js → Dashboard-D3BwbG43.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-TR-9nr5e.js → EmptyState-QOj765Ul.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-DKAxFOXc.js → KnowledgeDetail-J7n01mPu.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeHome-CM-1IDr4.js → KnowledgeHome-BFnH0NVk.js} +1 -1
  11. package/dist/renderer/assets/{Logs-DI5RFPri.js → Logs-DfSUifXc.js} +1 -1
  12. package/dist/renderer/assets/{Modal-CQpa4NPP.js → Modal-QjEGhyru.js} +1 -1
  13. package/dist/renderer/assets/{Page-BHXEAwzQ.js → Page-qdHFSw7x.js} +1 -1
  14. package/dist/renderer/assets/{ProjectDetail-Dn5elnHx.js → ProjectDetail-DS_rfvhh.js} +1 -1
  15. package/dist/renderer/assets/{ProjectList-D6yvEgb5.js → ProjectList-COtyZd7H.js} +1 -1
  16. package/dist/renderer/assets/{Select-BbkcXnXL.js → Select-BQ8mncVu.js} +1 -1
  17. package/dist/renderer/assets/{SessionChat-Vkf-7gvX.js → SessionChat-DjVnjuQy.js} +1 -1
  18. package/dist/renderer/assets/{SessionList-c5encjWd.js → SessionList-88dgGZJ8.js} +1 -1
  19. package/dist/renderer/assets/ThreadChat-BCdbTxBo.js +23 -0
  20. package/dist/renderer/assets/{Workspace-BC7hWqhP.js → Workspace-D_hzSJjX.js} +1 -1
  21. package/dist/renderer/assets/{arrow-left-I_Y1kdN9.js → arrow-left-BMpknulW.js} +1 -1
  22. package/dist/renderer/assets/{book-open-B3anUYvp.js → book-open-CiaOeQbU.js} +1 -1
  23. package/dist/renderer/assets/{check-BPNCDKgS.js → check-CgwYH4GF.js} +1 -1
  24. package/dist/renderer/assets/{chevron-down-BaxUcOIV.js → chevron-down-BzD2S1OO.js} +1 -1
  25. package/dist/renderer/assets/{circle-DJQiypFS.js → circle-BDHD7g7l.js} +1 -1
  26. package/dist/renderer/assets/{desktop-Cv51clcN.js → desktop-BiR5LPL5.js} +1 -1
  27. package/dist/renderer/assets/{funnel-DqKBtiqE.js → funnel-Bruf5FYA.js} +1 -1
  28. package/dist/renderer/assets/{heart-CmoNjvMO.js → heart-D7Mz04B0.js} +1 -1
  29. package/dist/renderer/assets/{index-DXJl2HOK.js → index-CL1e8m3U.js} +2 -2
  30. package/dist/renderer/assets/{index-BfXqhQ1Z.js → index-gZIRr4_o.js} +1 -1
  31. package/dist/renderer/assets/{pencil-CLUBszbw.js → pencil-C6gzDMse.js} +1 -1
  32. package/dist/renderer/assets/{play-l2RPZuAi.js → play-DqTXX-Pr.js} +1 -1
  33. package/dist/renderer/assets/{plug-CeUPIjRt.js → plug-ykmbeaO3.js} +1 -1
  34. package/dist/renderer/assets/{plus-BjkefS2P.js → plus-DHE60vMX.js} +1 -1
  35. package/dist/renderer/assets/projects-Bx5p10th.js +1 -0
  36. package/dist/renderer/assets/{save-D0yYt9tz.js → save-CGIvrdQI.js} +1 -1
  37. package/dist/renderer/assets/{search-B7s473Y4.js → search-D7IFAwyE.js} +1 -1
  38. package/dist/renderer/assets/{server-Dz_h8X_8.js → server-aOsirp7I.js} +1 -1
  39. package/dist/renderer/assets/{session-utils-BvNRTRXA.js → session-utils-Dxfp4ohQ.js} +1 -1
  40. package/dist/renderer/assets/{sessions-KPJsgKbQ.js → sessions-Db1XPbG2.js} +1 -1
  41. package/dist/renderer/assets/{shield-check-Wc5wfUUs.js → shield-check-ChXeQK-d.js} +1 -1
  42. package/dist/renderer/assets/{trash-2-7INdaB9w.js → trash-2-CfN-c1zw.js} +1 -1
  43. package/dist/renderer/assets/{user-CLbCgWNR.js → user-C7OHaBp4.js} +1 -1
  44. package/dist/renderer/assets/{wifi-off-BfDVPXbO.js → wifi-off-CpaJigye.js} +1 -1
  45. package/dist/renderer/index.html +1 -1
  46. package/dist-electron/electron/agent-runtime-detector.test.js +20 -0
  47. package/dist-electron/electron/lac-cli.test.js +2 -13
  48. package/dist-electron/electron/local-core-refactor.test.js +424 -64
  49. package/dist-electron/electron/workspace-task-store.test.js +118 -0
  50. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +17 -10
  51. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-progress.js +245 -9
  52. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +24 -11
  53. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-store.js +36 -11
  54. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-transport.js +2 -0
  55. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +90 -7
  56. package/dist-electron/services/local-ai-core/src/channel/lark/runtime-state.js +35 -40
  57. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +43 -18
  58. package/dist-electron/services/local-ai-core/src/cli/lac.js +1 -17
  59. package/dist-electron/services/local-ai-core/src/router/workspace-router.js +2 -2
  60. package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +1 -1
  61. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +31 -1
  62. package/dist-electron/services/local-ai-core/src/scheduler/local-schedule-adapter.js +4 -1
  63. package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js +4 -1
  64. package/dist-electron/src/pages/Threads/thread-chat-message-blocks.js +4 -114
  65. package/dist-electron/src/pages/Threads/thread-chat-model.js +14 -23
  66. package/dist-electron/src/pages/Threads/thread-chat-page-state.js +1 -1
  67. package/dist-electron/src/pages/Threads/thread-chat-permission.test.js +58 -31
  68. package/package.json +1 -1
  69. package/dist/renderer/assets/ThreadChat-DY4wzomi.js +0 -23
  70. package/dist/renderer/assets/projects-De73QY_o.js +0 -1
@@ -11,6 +11,8 @@ const node_os_1 = require("node:os");
11
11
  const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/local-core-acp-store.js");
12
12
  const local_core_acp_session_coordinator_js_1 = require("../services/local-ai-core/src/acp/local-core-acp-session-coordinator.js");
13
13
  const scheduler_service_js_1 = require("../services/local-ai-core/src/scheduler/scheduler-service.js");
14
+ const bootstrap_js_1 = require("../services/local-ai-core/src/kernel/bootstrap.js");
15
+ const local_core_controller_js_1 = require("../services/local-ai-core/src/runtime/local-core-controller.js");
14
16
  (0, node_test_1.default)('workspace registry entries persist in LocalCoreAcpStore', () => {
15
17
  const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'workspace-registry-'));
16
18
  try {
@@ -36,6 +38,50 @@ const scheduler_service_js_1 = require("../services/local-ai-core/src/scheduler/
36
38
  (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
37
39
  }
38
40
  });
41
+ (0, node_test_1.default)('scheduler create resolves a Lark route from the current thread binding', async () => {
42
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-binding-route-'));
43
+ try {
44
+ const runtime = (0, bootstrap_js_1.bootstrapLocalCoreRuntime)({
45
+ userDataPath,
46
+ enableKnowledge: false,
47
+ log: () => { },
48
+ });
49
+ const controller = new local_core_controller_js_1.LocalCoreController(userDataPath, runtime);
50
+ const thread = runtime.store.createThread('workspace-a', 'Lark thread');
51
+ const now = new Date().toISOString();
52
+ runtime.store.upsertPlatformThreadBinding({
53
+ workspace_id: 'workspace-a',
54
+ platform: 'lark',
55
+ chat_id: 'chat-1',
56
+ platform_user_id: 'user-1',
57
+ thread_id: thread.id,
58
+ last_platform_message_id: null,
59
+ created_at: now,
60
+ updated_at: now,
61
+ });
62
+ const job = await controller.createScheduledJob({
63
+ workspaceId: 'workspace-a',
64
+ threadId: thread.id,
65
+ executionMode: 'side-thread',
66
+ triggerType: 'cron',
67
+ cronExpr: '30 18 * * *',
68
+ promptTemplate: 'ping',
69
+ description: 'bound lark task',
70
+ enabled: true,
71
+ });
72
+ strict_1.default.equal(job.platform, 'lark');
73
+ strict_1.default.deepEqual(job.route, {
74
+ type: 'channel.chat',
75
+ channelId: 'chat-1',
76
+ participantId: 'user-1',
77
+ threadId: thread.id,
78
+ });
79
+ await controller.close();
80
+ }
81
+ finally {
82
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
83
+ }
84
+ });
39
85
  (0, node_test_1.default)('ACP runtime env includes the current workspace path for file returns', async () => {
40
86
  const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'workspace-env-'));
41
87
  try {
@@ -92,6 +138,78 @@ const scheduler_service_js_1 = require("../services/local-ai-core/src/scheduler/
92
138
  (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
93
139
  }
94
140
  });
141
+ (0, node_test_1.default)('ACP scheduled session can override permission mode without changing thread mode', async () => {
142
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-permission-mode-'));
143
+ try {
144
+ const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
145
+ const thread = store.createThread('workspace-a', 'Thread');
146
+ const capturedSessionNewParams = [];
147
+ let closeCount = 0;
148
+ const coordinator = new local_core_acp_session_coordinator_js_1.LocalCoreAcpSessionCoordinator({
149
+ store,
150
+ transport: {
151
+ spawnSession(input) {
152
+ return {
153
+ threadId: input.threadId,
154
+ bridgeSessionKey: input.bridgeSessionKey,
155
+ closed: false,
156
+ sessionId: '',
157
+ supportsLoad: false,
158
+ pendingPermissionByRun: new Map(),
159
+ schedulerJobCreatedByRun: new Map(),
160
+ launchPermissionMode: '',
161
+ };
162
+ },
163
+ initializeSession: async () => { },
164
+ request: async (_session, method, params) => {
165
+ if (method === 'session/new') {
166
+ capturedSessionNewParams.push(params);
167
+ return { sessionId: 'session-1' };
168
+ }
169
+ return {};
170
+ },
171
+ closeSession: () => {
172
+ closeCount += 1;
173
+ },
174
+ closeSessionWithError: () => { },
175
+ sendRaw: () => true,
176
+ },
177
+ runThreadMap: new Map(),
178
+ emitBridge: () => { },
179
+ });
180
+ await coordinator.ensureSession(thread.id, 'session:thread-1', {
181
+ workspaceId: 'workspace-a',
182
+ agentType: 'claudecode',
183
+ command: 'claude',
184
+ args: [],
185
+ env: {},
186
+ workDir: '/tmp/workspace-a',
187
+ model: '',
188
+ }, { permissionMode: 'bypassPermissions' });
189
+ strict_1.default.equal(capturedSessionNewParams[0]?._meta?.claudeCode?.options?.permissionMode, 'bypassPermissions');
190
+ strict_1.default.equal(store.getThreadRow(thread.id)?.agent_mode, 'default');
191
+ await coordinator.ensureSession(thread.id, 'session:thread-1', {
192
+ workspaceId: 'workspace-a',
193
+ agentType: 'claudecode',
194
+ command: 'claude',
195
+ args: [],
196
+ env: {},
197
+ workDir: '/tmp/workspace-a',
198
+ model: '',
199
+ });
200
+ strict_1.default.equal(closeCount, 1);
201
+ strict_1.default.deepEqual(capturedSessionNewParams[1]?._meta, {
202
+ claudeCode: {
203
+ emitRawSDKMessages: [
204
+ { type: 'system', subtype: 'local_command_output' },
205
+ ],
206
+ },
207
+ });
208
+ }
209
+ finally {
210
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
211
+ }
212
+ });
95
213
  (0, node_test_1.default)('scheduler uses short ids for new jobs and resolves legacy full ids by short id', () => {
96
214
  const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-id-store-'));
97
215
  try {
@@ -10,6 +10,7 @@ const local_core_acp_response_processor_js_1 = require("./local-core-acp-respons
10
10
  const local_core_acp_content_js_1 = require("./local-core-acp-content.js");
11
11
  const command_risk_js_1 = require("../security/command-risk.js");
12
12
  const local_core_slash_commands_js_1 = require("./local-core-slash-commands.js");
13
+ const local_core_acp_progress_js_1 = require("./local-core-acp-progress.js");
13
14
  const ACP_PROMPT_TIMEOUT_MS = 15 * 60 * 1000;
14
15
  class LocalCoreAcpBackend {
15
16
  options;
@@ -27,11 +28,11 @@ class LocalCoreAcpBackend {
27
28
  });
28
29
  this.turnCoordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
29
30
  emitBridge: (event) => this.emitBridgeEvent(event),
30
- appendMessage: (threadId, role, content, kind, toolCall) => {
31
- this.options.store.appendMessage(threadId, role, content, kind, toolCall);
31
+ appendMessage: (threadId, role, content, kind, toolCall, bridgeKind, bridgeStatus) => {
32
+ this.options.store.appendMessage(threadId, role, content, kind, toolCall, bridgeKind, bridgeStatus);
32
33
  },
33
- upsertMessage: (threadId, id, role, content, kind, toolCall) => {
34
- this.options.store.upsertMessage(threadId, id, role, content, kind, toolCall);
34
+ upsertMessage: (threadId, id, role, content, kind, toolCall, bridgeKind, bridgeStatus) => {
35
+ this.options.store.upsertMessage(threadId, id, role, content, kind, toolCall, bridgeKind, bridgeStatus);
35
36
  },
36
37
  updateRunStatus: (runId, threadId, status) => {
37
38
  this.options.store.updateRun(runId, threadId, status);
@@ -129,7 +130,7 @@ class LocalCoreAcpBackend {
129
130
  this.options.store.deleteThread(threadId);
130
131
  return { deleted: true };
131
132
  }
132
- async sendThreadMessage(threadId, input, config) {
133
+ async sendThreadMessage(threadId, input, config, options = {}) {
133
134
  if (!config) {
134
135
  throw new Error('localcore-acp message send requires a workspace config.');
135
136
  }
@@ -199,7 +200,7 @@ class LocalCoreAcpBackend {
199
200
  sessionKey: row.bridge_session_key,
200
201
  },
201
202
  });
202
- void this.runPrompt(threadId, runId, row.bridge_session_key, config, message).catch((error) => {
203
+ void this.runPrompt(threadId, runId, row.bridge_session_key, config, message, options).catch((error) => {
203
204
  this.options.log?.(`localcore-acp prompt failed for ${threadId}: ${error instanceof Error ? error.message : String(error)}`);
204
205
  });
205
206
  return { runId };
@@ -283,7 +284,7 @@ class LocalCoreAcpBackend {
283
284
  async interruptRun(runId) {
284
285
  return this.sessionCoordinator.interruptRun(runId);
285
286
  }
286
- async runPrompt(threadId, runId, bridgeSessionKey, config, input) {
287
+ async runPrompt(threadId, runId, bridgeSessionKey, config, input, options = {}) {
287
288
  const row = this.options.store.getThreadRow(threadId);
288
289
  if (!row) {
289
290
  throw new Error(`Thread not found: ${threadId}`);
@@ -297,16 +298,19 @@ class LocalCoreAcpBackend {
297
298
  const content = message.displayText;
298
299
  let session = null;
299
300
  try {
300
- session = await this.sessionCoordinator.ensureSession(threadId, bridgeSessionKey, config);
301
+ session = await this.sessionCoordinator.ensureSession(threadId, bridgeSessionKey, config, {
302
+ permissionMode: options.permissionMode,
303
+ });
301
304
  session.currentRunId = runId;
302
305
  session.currentTurn = {
303
306
  runId,
304
307
  replyCtx: runId,
305
308
  previewHandle: (0, node_crypto_1.randomUUID)(),
306
309
  thoughtPreviewHandle: (0, node_crypto_1.randomUUID)(),
307
- thoughtMessageId: `${runId}-thought`,
310
+ thoughtMessageId: `${runId}-thought-1`,
308
311
  assistantText: '',
309
312
  thoughtText: '',
313
+ thoughtSequence: 1,
310
314
  typingStarted: true,
311
315
  previewStarted: false,
312
316
  thoughtPreviewStarted: false,
@@ -317,6 +321,7 @@ class LocalCoreAcpBackend {
317
321
  pendingToolCalls: {},
318
322
  pendingToolCallOrder: [],
319
323
  toolCallSequence: 0,
324
+ toolObservations: [],
320
325
  permission: null,
321
326
  };
322
327
  const promptPromise = this.transport.request(session, 'session/prompt', {
@@ -330,9 +335,11 @@ class LocalCoreAcpBackend {
330
335
  if (!currentTurn || currentTurn.runId !== runId) {
331
336
  return;
332
337
  }
338
+ this.turnCoordinator.closePendingThoughtSegment(session);
333
339
  this.turnCoordinator.flushPendingToolCall(session);
334
340
  if (currentTurn.assistantText) {
335
- const processed = await this.responseProcessor.processAssistantResponse(threadId, currentTurn.assistantText);
341
+ const assistantText = (0, local_core_acp_progress_js_1.stripObservedToolTranscriptsFromAssistantText)(currentTurn.assistantText, currentTurn.toolObservations);
342
+ const processed = await this.responseProcessor.processAssistantResponse(threadId, assistantText);
336
343
  if (processed.displayContent) {
337
344
  this.options.store.appendMessage(threadId, 'assistant', processed.displayContent, 'final');
338
345
  this.options.eventBus.emit({
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.applyAssistantMessageChunk = applyAssistantMessageChunk;
4
4
  exports.applyThoughtChunk = applyThoughtChunk;
5
+ exports.closeThoughtSegment = closeThoughtSegment;
5
6
  exports.registerPendingToolCall = registerPendingToolCall;
6
7
  exports.resolveToolCallForUpdate = resolveToolCallForUpdate;
7
8
  exports.resolveFallbackToolCall = resolveFallbackToolCall;
@@ -16,6 +17,8 @@ exports.isTerminalToolStatus = isTerminalToolStatus;
16
17
  exports.formatPlanProgress = formatPlanProgress;
17
18
  exports.extractToolCallKey = extractToolCallKey;
18
19
  exports.extractToolCallInput = extractToolCallInput;
20
+ exports.recordToolObservation = recordToolObservation;
21
+ exports.stripObservedToolTranscriptsFromAssistantText = stripObservedToolTranscriptsFromAssistantText;
19
22
  function applyAssistantMessageChunk(currentTurn, text) {
20
23
  if (!text) {
21
24
  return null;
@@ -27,20 +30,26 @@ function applyAssistantMessageChunk(currentTurn, text) {
27
30
  bridgeType: 'preview_start',
28
31
  previewHandle: currentTurn.previewHandle,
29
32
  content: currentTurn.assistantText,
33
+ bridgeKind: 'assistant',
30
34
  };
31
35
  }
32
36
  return {
33
37
  bridgeType: 'update_message',
34
38
  previewHandle: currentTurn.previewHandle,
35
39
  content: currentTurn.assistantText,
40
+ bridgeKind: 'assistant',
36
41
  };
37
42
  }
38
43
  function applyThoughtChunk(currentTurn, text) {
39
44
  if (!text) {
40
45
  return null;
41
46
  }
42
- currentTurn.thoughtText = mergeStreamingText(currentTurn.thoughtText, text);
43
- const content = `💭 ${currentTurn.thoughtText.trim()}`;
47
+ const projection = mergeThoughtSegment(currentTurn.thoughtText, text);
48
+ currentTurn.thoughtText = projection.fullText;
49
+ const content = currentTurn.thoughtText.trim();
50
+ if (!content) {
51
+ return null;
52
+ }
44
53
  if (!currentTurn.thoughtPreviewStarted) {
45
54
  currentTurn.thoughtPreviewStarted = true;
46
55
  return {
@@ -48,6 +57,7 @@ function applyThoughtChunk(currentTurn, text) {
48
57
  previewHandle: currentTurn.thoughtPreviewHandle,
49
58
  messageId: currentTurn.thoughtMessageId,
50
59
  content,
60
+ bridgeKind: 'thought',
51
61
  };
52
62
  }
53
63
  return {
@@ -55,25 +65,70 @@ function applyThoughtChunk(currentTurn, text) {
55
65
  previewHandle: currentTurn.thoughtPreviewHandle,
56
66
  messageId: currentTurn.thoughtMessageId,
57
67
  content,
68
+ bridgeKind: 'thought',
58
69
  };
59
70
  }
60
- function mergeStreamingText(current, next) {
71
+ function closeThoughtSegment(currentTurn) {
72
+ const thoughtText = currentTurn.thoughtText || '';
73
+ if (!thoughtText.trim()) {
74
+ return;
75
+ }
76
+ const nextSequence = (currentTurn.thoughtSequence || 1) + 1;
77
+ const runId = currentTurn.runId || 'thought';
78
+ currentTurn.thoughtSequence = nextSequence;
79
+ currentTurn.thoughtText = '';
80
+ currentTurn.thoughtPreviewStarted = false;
81
+ currentTurn.thoughtMessageId = `${runId}-thought-${nextSequence}`;
82
+ currentTurn.thoughtPreviewHandle = `${runId}-thought-preview-${nextSequence}`;
83
+ }
84
+ function mergeThoughtSegment(current, next) {
61
85
  if (!current) {
62
- return next;
86
+ return { fullText: next, segmentText: next };
63
87
  }
64
88
  if (next.startsWith(current)) {
65
- return next;
89
+ return {
90
+ fullText: next,
91
+ segmentText: next.slice(current.length),
92
+ };
66
93
  }
67
94
  if (current.endsWith(next)) {
68
- return current;
95
+ return {
96
+ fullText: current,
97
+ segmentText: '',
98
+ };
69
99
  }
70
100
  const maxOverlap = Math.min(current.length, next.length);
71
101
  for (let size = maxOverlap; size > 0; size -= 1) {
72
102
  if (current.endsWith(next.slice(0, size))) {
73
- return current + next.slice(size);
103
+ const suffix = next.slice(size);
104
+ return {
105
+ fullText: current + suffix,
106
+ segmentText: suffix,
107
+ };
74
108
  }
75
109
  }
76
- return current + next;
110
+ return {
111
+ fullText: appendThoughtPiece(current, next),
112
+ segmentText: next,
113
+ };
114
+ }
115
+ function appendThoughtPiece(current, next) {
116
+ if (!current.trim()) {
117
+ return next;
118
+ }
119
+ if (!next.trim()) {
120
+ return current;
121
+ }
122
+ if (/^[,.;:!?,。;:!?、)】》]/.test(next.trimStart())) {
123
+ return current.trimEnd() + next.trimStart();
124
+ }
125
+ if (/^[-/]/.test(next.trimStart()) || /[-/][A-Za-z0-9]*$/.test(current.trimEnd())) {
126
+ return current.trimEnd() + next.trimStart();
127
+ }
128
+ if (/\s$/.test(current) || /^\s/.test(next)) {
129
+ return current + next;
130
+ }
131
+ return `${current.trimEnd()} ${next.trimStart()}`;
77
132
  }
78
133
  function registerPendingToolCall(input) {
79
134
  const title = String(input.update.title || 'Running tool').trim();
@@ -177,7 +232,7 @@ function formatPlanProgress(entries) {
177
232
  .map((entry) => String(entry?.content || '').trim())
178
233
  .filter(Boolean)
179
234
  .join(' | ');
180
- return summary ? `💭 ${summary}` : '';
235
+ return summary;
181
236
  }
182
237
  function extractToolCallKey(update) {
183
238
  for (const key of ['toolCallId', 'tool_call_id', 'callId', 'call_id', 'invocationId', 'invocation_id', 'id']) {
@@ -199,3 +254,184 @@ function extractToolCallInput(update) {
199
254
  }
200
255
  return undefined;
201
256
  }
257
+ function recordToolObservation(currentTurn, observation) {
258
+ const normalized = {
259
+ ...observation,
260
+ name: observation.name?.trim() || undefined,
261
+ title: observation.title?.trim() || undefined,
262
+ status: observation.status?.trim() || undefined,
263
+ outputText: observation.outputText?.trim() || undefined,
264
+ };
265
+ if (!normalized.name && !normalized.title && normalized.input === undefined && !normalized.outputText) {
266
+ return;
267
+ }
268
+ currentTurn.toolObservations = [
269
+ ...(currentTurn.toolObservations || []),
270
+ normalized,
271
+ ];
272
+ }
273
+ function stripObservedToolTranscriptsFromAssistantText(text, observations) {
274
+ let remaining = String(text || '');
275
+ const knownObservations = observations || [];
276
+ for (let guard = 0; guard < knownObservations.length; guard += 1) {
277
+ const stripped = stripOneObservedToolTranscriptPrefix(remaining, knownObservations);
278
+ if (stripped === remaining) {
279
+ break;
280
+ }
281
+ remaining = stripped;
282
+ }
283
+ return remaining.trim();
284
+ }
285
+ function stripOneObservedToolTranscriptPrefix(text, observations) {
286
+ const trimmedStart = text.replace(/^\s+/, '');
287
+ if (!trimmedStart) {
288
+ return text;
289
+ }
290
+ const matchingEvidence = observations
291
+ .map((observation) => createToolObservationEvidence(observation))
292
+ .find((evidence) => matchesObservedToolTranscriptPrefix(trimmedStart, evidence));
293
+ if (!matchingEvidence) {
294
+ return text;
295
+ }
296
+ const boundary = findToolTranscriptPrefixBoundary(trimmedStart, matchingEvidence);
297
+ if (boundary <= 0) {
298
+ return text;
299
+ }
300
+ return trimmedStart.slice(boundary);
301
+ }
302
+ function createToolObservationEvidence(observation) {
303
+ const serializedInput = serializeObservationValue(observation.input);
304
+ const serializedOutput = observation.outputText || '';
305
+ const structuredValues = collectStructuredStringValues(serializedOutput);
306
+ return {
307
+ toolNames: uniqueNonEmpty([
308
+ observation.name,
309
+ observation.title,
310
+ ].map((value) => normalizeEvidencePhrase(value || ''))),
311
+ urls: uniqueNonEmpty([
312
+ ...extractUrls(serializedInput),
313
+ ...extractUrls(serializedOutput),
314
+ ]),
315
+ phrases: uniqueNonEmpty([
316
+ ...structuredValues,
317
+ observation.title,
318
+ serializedInput.length <= 300 ? serializedInput : '',
319
+ ].map((value) => normalizeEvidencePhrase(value || '')).filter((value) => value.length >= 12)),
320
+ };
321
+ }
322
+ function matchesObservedToolTranscriptPrefix(text, evidence) {
323
+ const prefix = text.slice(0, 3000);
324
+ const normalizedPrefix = normalizeEvidencePhrase(prefix);
325
+ const hasToolName = evidence.toolNames.some((name) => name && normalizedPrefix.includes(name));
326
+ const urlMatches = evidence.urls.filter((url) => prefix.includes(url)).length;
327
+ const phraseMatches = evidence.phrases.filter((phrase) => normalizedPrefix.includes(phrase)).length;
328
+ if (hasToolName && (urlMatches > 0 || phraseMatches > 0)) {
329
+ return true;
330
+ }
331
+ return urlMatches > 0 && phraseMatches > 0;
332
+ }
333
+ function findToolTranscriptPrefixBoundary(text, evidence) {
334
+ const lines = text.split(/\n/);
335
+ const firstContentLine = lines.find((line) => line.trim());
336
+ if (!firstContentLine || !isTranscriptStartLine(firstContentLine, evidence)) {
337
+ return 0;
338
+ }
339
+ let offset = 0;
340
+ let seenEvidence = false;
341
+ for (let index = 0; index < lines.length; index += 1) {
342
+ const line = lines[index] || '';
343
+ const lineWithNewlineLength = line.length + (index < lines.length - 1 ? 1 : 0);
344
+ if (lineContainsObservationEvidence(line, evidence)) {
345
+ seenEvidence = true;
346
+ }
347
+ if (seenEvidence && index > 0 && line.trim() && !isStructuredTranscriptLine(line, evidence)) {
348
+ return offset;
349
+ }
350
+ offset += lineWithNewlineLength;
351
+ }
352
+ return 0;
353
+ }
354
+ function isTranscriptStartLine(line, evidence) {
355
+ const trimmed = line.trim();
356
+ return lineContainsObservationEvidence(line, evidence) &&
357
+ (/^(```|\{|\[|\*\*[^*]+\*\*|#+\s)/.test(trimmed) || /^[A-Za-z0-9_.-]+:/.test(trimmed));
358
+ }
359
+ function isStructuredTranscriptLine(line, evidence) {
360
+ const trimmed = line.trim();
361
+ if (!trimmed) {
362
+ return true;
363
+ }
364
+ if (lineContainsObservationEvidence(line, evidence)) {
365
+ return true;
366
+ }
367
+ return /^(```|\{|\}|\[|\]|".+":|\*\*[^*]+:\*\*|\*[^*]+\*|#+\s)/.test(trimmed) ||
368
+ /[::]\s*$/.test(trimmed) ||
369
+ /[{}\[\]"]/g.test(trimmed) ||
370
+ /^[A-Za-z0-9_.-]+:/.test(trimmed);
371
+ }
372
+ function lineContainsObservationEvidence(line, evidence) {
373
+ const normalizedLine = normalizeEvidencePhrase(line);
374
+ return evidence.toolNames.some((name) => name && normalizedLine.includes(name)) ||
375
+ evidence.urls.some((url) => line.includes(url)) ||
376
+ evidence.phrases.some((phrase) => phrase && normalizedLine.includes(phrase));
377
+ }
378
+ function serializeObservationValue(value) {
379
+ if (value == null) {
380
+ return '';
381
+ }
382
+ if (typeof value === 'string') {
383
+ return value;
384
+ }
385
+ try {
386
+ return JSON.stringify(value);
387
+ }
388
+ catch {
389
+ return String(value);
390
+ }
391
+ }
392
+ function collectStructuredStringValues(text) {
393
+ const values = [];
394
+ try {
395
+ collectStringValues(JSON.parse(text), values);
396
+ }
397
+ catch {
398
+ const matches = text.matchAll(/"(title|description|url|name|summary)"\s*:\s*"([^"]{12,})"/gi);
399
+ for (const match of matches) {
400
+ values.push(match[2] || '');
401
+ }
402
+ }
403
+ return values;
404
+ }
405
+ function collectStringValues(value, values) {
406
+ if (typeof value === 'string') {
407
+ values.push(value);
408
+ return;
409
+ }
410
+ if (Array.isArray(value)) {
411
+ for (const item of value) {
412
+ collectStringValues(item, values);
413
+ }
414
+ return;
415
+ }
416
+ if (!value || typeof value !== 'object') {
417
+ return;
418
+ }
419
+ for (const entry of Object.values(value)) {
420
+ collectStringValues(entry, values);
421
+ }
422
+ }
423
+ function extractUrls(text) {
424
+ return [...text.matchAll(/https?:\/\/[^\s"'`<>),\]}]+/g)]
425
+ .map((match) => match[0])
426
+ .filter(Boolean);
427
+ }
428
+ function normalizeEvidencePhrase(value) {
429
+ return String(value || '')
430
+ .replace(/\\[nrt]/g, ' ')
431
+ .replace(/\s+/g, ' ')
432
+ .trim()
433
+ .toLowerCase();
434
+ }
435
+ function uniqueNonEmpty(values) {
436
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
437
+ }
@@ -25,9 +25,13 @@ class LocalCoreAcpSessionCoordinator {
25
25
  this.options.transport.closeSession(session);
26
26
  this.sessions.delete(threadId);
27
27
  }
28
- async ensureSession(threadId, bridgeSessionKey, config) {
28
+ async ensureSession(threadId, bridgeSessionKey, config, options = {}) {
29
29
  const existing = this.sessions.get(threadId);
30
- if (existing && !existing.closed && existing.sessionId) {
30
+ const permissionMode = this.resolveLaunchPermissionMode(threadId, options.permissionMode);
31
+ if (existing
32
+ && !existing.closed
33
+ && existing.sessionId
34
+ && existing.launchPermissionMode === permissionMode) {
31
35
  return existing;
32
36
  }
33
37
  if (existing) {
@@ -43,6 +47,7 @@ class LocalCoreAcpSessionCoordinator {
43
47
  config,
44
48
  runtimeEnv: this.buildAgentRuntimeEnv(threadId, String(baseEnv.PATH || '')),
45
49
  });
50
+ session.launchPermissionMode = permissionMode;
46
51
  this.sessions.set(threadId, session);
47
52
  await this.options.transport.initializeSession(session);
48
53
  const row = this.options.store.getThreadRow(threadId);
@@ -68,7 +73,7 @@ class LocalCoreAcpSessionCoordinator {
68
73
  const created = await this.options.transport.request(session, 'session/new', {
69
74
  cwd: config.workDir,
70
75
  mcpServers: [],
71
- _meta: this.buildSessionMeta(threadId),
76
+ _meta: this.buildSessionMeta(threadId, permissionMode),
72
77
  }, 30000);
73
78
  session.sessionId = String(created.sessionId || created.session_id || created.id || created.session?.sessionId || created.session?.session_id || created.session?.id || '').trim();
74
79
  if (!session.sessionId) {
@@ -182,17 +187,25 @@ class LocalCoreAcpSessionCoordinator {
182
187
  }
183
188
  return env;
184
189
  }
185
- buildSessionMeta(threadId) {
190
+ resolveLaunchPermissionMode(threadId, permissionModeOverride = '') {
186
191
  const row = this.options.store.getThreadRow(threadId);
187
- const mode = String(row?.agent_mode || '').trim();
188
- if (!mode || mode === 'default') {
189
- return undefined;
190
- }
192
+ const mode = String(permissionModeOverride || row?.agent_mode || '').trim();
193
+ return !mode || mode === 'default' ? '' : mode;
194
+ }
195
+ buildSessionMeta(threadId, permissionModeOverride = '') {
196
+ const mode = this.resolveLaunchPermissionMode(threadId, permissionModeOverride);
191
197
  return {
192
198
  claudeCode: {
193
- options: {
194
- permissionMode: mode,
195
- },
199
+ emitRawSDKMessages: [
200
+ { type: 'system', subtype: 'local_command_output' },
201
+ ],
202
+ ...(mode
203
+ ? {
204
+ options: {
205
+ permissionMode: mode,
206
+ },
207
+ }
208
+ : {}),
196
209
  },
197
210
  };
198
211
  }