@kafca/agentdock 0.1.28 → 0.1.29

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-ngf0eCzI.js} +1 -1
  3. package/dist/renderer/assets/{Chat-CNEOxtyz.js → Chat-tCGY5odh.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-BTd1QBj9.js → ChatMarkdown-vqDWgVNb.js} +1 -1
  5. package/dist/renderer/assets/{Config-ByKhcLa6.js → Config-YsVDQmIs.js} +1 -1
  6. package/dist/renderer/assets/{CronList-0mi8dcC5.js → CronList-DQN0T7Yc.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-C8-tB_qg.js → Dashboard-C2O6Yxq3.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-TR-9nr5e.js → EmptyState-CTkA-fBQ.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-DKAxFOXc.js → KnowledgeDetail-DjLZDItQ.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeHome-CM-1IDr4.js → KnowledgeHome-2NHeRZxW.js} +1 -1
  11. package/dist/renderer/assets/{Logs-DI5RFPri.js → Logs-CQ6j-Sct.js} +1 -1
  12. package/dist/renderer/assets/{Modal-CQpa4NPP.js → Modal-9g1uWJfC.js} +1 -1
  13. package/dist/renderer/assets/{Page-BHXEAwzQ.js → Page-B4mPLn6n.js} +1 -1
  14. package/dist/renderer/assets/{ProjectDetail-Dn5elnHx.js → ProjectDetail-pBXOAeL4.js} +1 -1
  15. package/dist/renderer/assets/{ProjectList-D6yvEgb5.js → ProjectList-B9PYfZaS.js} +1 -1
  16. package/dist/renderer/assets/{Select-BbkcXnXL.js → Select-BWFQJ2R8.js} +1 -1
  17. package/dist/renderer/assets/{SessionChat-Vkf-7gvX.js → SessionChat-fBrzJs3A.js} +1 -1
  18. package/dist/renderer/assets/{SessionList-c5encjWd.js → SessionList-DjHOsk7X.js} +1 -1
  19. package/dist/renderer/assets/ThreadChat-MagPeJD0.js +23 -0
  20. package/dist/renderer/assets/{Workspace-BC7hWqhP.js → Workspace-nfTcxlhw.js} +1 -1
  21. package/dist/renderer/assets/{arrow-left-I_Y1kdN9.js → arrow-left-CIWZ6JEB.js} +1 -1
  22. package/dist/renderer/assets/{book-open-B3anUYvp.js → book-open-Ck6BgBhN.js} +1 -1
  23. package/dist/renderer/assets/{check-BPNCDKgS.js → check-CtB2ZLVE.js} +1 -1
  24. package/dist/renderer/assets/{chevron-down-BaxUcOIV.js → chevron-down--8lODnN0.js} +1 -1
  25. package/dist/renderer/assets/{circle-DJQiypFS.js → circle-CKQOJcbh.js} +1 -1
  26. package/dist/renderer/assets/{desktop-Cv51clcN.js → desktop-Do2I28CT.js} +1 -1
  27. package/dist/renderer/assets/{funnel-DqKBtiqE.js → funnel-CkZuFIwa.js} +1 -1
  28. package/dist/renderer/assets/{heart-CmoNjvMO.js → heart-CAX_CqFQ.js} +1 -1
  29. package/dist/renderer/assets/{index-BfXqhQ1Z.js → index--_L0Y77h.js} +1 -1
  30. package/dist/renderer/assets/{index-DXJl2HOK.js → index-DvAYflFj.js} +2 -2
  31. package/dist/renderer/assets/{pencil-CLUBszbw.js → pencil-sIRhtchu.js} +1 -1
  32. package/dist/renderer/assets/{play-l2RPZuAi.js → play-CJNRbCL5.js} +1 -1
  33. package/dist/renderer/assets/{plug-CeUPIjRt.js → plug-DsFe9GBx.js} +1 -1
  34. package/dist/renderer/assets/{plus-BjkefS2P.js → plus-KDwEI-Rs.js} +1 -1
  35. package/dist/renderer/assets/projects-CJUs2vaq.js +1 -0
  36. package/dist/renderer/assets/{save-D0yYt9tz.js → save-C-p5r3K-.js} +1 -1
  37. package/dist/renderer/assets/{search-B7s473Y4.js → search-BZxQJb2k.js} +1 -1
  38. package/dist/renderer/assets/{server-Dz_h8X_8.js → server-BV-K022I.js} +1 -1
  39. package/dist/renderer/assets/{session-utils-BvNRTRXA.js → session-utils-ClQTa8yS.js} +1 -1
  40. package/dist/renderer/assets/{sessions-KPJsgKbQ.js → sessions-B0FSCA9O.js} +1 -1
  41. package/dist/renderer/assets/{shield-check-Wc5wfUUs.js → shield-check-B6AjkXUd.js} +1 -1
  42. package/dist/renderer/assets/{trash-2-7INdaB9w.js → trash-2-41Pv4qRx.js} +1 -1
  43. package/dist/renderer/assets/{user-CLbCgWNR.js → user-NKKHCKm9.js} +1 -1
  44. package/dist/renderer/assets/{wifi-off-BfDVPXbO.js → wifi-off-E1ZNhLG5.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 +127 -50
  49. package/dist-electron/electron/workspace-task-store.test.js +112 -0
  50. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +10 -8
  51. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-progress.js +6 -2
  52. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +15 -6
  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 +1 -0
  55. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +11 -5
  56. package/dist-electron/services/local-ai-core/src/channel/lark/runtime-state.js +34 -39
  57. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +31 -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,72 @@ 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.equal(capturedSessionNewParams[1]?._meta, undefined);
202
+ }
203
+ finally {
204
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
205
+ }
206
+ });
95
207
  (0, node_test_1.default)('scheduler uses short ids for new jobs and resolves legacy full ids by short id', () => {
96
208
  const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-id-store-'));
97
209
  try {
@@ -27,11 +27,11 @@ class LocalCoreAcpBackend {
27
27
  });
28
28
  this.turnCoordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
29
29
  emitBridge: (event) => this.emitBridgeEvent(event),
30
- appendMessage: (threadId, role, content, kind, toolCall) => {
31
- this.options.store.appendMessage(threadId, role, content, kind, toolCall);
30
+ appendMessage: (threadId, role, content, kind, toolCall, bridgeKind, bridgeStatus) => {
31
+ this.options.store.appendMessage(threadId, role, content, kind, toolCall, bridgeKind, bridgeStatus);
32
32
  },
33
- upsertMessage: (threadId, id, role, content, kind, toolCall) => {
34
- this.options.store.upsertMessage(threadId, id, role, content, kind, toolCall);
33
+ upsertMessage: (threadId, id, role, content, kind, toolCall, bridgeKind, bridgeStatus) => {
34
+ this.options.store.upsertMessage(threadId, id, role, content, kind, toolCall, bridgeKind, bridgeStatus);
35
35
  },
36
36
  updateRunStatus: (runId, threadId, status) => {
37
37
  this.options.store.updateRun(runId, threadId, status);
@@ -129,7 +129,7 @@ class LocalCoreAcpBackend {
129
129
  this.options.store.deleteThread(threadId);
130
130
  return { deleted: true };
131
131
  }
132
- async sendThreadMessage(threadId, input, config) {
132
+ async sendThreadMessage(threadId, input, config, options = {}) {
133
133
  if (!config) {
134
134
  throw new Error('localcore-acp message send requires a workspace config.');
135
135
  }
@@ -199,7 +199,7 @@ class LocalCoreAcpBackend {
199
199
  sessionKey: row.bridge_session_key,
200
200
  },
201
201
  });
202
- void this.runPrompt(threadId, runId, row.bridge_session_key, config, message).catch((error) => {
202
+ void this.runPrompt(threadId, runId, row.bridge_session_key, config, message, options).catch((error) => {
203
203
  this.options.log?.(`localcore-acp prompt failed for ${threadId}: ${error instanceof Error ? error.message : String(error)}`);
204
204
  });
205
205
  return { runId };
@@ -283,7 +283,7 @@ class LocalCoreAcpBackend {
283
283
  async interruptRun(runId) {
284
284
  return this.sessionCoordinator.interruptRun(runId);
285
285
  }
286
- async runPrompt(threadId, runId, bridgeSessionKey, config, input) {
286
+ async runPrompt(threadId, runId, bridgeSessionKey, config, input, options = {}) {
287
287
  const row = this.options.store.getThreadRow(threadId);
288
288
  if (!row) {
289
289
  throw new Error(`Thread not found: ${threadId}`);
@@ -297,7 +297,9 @@ class LocalCoreAcpBackend {
297
297
  const content = message.displayText;
298
298
  let session = null;
299
299
  try {
300
- session = await this.sessionCoordinator.ensureSession(threadId, bridgeSessionKey, config);
300
+ session = await this.sessionCoordinator.ensureSession(threadId, bridgeSessionKey, config, {
301
+ permissionMode: options.permissionMode,
302
+ });
301
303
  session.currentRunId = runId;
302
304
  session.currentTurn = {
303
305
  runId,
@@ -27,12 +27,14 @@ function applyAssistantMessageChunk(currentTurn, text) {
27
27
  bridgeType: 'preview_start',
28
28
  previewHandle: currentTurn.previewHandle,
29
29
  content: currentTurn.assistantText,
30
+ bridgeKind: 'assistant',
30
31
  };
31
32
  }
32
33
  return {
33
34
  bridgeType: 'update_message',
34
35
  previewHandle: currentTurn.previewHandle,
35
36
  content: currentTurn.assistantText,
37
+ bridgeKind: 'assistant',
36
38
  };
37
39
  }
38
40
  function applyThoughtChunk(currentTurn, text) {
@@ -40,7 +42,7 @@ function applyThoughtChunk(currentTurn, text) {
40
42
  return null;
41
43
  }
42
44
  currentTurn.thoughtText = mergeStreamingText(currentTurn.thoughtText, text);
43
- const content = `💭 ${currentTurn.thoughtText.trim()}`;
45
+ const content = currentTurn.thoughtText.trim();
44
46
  if (!currentTurn.thoughtPreviewStarted) {
45
47
  currentTurn.thoughtPreviewStarted = true;
46
48
  return {
@@ -48,6 +50,7 @@ function applyThoughtChunk(currentTurn, text) {
48
50
  previewHandle: currentTurn.thoughtPreviewHandle,
49
51
  messageId: currentTurn.thoughtMessageId,
50
52
  content,
53
+ bridgeKind: 'thought',
51
54
  };
52
55
  }
53
56
  return {
@@ -55,6 +58,7 @@ function applyThoughtChunk(currentTurn, text) {
55
58
  previewHandle: currentTurn.thoughtPreviewHandle,
56
59
  messageId: currentTurn.thoughtMessageId,
57
60
  content,
61
+ bridgeKind: 'thought',
58
62
  };
59
63
  }
60
64
  function mergeStreamingText(current, next) {
@@ -177,7 +181,7 @@ function formatPlanProgress(entries) {
177
181
  .map((entry) => String(entry?.content || '').trim())
178
182
  .filter(Boolean)
179
183
  .join(' | ');
180
- return summary ? `💭 ${summary}` : '';
184
+ return summary;
181
185
  }
182
186
  function extractToolCallKey(update) {
183
187
  for (const key of ['toolCallId', 'tool_call_id', 'callId', 'call_id', 'invocationId', 'invocation_id', 'id']) {
@@ -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,10 +187,14 @@ 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') {
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);
197
+ if (!mode) {
189
198
  return undefined;
190
199
  }
191
200
  return {
@@ -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',
@@ -38,6 +38,7 @@ class LocalCoreAcpTransport {
38
38
  closed: false,
39
39
  closeReason: null,
40
40
  promptPromise: null,
41
+ launchPermissionMode: '',
41
42
  };
42
43
  child.stdout.setEncoding('utf8');
43
44
  child.stdout.on('data', (chunk) => this.handleStdout(session, chunk));
@@ -160,11 +160,13 @@ class LocalCoreAcpTurnCoordinator {
160
160
  });
161
161
  this.options.updateRunStatus(currentRunId, session.threadId, 'awaiting_input');
162
162
  const permissionPrompt = (0, local_core_acp_permission_lifecycle_js_1.createPermissionPrompt)(toolTitle);
163
- this.options.appendMessage(session.threadId, 'assistant', permissionPrompt, 'progress');
163
+ this.options.appendMessage(session.threadId, 'assistant', permissionPrompt, 'progress', undefined, 'permission', 'awaiting_input');
164
164
  this.options.emitBridge({
165
165
  type: 'buttons',
166
166
  sessionKey: session.bridgeSessionKey,
167
167
  replyCtx: currentRunId,
168
+ bridgeKind: 'permission',
169
+ bridgeStatus: 'awaiting_input',
168
170
  content: permissionPrompt,
169
171
  buttonRows,
170
172
  });
@@ -194,6 +196,7 @@ class LocalCoreAcpTurnCoordinator {
194
196
  sessionKey: session.bridgeSessionKey,
195
197
  replyCtx: currentRunId,
196
198
  previewHandle: projection.previewHandle,
199
+ bridgeKind: projection.bridgeKind,
197
200
  content: projection.content,
198
201
  });
199
202
  return;
@@ -208,13 +211,14 @@ class LocalCoreAcpTurnCoordinator {
208
211
  return;
209
212
  }
210
213
  if (this.options.upsertMessage) {
211
- this.options.upsertMessage(session.threadId, projection.messageId, 'assistant', projection.content, 'progress');
214
+ this.options.upsertMessage(session.threadId, projection.messageId, 'assistant', projection.content, 'progress', undefined, projection.bridgeKind);
212
215
  }
213
216
  this.options.emitBridge({
214
217
  type: projection.bridgeType,
215
218
  sessionKey: session.bridgeSessionKey,
216
219
  replyCtx: currentRunId,
217
220
  previewHandle: projection.previewHandle,
221
+ bridgeKind: projection.bridgeKind,
218
222
  content: projection.content,
219
223
  });
220
224
  return;
@@ -290,11 +294,12 @@ class LocalCoreAcpTurnCoordinator {
290
294
  if (!content) {
291
295
  return;
292
296
  }
293
- this.options.appendMessage(session.threadId, 'assistant', content, 'progress');
297
+ this.options.appendMessage(session.threadId, 'assistant', content, 'progress', undefined, 'plan');
294
298
  this.options.emitBridge({
295
299
  type: 'reply',
296
300
  sessionKey: session.bridgeSessionKey,
297
301
  replyCtx: currentRunId,
302
+ bridgeKind: 'plan',
298
303
  content,
299
304
  });
300
305
  return;
@@ -305,16 +310,17 @@ class LocalCoreAcpTurnCoordinator {
305
310
  }
306
311
  emitProgress(session, currentRunId, content, messageId, toolCall) {
307
312
  if (messageId && this.options.upsertMessage) {
308
- this.options.upsertMessage(session.threadId, messageId, 'assistant', content, 'progress', toolCall);
313
+ this.options.upsertMessage(session.threadId, messageId, 'assistant', content, 'progress', toolCall, toolCall ? 'tool' : undefined);
309
314
  }
310
315
  else {
311
- this.options.appendMessage(session.threadId, 'assistant', content, 'progress', toolCall);
316
+ this.options.appendMessage(session.threadId, 'assistant', content, 'progress', toolCall, toolCall ? 'tool' : undefined);
312
317
  }
313
318
  this.options.emitBridge({
314
319
  type: 'reply',
315
320
  sessionKey: session.bridgeSessionKey,
316
321
  replyCtx: currentRunId,
317
322
  messageId,
323
+ bridgeKind: toolCall ? 'tool' : undefined,
318
324
  content,
319
325
  toolCall,
320
326
  });
@@ -31,31 +31,25 @@ function createLarkTurnState(sessionKey, sourceMessageId) {
31
31
  return turn;
32
32
  }
33
33
  function foldToolResultForLark(content) {
34
- const normalized = String(content || '').trim();
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 [name.startsWith('🔧 ') ? name : `🔧 ${name}`, input ? `参数:${input}` : '']
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(content) {
54
- const normalized = foldToolResultForLark(content).toLowerCase();
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 (content.startsWith('💭 ')) {
77
+ if (bridgeKind === 'thought') {
84
78
  turn.pendingThoughtKey = event.previewHandle || 'thinking-preview';
85
79
  turn.pendingThoughtText = content;
86
- pushUniqueLarkTurnLine(turn.thinkingSteps, content.slice(3).trim());
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 (content.startsWith('💭 ')) {
103
+ if (bridgeKind === 'thought' || bridgeKind === 'plan') {
110
104
  turn.pendingThoughtKey = progressKey('thinking', event);
111
105
  turn.pendingThoughtText = content;
112
- pushUniqueLarkTurnLine(turn.thinkingSteps, content.slice(3).trim());
106
+ pushUniqueLarkTurnLine(turn.thinkingSteps, content);
113
107
  return;
114
108
  }
115
- if (content.startsWith('🔧 ')) {
109
+ if (bridgeKind === 'tool') {
116
110
  pushUniqueLarkTurnLine(turn.toolCalls, content.slice(3).trim());
117
111
  return;
118
112
  }
119
- if (content.startsWith('⏳ ') || content.startsWith('📤 ')) {
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 (content.startsWith('💭 ')) {
124
+ if (bridgeKind === 'thought') {
130
125
  return { key: 'noop', text: '', buttonRows: [], isFinal: false };
131
126
  }
132
- if (content.startsWith('🔧 ')) {
133
- if (isPendingToolProgressForLark(content)) {
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), content.startsWith('⏳ ') ? content : `⏳ ${content}`);
136
+ return renderProgressMessage(progressKey('status', event), `⏳ ${content}`);
142
137
  }
143
138
  if (event.type === 'reply') {
144
- if (content.startsWith('💭 ')) {
145
- return { key: 'noop', text: '', buttonRows: [], isFinal: false };
139
+ if (bridgeKind === 'thought' || bridgeKind === 'plan') {
140
+ return renderProgressMessage(progressKey(bridgeKind, event), renderProcessText(bridgeKind, content));
146
141
  }
147
- if (content.startsWith('🔧 ')) {
148
- if (isPendingToolProgressForLark(content)) {
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 (content.startsWith('⏳ ') || content.startsWith('📤 ')) {
148
+ if (bridgeKind === 'status') {
154
149
  return renderProgressMessage(progressKey('status', event), content);
155
150
  }
156
151
  return {
@@ -186,7 +181,7 @@ 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;
@@ -229,21 +224,21 @@ function progressKey(prefix, event) {
229
224
  return `${prefix}:${content.slice(0, 120)}`;
230
225
  }
231
226
  function isThoughtBridgeEvent(event) {
232
- if (event.type !== 'preview_start' && event.type !== 'update_message' && event.type !== 'reply') {
233
- return false;
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 String(event.content || '').trim().startsWith('💭 ');
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
- const normalized = String(content || '').trim();
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) {