@kafca/agentdock 0.1.23 → 0.1.24

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 +1 -0
  2. package/dist/renderer/assets/{Badge-C2K5apfV.js → Badge-CXcUMqb-.js} +1 -1
  3. package/dist/renderer/assets/{Chat-DI2YeVKb.js → Chat-BRxdPzi-.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-DMAr15Oi.js → ChatMarkdown-sWGBIfIb.js} +1 -1
  5. package/dist/renderer/assets/{Config-7rP3EuUf.js → Config-DqxuxIGg.js} +1 -1
  6. package/dist/renderer/assets/{CronList-81QslYE8.js → CronList-OgHxbti5.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-Db2T7rSZ.js → Dashboard-DmklXiH1.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-BBJ3cTvB.js → EmptyState-BvDmi8dq.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-Dz_6flG3.js → KnowledgeDetail-Bj59Hdpo.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeHome-DIh046mN.js → KnowledgeHome-CigQ-nSo.js} +1 -1
  11. package/dist/renderer/assets/{Logs-BXBtHJLf.js → Logs-Bfc7gBpY.js} +1 -1
  12. package/dist/renderer/assets/{Modal-BB2Av_Xs.js → Modal-CfmiFknz.js} +1 -1
  13. package/dist/renderer/assets/{Page-DPSdvMgU.js → Page-BxtLEw_b.js} +1 -1
  14. package/dist/renderer/assets/{ProjectDetail-C6EWGvGg.js → ProjectDetail-CcUphxGm.js} +1 -1
  15. package/dist/renderer/assets/{ProjectList-BBo5ygnS.js → ProjectList-CVkvDIfy.js} +1 -1
  16. package/dist/renderer/assets/{Select-CNX-Tx_O.js → Select-DgQMZ0MD.js} +1 -1
  17. package/dist/renderer/assets/{SessionChat-BIn46jqZ.js → SessionChat-CRmGKfBx.js} +1 -1
  18. package/dist/renderer/assets/{SessionList-CZt6f1Rd.js → SessionList-D0E8XbYw.js} +1 -1
  19. package/dist/renderer/assets/ThreadChat-DUgqZSBB.js +23 -0
  20. package/dist/renderer/assets/{Workspace-C_4tdpjX.js → Workspace-Q4g4rlRT.js} +1 -1
  21. package/dist/renderer/assets/{arrow-left-cHr5wkpq.js → arrow-left-BySN_zhF.js} +1 -1
  22. package/dist/renderer/assets/{book-open-4nx5C8T-.js → book-open-BGlrnrTc.js} +1 -1
  23. package/dist/renderer/assets/{check-exTS-R-x.js → check-BiJogEa5.js} +1 -1
  24. package/dist/renderer/assets/{chevron-down-DSvxXQa0.js → chevron-down-D2fHRJv6.js} +1 -1
  25. package/dist/renderer/assets/{circle-DyvEovdw.js → circle-CIcjixmI.js} +1 -1
  26. package/dist/renderer/assets/desktop-DAXY9Bxv.js +4 -0
  27. package/dist/renderer/assets/{desktop-B-KVT10D.js → desktop-DyA_NYew.js} +1 -1
  28. package/dist/renderer/assets/{funnel-DA9qVdTy.js → funnel-BUzwb-id.js} +1 -1
  29. package/dist/renderer/assets/{heart-DuEV0ZRJ.js → heart-DZEQ1z_V.js} +1 -1
  30. package/dist/renderer/assets/{index-Biz-aexG.js → index-CB_KyLOr.js} +2 -2
  31. package/dist/renderer/assets/{index-BgflqkCN.js → index-DgsIo4vJ.js} +1 -1
  32. package/dist/renderer/assets/{pencil-DBDxeNmO.js → pencil-DGL4agcl.js} +1 -1
  33. package/dist/renderer/assets/{play-IzlP83_7.js → play-DzasCLM4.js} +1 -1
  34. package/dist/renderer/assets/{plug-CuT-qFnW.js → plug-dubNMe_m.js} +1 -1
  35. package/dist/renderer/assets/{plus-DEqgkn63.js → plus-DN4kML4k.js} +1 -1
  36. package/dist/renderer/assets/projects-migPtfRe.js +1 -0
  37. package/dist/renderer/assets/{save-D4sRo07y.js → save-CNIc9tjl.js} +1 -1
  38. package/dist/renderer/assets/{search-DbPZLuV8.js → search-C0EGbF3I.js} +1 -1
  39. package/dist/renderer/assets/{server-D1jUQmwN.js → server-BMxQ4zmc.js} +1 -1
  40. package/dist/renderer/assets/{session-utils-Aife8ep2.js → session-utils-Bf5O-iMi.js} +1 -1
  41. package/dist/renderer/assets/{sessions-DaH3H7gC.js → sessions-BMSs6rEI.js} +1 -1
  42. package/dist/renderer/assets/{shield-check-BGwUFWNr.js → shield-check-i3Tf1TUz.js} +1 -1
  43. package/dist/renderer/assets/{trash-2-CIFBioBa.js → trash-2-SKrcp18f.js} +1 -1
  44. package/dist/renderer/assets/{user-CnKcvHdo.js → user-cNUZddjZ.js} +1 -1
  45. package/dist/renderer/assets/{wifi-off-ClO53WGa.js → wifi-off-Dyb84ANR.js} +1 -1
  46. package/dist/renderer/index.html +1 -1
  47. package/dist-electron/electron/channel-content.test.js +29 -0
  48. package/dist-electron/electron/lac-cli.test.js +49 -4
  49. package/dist-electron/electron/local-core-acp-content.test.js +39 -0
  50. package/dist-electron/electron/local-core-contracts.test.js +67 -0
  51. package/dist-electron/electron/local-core-refactor.test.js +123 -0
  52. package/dist-electron/electron/workspace-task-store.test.js +124 -0
  53. package/dist-electron/packages/contracts/src/local-core.js +110 -0
  54. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-content.js +25 -4
  55. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-response-processor.js +3 -2
  56. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-store.js +44 -28
  57. package/dist-electron/services/local-ai-core/src/acp/workspace-acp-permissions.js +5 -48
  58. package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +68 -25
  59. package/dist-electron/services/local-ai-core/src/channel/shared/content.js +23 -0
  60. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +23 -28
  61. package/dist-electron/services/local-ai-core/src/cli/lac.js +19 -16
  62. package/dist-electron/services/local-ai-core/src/scheduler/job-id.js +15 -0
  63. package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +30 -5
  64. package/dist-electron/shared/desktop.js +27 -5
  65. package/dist-electron/src/pages/Threads/thread-chat-model.js +57 -0
  66. package/dist-electron/src/pages/Threads/thread-chat-permission.test.js +70 -0
  67. package/package.json +1 -1
  68. package/dist/renderer/assets/ThreadChat-BotD0gKQ.js +0 -23
  69. package/dist/renderer/assets/desktop-CVibRyAw.js +0 -4
  70. package/dist/renderer/assets/projects-DDtDN1rc.js +0 -1
@@ -39,6 +39,7 @@ const node_crypto_1 = require("node:crypto");
39
39
  const node_fs_1 = require("node:fs");
40
40
  const node_path_1 = require("node:path");
41
41
  const desktop_js_1 = require("../../../../../shared/desktop.js");
42
+ const content_js_1 = require("../shared/content.js");
42
43
  const file_utils_js_1 = require("../shared/file-utils.js");
43
44
  const cards_js_1 = require("./cards.js");
44
45
  const config_js_1 = require("./config.js");
@@ -650,7 +651,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
650
651
  }
651
652
  this.options.store.clearPlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId);
652
653
  const wrappedText = (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text);
653
- await router.sendThreadMessage(threadId, this.createThreadMessageInput(wrappedText, input.contentParts));
654
+ await router.sendThreadMessage(threadId, (0, content_js_1.createChannelThreadMessageInput)(wrappedText, input.contentParts));
654
655
  return { paired: true, threadId };
655
656
  }
656
657
  close() {
@@ -857,15 +858,24 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
857
858
  }
858
859
  }
859
860
  }
861
+ if (messageType === 'file') {
862
+ const fileKey = String(parsedContent.file_key || '').trim();
863
+ if (fileKey) {
864
+ try {
865
+ contentParts.push(await this.downloadMessageFile(workspaceId, String(message.message_id || ''), fileKey, String(parsedContent.file_name || parsedContent.name || '').trim(), instanceId));
866
+ }
867
+ catch (error) {
868
+ const errorText = `[File download failed: ${error instanceof Error ? error.message : String(error)}]`;
869
+ contentParts.push({ type: 'text', text: errorText });
870
+ this.options.log?.(`localcore-lark file download failed for ${workspaceId}: message=${String(message.message_id || '')} fileKey=${fileKey} error=${error instanceof Error ? error.message : String(error)}`);
871
+ }
872
+ }
873
+ }
860
874
  if (contentParts.length === 0) {
861
875
  this.options.log?.(`localcore-lark ignored unsupported message for ${workspaceId}: type=${String(message.message_type || 'unknown')} contentKeys=${JSON.stringify(Object.keys(parsedContent))}`);
862
876
  return;
863
877
  }
864
- const displayText = text || (contentParts.some((part) => part.type === 'image') ? '[Image]' : contentParts
865
- .filter((part) => part.type === 'text')
866
- .map((part) => part.text)
867
- .join('\n\n')
868
- .trim());
878
+ const displayText = text || this.summarizeInboundContentParts(contentParts);
869
879
  const platformUserId = String(sender.sender_id?.user_id || sender.sender_id?.open_id || '').trim();
870
880
  const chatId = String(message.chat_id || platformUserId).trim();
871
881
  if (!platformUserId || !chatId) {
@@ -887,25 +897,6 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
887
897
  contentParts,
888
898
  });
889
899
  }
890
- createWrappedContentParts(wrappedText, parts) {
891
- const nonTextParts = Array.isArray(parts)
892
- ? parts.filter((part) => part.type !== 'text')
893
- : [];
894
- return [
895
- { type: 'text', text: wrappedText },
896
- ...nonTextParts,
897
- ];
898
- }
899
- createThreadMessageInput(wrappedText, parts) {
900
- const hasNonTextPart = Array.isArray(parts) && parts.some((part) => part.type !== 'text');
901
- if (!hasNonTextPart) {
902
- return wrappedText;
903
- }
904
- return {
905
- displayText: wrappedText,
906
- contentParts: this.createWrappedContentParts(wrappedText, parts),
907
- };
908
- }
909
900
  async downloadMessageImage(workspaceId, messageId, imageKey, instanceId) {
910
901
  const state = this.resolveRuntimeState(workspaceId, instanceId).state;
911
902
  if (!state?.client) {
@@ -935,6 +926,58 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
935
926
  fileName: `${imageKey}.${this.sniffImageExtension(buffer)}`,
936
927
  };
937
928
  }
929
+ async downloadMessageFile(workspaceId, messageId, fileKey, fileName, instanceId) {
930
+ const state = this.resolveRuntimeState(workspaceId, instanceId).state;
931
+ if (!state?.client) {
932
+ throw new Error('Lark client is not connected');
933
+ }
934
+ if (!messageId) {
935
+ throw new Error('Lark file message is missing message_id');
936
+ }
937
+ const resource = await state.client.im.messageResource.get({
938
+ path: {
939
+ message_id: messageId,
940
+ file_key: fileKey,
941
+ },
942
+ params: {
943
+ type: 'file',
944
+ },
945
+ });
946
+ const buffer = await this.readLarkResourceBuffer(resource);
947
+ if (buffer.length === 0) {
948
+ throw new Error('Lark file resource is empty');
949
+ }
950
+ return {
951
+ type: 'file',
952
+ data: buffer.toString('base64'),
953
+ mimeType: this.extractHeaderMimeType(resource?.headers) || 'application/octet-stream',
954
+ fileName: fileName || fileKey,
955
+ size: buffer.length,
956
+ };
957
+ }
958
+ summarizeInboundContentParts(parts) {
959
+ const text = parts
960
+ .filter((part) => part.type === 'text')
961
+ .map((part) => part.text)
962
+ .filter(Boolean)
963
+ .join('\n\n')
964
+ .trim();
965
+ if (text)
966
+ return text;
967
+ const attachmentSummary = parts
968
+ .map((part) => {
969
+ if (part.type === 'image') {
970
+ return '[Image]';
971
+ }
972
+ if (part.type === 'file') {
973
+ return part.fileName ? `[File: ${part.fileName}]` : '[File]';
974
+ }
975
+ return '';
976
+ })
977
+ .filter(Boolean)
978
+ .join('\n');
979
+ return attachmentSummary;
980
+ }
938
981
  async readLarkResourceBuffer(resource) {
939
982
  const readable = resource?.getReadableStream?.();
940
983
  if (!readable || typeof readable.on !== 'function') {
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createWrappedChannelContentParts = createWrappedChannelContentParts;
4
+ exports.createChannelThreadMessageInput = createChannelThreadMessageInput;
5
+ function createWrappedChannelContentParts(wrappedText, parts) {
6
+ const nonTextParts = Array.isArray(parts)
7
+ ? parts.filter((part) => part.type !== 'text')
8
+ : [];
9
+ return [
10
+ { type: 'text', text: wrappedText },
11
+ ...nonTextParts,
12
+ ];
13
+ }
14
+ function createChannelThreadMessageInput(wrappedText, parts) {
15
+ const hasNonTextPart = Array.isArray(parts) && parts.some((part) => part.type !== 'text');
16
+ if (!hasNonTextPart) {
17
+ return wrappedText;
18
+ }
19
+ return {
20
+ displayText: wrappedText,
21
+ contentParts: createWrappedChannelContentParts(wrappedText, parts),
22
+ };
23
+ }
@@ -4,12 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.LocalCoreWeixinGateway = void 0;
7
+ exports.createWeixinAttachmentContentPart = createWeixinAttachmentContentPart;
7
8
  const node_crypto_1 = __importDefault(require("node:crypto"));
8
9
  const node_fs_1 = __importDefault(require("node:fs"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
10
11
  const node_events_1 = require("node:events");
11
12
  const node_crypto_2 = require("node:crypto");
12
13
  const desktop_js_1 = require("../../../../../shared/desktop.js");
14
+ const content_js_1 = require("../shared/content.js");
13
15
  const file_utils_js_1 = require("../shared/file-utils.js");
14
16
  const config_js_1 = require("./config.js");
15
17
  const transport_js_1 = require("./transport.js");
@@ -97,6 +99,23 @@ function splitTextByUtf8Bytes(text, maxBytes) {
97
99
  pushCurrent();
98
100
  return chunks;
99
101
  }
102
+ function createWeixinAttachmentContentPart(att) {
103
+ if (att.kind === 'image') {
104
+ if (!att.data)
105
+ return null;
106
+ return {
107
+ type: 'image',
108
+ data: att.data,
109
+ mimeType: att.mimeType,
110
+ fileName: att.name,
111
+ };
112
+ }
113
+ return {
114
+ type: 'file',
115
+ path: att.path,
116
+ fileName: att.name,
117
+ };
118
+ }
100
119
  function truncateTextByUtf8Bytes(text, maxBytes) {
101
120
  const normalized = String(text || '').replace(/\r\n/g, '\n').trim();
102
121
  if (utf8ByteLength(normalized) <= maxBytes)
@@ -658,7 +677,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
658
677
  }
659
678
  this.options.store.updatePlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId, input.contextToken || input.messageId, platformKey);
660
679
  const wrappedText = (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text);
661
- await router.sendThreadMessage(threadId, this.createThreadMessageInput(wrappedText, input.contentParts));
680
+ await router.sendThreadMessage(threadId, (0, content_js_1.createChannelThreadMessageInput)(wrappedText, input.contentParts));
662
681
  return { paired: true, threadId };
663
682
  }
664
683
  // ==================== Private: Bindings ====================
@@ -825,14 +844,9 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
825
844
  if (att) {
826
845
  attachmentText += attachmentText ? '\n' : '';
827
846
  attachmentText += att.kind === 'image' ? `[Image: ${att.path}]` : `[File "${att.name}": ${att.path}]`;
828
- if (att.kind === 'image' && att.data) {
829
- attachmentParts.push({
830
- type: 'image',
831
- data: att.data,
832
- mimeType: att.mimeType,
833
- fileName: att.name,
834
- });
835
- }
847
+ const part = createWeixinAttachmentContentPart(att);
848
+ if (part)
849
+ attachmentParts.push(part);
836
850
  }
837
851
  }
838
852
  catch (dlErr) {
@@ -1017,25 +1031,6 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
1017
1031
  mimeType: kind === 'image' ? this.mimeTypeForImageExt(ext) : undefined,
1018
1032
  };
1019
1033
  }
1020
- createWrappedContentParts(wrappedText, parts) {
1021
- const nonTextParts = Array.isArray(parts)
1022
- ? parts.filter((part) => part.type !== 'text')
1023
- : [];
1024
- return [
1025
- { type: 'text', text: wrappedText },
1026
- ...nonTextParts,
1027
- ];
1028
- }
1029
- createThreadMessageInput(wrappedText, parts) {
1030
- const hasNonTextPart = Array.isArray(parts) && parts.some((part) => part.type !== 'text');
1031
- if (!hasNonTextPart) {
1032
- return wrappedText;
1033
- }
1034
- return {
1035
- displayText: wrappedText,
1036
- contentParts: this.createWrappedContentParts(wrappedText, parts),
1037
- };
1038
- }
1039
1034
  mimeTypeForImageExt(ext) {
1040
1035
  if (ext === '.jpg')
1041
1036
  return 'image/jpeg';
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runCli = runCli;
7
7
  const node_process_1 = __importDefault(require("node:process"));
8
+ const index_js_1 = require("../../../../packages/contracts/src/index.js");
9
+ const job_id_js_1 = require("../scheduler/job-id.js");
8
10
  const DEFAULT_BASE_URL = 'http://127.0.0.1:9831/api/local/v1';
9
11
  async function runCli(argv, env = node_process_1.default.env, io = node_process_1.default) {
10
12
  try {
@@ -51,7 +53,8 @@ async function runCli(argv, env = node_process_1.default.env, io = node_process_
51
53
  async function handleChannelSendFile(flags, env, io, json) {
52
54
  const context = resolveContext(flags, env);
53
55
  const filePath = getRequiredFlag(flags, 'path');
54
- const platform = getFlag(flags, 'platform') || context.platform;
56
+ const rawPlatform = getFlag(flags, 'platform') || context.platform;
57
+ const platform = rawPlatform ? (0, index_js_1.normalizeChannelPlatform)(rawPlatform) : '';
55
58
  if (!platform) {
56
59
  throw new Error('channel send-file requires a platform. Set LOCAL_AI_PLATFORM or pass --platform.');
57
60
  }
@@ -113,8 +116,8 @@ async function handleAdd(flags, env, io, json) {
113
116
  description,
114
117
  enabled: true,
115
118
  });
116
- print(json, io.stdout, job, [
117
- `Created scheduler job ${job.id}`,
119
+ print(json, io.stdout, presentJob(job), [
120
+ `Created scheduler job ${(0, job_id_js_1.toPublicScheduledJobId)(job.id)}`,
118
121
  `Schedule: ${job.cronExpr || ''}`,
119
122
  `Execution mode: ${job.executionMode}`,
120
123
  `Description: ${job.description}`,
@@ -132,7 +135,7 @@ async function handleList(flags, env, io, json) {
132
135
  const jobs = threadId
133
136
  ? response.jobs.filter((job) => job.route.threadId === threadId)
134
137
  : response.jobs;
135
- print(json, io.stdout, { jobs }, jobs.length === 0 ? 'No scheduler jobs.' : jobs.map(formatJobLine).join('\n'));
138
+ print(json, io.stdout, { jobs: jobs.map(presentJob) }, jobs.length === 0 ? 'No scheduler jobs.' : jobs.map(formatJobLine).join('\n'));
136
139
  return 0;
137
140
  }
138
141
  async function handleInfo(jobId, flags, env, io, json) {
@@ -142,7 +145,7 @@ async function handleInfo(jobId, flags, env, io, json) {
142
145
  const context = resolveContext(flags, env);
143
146
  const job = await request(context.baseUrl, 'GET', `/scheduler/jobs/${encodeURIComponent(jobId)}`);
144
147
  const runs = await request(context.baseUrl, 'GET', `/scheduler/jobs/${encodeURIComponent(jobId)}/runs`);
145
- print(json, io.stdout, { job, runs: runs.runs }, formatJobDetails(job, runs.runs[0]));
148
+ print(json, io.stdout, { job: presentJob(job), runs: runs.runs }, formatJobDetails(job, runs.runs[0]));
146
149
  return 0;
147
150
  }
148
151
  async function handleEdit(jobId, flags, env, io, json) {
@@ -168,14 +171,14 @@ async function handleEdit(jobId, flags, env, io, json) {
168
171
  input.enabled = enabled;
169
172
  }
170
173
  if (executionMode) {
171
- input.executionMode = parseExecutionMode(executionMode);
174
+ input.executionMode = (0, index_js_1.normalizeScheduledJobExecutionMode)(executionMode);
172
175
  }
173
176
  if (Object.keys(input).length === 0) {
174
177
  throw new Error('scheduler edit requires at least one editable field.');
175
178
  }
176
179
  const context = resolveContext(flags, env);
177
180
  const job = await request(context.baseUrl, 'PATCH', `/scheduler/jobs/${encodeURIComponent(jobId)}`, input);
178
- print(json, io.stdout, job, `Updated scheduler job ${job.id}`);
181
+ print(json, io.stdout, presentJob(job), `Updated scheduler job ${(0, job_id_js_1.toPublicScheduledJobId)(job.id)}`);
179
182
  return 0;
180
183
  }
181
184
  async function handleDelete(jobId, flags, env, io, json) {
@@ -284,13 +287,7 @@ function normalizeMaybeBooleanFlag(value) {
284
287
  return value === 'true' ? '' : value;
285
288
  }
286
289
  function getExecutionMode(flags) {
287
- return parseExecutionMode(getFlag(flags, 'execution-mode') || 'same-thread');
288
- }
289
- function parseExecutionMode(value) {
290
- if (value === 'same-thread' || value === 'side-thread') {
291
- return value;
292
- }
293
- throw new Error('Flag --execution-mode must be same-thread or side-thread');
290
+ return (0, index_js_1.normalizeScheduledJobExecutionMode)(getFlag(flags, 'execution-mode'));
294
291
  }
295
292
  function print(asJson, output, payload, text) {
296
293
  output.write(asJson ? `${JSON.stringify(payload, null, 2)}\n` : `${text}\n`);
@@ -309,11 +306,11 @@ function printUsage(output) {
309
306
  }
310
307
  function formatJobLine(job) {
311
308
  const schedule = job.triggerType === 'cron' ? job.cronExpr || '' : job.runAt || '';
312
- return `${job.id} | ${job.enabled ? 'enabled' : 'disabled'} | ${job.executionMode} | ${schedule} | ${job.description}`;
309
+ return `${(0, job_id_js_1.toPublicScheduledJobId)(job.id)} | ${job.enabled ? 'enabled' : 'disabled'} | ${job.executionMode} | ${schedule} | ${job.description}`;
313
310
  }
314
311
  function formatJobDetails(job, latestRun) {
315
312
  return [
316
- `Job: ${job.id}`,
313
+ `Job: ${(0, job_id_js_1.toPublicScheduledJobId)(job.id)}`,
317
314
  `Workspace: ${job.workspaceId}`,
318
315
  `Platform: ${job.platform}`,
319
316
  `Thread: ${job.route.threadId || ''}`,
@@ -325,6 +322,12 @@ function formatJobDetails(job, latestRun) {
325
322
  latestRun ? `Latest run: ${latestRun.status} @ ${latestRun.triggeredAt}` : 'Latest run: none',
326
323
  ].join('\n');
327
324
  }
325
+ function presentJob(job) {
326
+ return {
327
+ ...job,
328
+ id: (0, job_id_js_1.toPublicScheduledJobId)(job.id),
329
+ };
330
+ }
328
331
  function formatError(error) {
329
332
  return error instanceof Error ? error.message : String(error);
330
333
  }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createScheduledJobId = createScheduledJobId;
4
+ exports.toPublicScheduledJobId = toPublicScheduledJobId;
5
+ const node_crypto_1 = require("node:crypto");
6
+ function createScheduledJobId() {
7
+ return (0, node_crypto_1.randomUUID)().split('-')[0] || (0, node_crypto_1.randomUUID)();
8
+ }
9
+ function toPublicScheduledJobId(jobId) {
10
+ const normalized = jobId.startsWith('job:') ? jobId.slice(4) : jobId;
11
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(normalized)) {
12
+ return normalized.split('-')[0] || normalized;
13
+ }
14
+ return normalized;
15
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SchedulerService = void 0;
4
4
  const node_events_1 = require("node:events");
5
+ const job_id_js_1 = require("./job-id.js");
5
6
  const scheduler_run_lifecycle_js_1 = require("./scheduler-run-lifecycle.js");
6
7
  class SchedulerService extends node_events_1.EventEmitter {
7
8
  options;
@@ -34,7 +35,8 @@ class SchedulerService extends node_events_1.EventEmitter {
34
35
  return this.options.store.listScheduledJobs(workspaceId);
35
36
  }
36
37
  getJob(jobId) {
37
- return this.options.store.getScheduledJob(jobId);
38
+ const resolvedJobId = this.resolveJobId(jobId);
39
+ return resolvedJobId ? this.options.store.getScheduledJob(resolvedJobId) : undefined;
38
40
  }
39
41
  createJob(input) {
40
42
  const job = this.options.store.createScheduledJob(input);
@@ -43,24 +45,47 @@ class SchedulerService extends node_events_1.EventEmitter {
43
45
  return job;
44
46
  }
45
47
  updateJob(jobId, input) {
46
- const job = this.options.store.updateScheduledJob(jobId, input);
48
+ const job = this.options.store.updateScheduledJob(this.resolveRequiredJobId(jobId), input);
47
49
  this.emit('job', job);
48
50
  this.options.eventBus.emit({ type: 'scheduler.job.updated', payload: job });
49
51
  return job;
50
52
  }
51
53
  deleteJob(jobId) {
52
- return this.options.store.deleteScheduledJob(jobId);
54
+ return this.options.store.deleteScheduledJob(this.resolveRequiredJobId(jobId));
53
55
  }
54
56
  listJobRuns(jobId) {
55
- return this.options.store.listScheduledJobRuns(jobId);
57
+ return this.options.store.listScheduledJobRuns(this.resolveRequiredJobId(jobId));
56
58
  }
57
59
  async runJobNow(jobId) {
58
- const job = this.options.store.getScheduledJob(jobId);
60
+ const resolvedJobId = this.resolveRequiredJobId(jobId);
61
+ const job = this.options.store.getScheduledJob(resolvedJobId);
59
62
  if (!job) {
60
63
  throw new Error(`Scheduled job not found: ${jobId}`);
61
64
  }
62
65
  return this.executeJob(job, new Date().toISOString(), true);
63
66
  }
67
+ resolveJobId(jobId) {
68
+ if (this.options.store.getScheduledJob(jobId)) {
69
+ return jobId;
70
+ }
71
+ const matches = this.options.store
72
+ .listScheduledJobs()
73
+ .filter((job) => (0, job_id_js_1.toPublicScheduledJobId)(job.id) === jobId);
74
+ if (matches.length === 0) {
75
+ return '';
76
+ }
77
+ if (matches.length > 1) {
78
+ throw new Error(`Scheduled job id is ambiguous: ${jobId}`);
79
+ }
80
+ return matches[0].id;
81
+ }
82
+ resolveRequiredJobId(jobId) {
83
+ const resolvedJobId = this.resolveJobId(jobId);
84
+ if (!resolvedJobId) {
85
+ throw new Error(`Scheduled job not found: ${jobId}`);
86
+ }
87
+ return resolvedJobId;
88
+ }
64
89
  async tick() {
65
90
  if (this.pollInFlight) {
66
91
  return;
@@ -62,13 +62,14 @@ exports.SCHEDULER_PROTOCOL_INSTRUCTION = [
62
62
  'lac scheduler add --cron "<5-field cron>" --message "<exact message to send>" --desc "<short label>" [--execution-mode same-thread|side-thread]',
63
63
  'lac scheduler list',
64
64
  'lac scheduler list --thread',
65
- 'lac scheduler info <job-id>',
66
- 'lac scheduler edit <job-id> [--cron "<5-field cron>"] [--message "<exact message>"] [--desc "<short label>"] [--enabled true|false] [--execution-mode same-thread|side-thread]',
67
- 'lac scheduler del <job-id>',
68
- 'lac scheduler run <job-id>',
65
+ 'lac scheduler info <short-job-id>',
66
+ 'lac scheduler edit <short-job-id> [--cron "<5-field cron>"] [--message "<exact message>"] [--desc "<short label>"] [--enabled true|false] [--execution-mode same-thread|side-thread]',
67
+ 'lac scheduler del <short-job-id>',
68
+ 'lac scheduler run <short-job-id>',
69
69
  'Environment variables LOCAL_AI_WORKSPACE_ID, LOCAL_AI_THREAD_ID, LOCAL_AI_PLATFORM, LOCAL_AI_CHAT_ID, and LOCAL_AI_PLATFORM_USER_ID are already set when available.',
70
70
  'Prefer relying on those variables instead of inventing your own route or creating session-only cron jobs.',
71
71
  'By default, `lac scheduler list` shows all scheduled tasks in the current workspace. Use `lac scheduler list --thread` to show only the current conversation thread.',
72
+ 'Use the short job id shown by `lac scheduler list`; do not add a `job:` prefix or expand it to a full UUID.',
72
73
  'Use `--execution-mode same-thread` to reuse the current thread, or `--execution-mode side-thread` to run in a dedicated scheduled thread.',
73
74
  'Only use the scheduler CLI when the user explicitly asks for scheduled automation.',
74
75
  '[/Scheduler Tools]',
@@ -169,7 +170,28 @@ function normalizePermissionResponse(input) {
169
170
  if (!input) {
170
171
  return null;
171
172
  }
172
- return PERMISSION_RESPONSE_MAP[String(input).trim().toLowerCase()] || null;
173
+ const normalized = String(input).trim().toLowerCase();
174
+ const mapped = PERMISSION_RESPONSE_MAP[normalized];
175
+ if (mapped) {
176
+ return mapped;
177
+ }
178
+ if (normalized.includes('allow_all') ||
179
+ normalized.includes('allow-always') ||
180
+ normalized.includes('allow_always') ||
181
+ normalized.includes('allow always') ||
182
+ normalized.includes('always allow') ||
183
+ normalized.includes('allow all') ||
184
+ normalized.includes('始终允许') ||
185
+ normalized.includes('永久允许')) {
186
+ return 'allow all';
187
+ }
188
+ if (normalized.startsWith('reject') || normalized.startsWith('deny')) {
189
+ return 'deny';
190
+ }
191
+ if (normalized.startsWith('allow')) {
192
+ return 'allow';
193
+ }
194
+ return null;
173
195
  }
174
196
  function isPermissionButtonOption(option) {
175
197
  return Boolean(normalizePermissionResponse(option?.data));
@@ -5,6 +5,9 @@ exports.isInternalProgressMessage = isInternalProgressMessage;
5
5
  exports.isAwaitingInputMessage = isAwaitingInputMessage;
6
6
  exports.findStreamingPreviewMessage = findStreamingPreviewMessage;
7
7
  exports.shouldReplacePreviewWithReply = shouldReplacePreviewWithReply;
8
+ exports.advancePreviewContent = advancePreviewContent;
9
+ exports.settlePreviewMessages = settlePreviewMessages;
10
+ exports.finalizeTurnMessageKinds = finalizeTurnMessageKinds;
8
11
  exports.extractVisibleMessageContent = extractVisibleMessageContent;
9
12
  exports.sessionMatchesDesktop = sessionMatchesDesktop;
10
13
  exports.toMessages = toMessages;
@@ -65,6 +68,60 @@ function shouldReplacePreviewWithReply(message, replyContent, replyCtx) {
65
68
  const content = message.streamTargetContent ?? message.content;
66
69
  return Boolean(replyContent && content === replyContent && !isInternalProgressMessage(content));
67
70
  }
71
+ function advancePreviewContent(content, target) {
72
+ if (!target) {
73
+ return '';
74
+ }
75
+ if (!content || !target.startsWith(content)) {
76
+ return target.slice(0, Math.min(target.length, Math.max(1, Math.ceil(target.length / 24))));
77
+ }
78
+ if (content === target) {
79
+ return target;
80
+ }
81
+ const remaining = target.length - content.length;
82
+ const step = remaining > 160 ? 14 : remaining > 80 ? 8 : remaining > 24 ? 4 : 2;
83
+ return target.slice(0, Math.min(target.length, content.length + step));
84
+ }
85
+ function settlePreviewMessages(messages, turnKey) {
86
+ let changed = false;
87
+ const next = messages.map((message) => {
88
+ if (!message.preview) {
89
+ return message;
90
+ }
91
+ if (turnKey && message.turnKey !== turnKey) {
92
+ return message;
93
+ }
94
+ changed = true;
95
+ return {
96
+ ...message,
97
+ content: message.streamTargetContent ?? message.content,
98
+ streamTargetContent: undefined,
99
+ preview: false,
100
+ previewPlainText: false,
101
+ };
102
+ });
103
+ return changed ? next : messages;
104
+ }
105
+ function finalizeTurnMessageKinds(messages, turnKey) {
106
+ if (!turnKey) {
107
+ return messages;
108
+ }
109
+ const candidates = messages.filter((message) => message.turnKey === turnKey && !message.preview);
110
+ if (candidates.length === 0) {
111
+ return messages;
112
+ }
113
+ const finalMessage = [...candidates].reverse().find((message) => !isInternalProgressMessage(message.content));
114
+ const finalId = finalMessage?.id;
115
+ return messages.map((message) => {
116
+ if (message.turnKey !== turnKey || message.preview) {
117
+ return message;
118
+ }
119
+ return {
120
+ ...message,
121
+ kind: finalId && message.id === finalId ? 'final' : 'progress',
122
+ };
123
+ });
124
+ }
68
125
  function extractVisibleMessageContent(content) {
69
126
  if (!content) {
70
127
  return '';
@@ -157,3 +157,73 @@ function createMessage(overrides) {
157
157
  strict_1.default.equal((0, thread_chat_model_1.shouldReplacePreviewWithReply)(thoughtPreview, 'Hi! How can I help you today?', 'run-1'), false);
158
158
  strict_1.default.equal((0, thread_chat_model_1.shouldReplacePreviewWithReply)(answerPreview, 'Hi! How can I help you today?', 'run-1'), true);
159
159
  });
160
+ (0, node_test_1.default)('finalizeTurnMessageKinds marks only the last non-progress turn message as final', () => {
161
+ const messages = [
162
+ {
163
+ id: 'thought',
164
+ role: 'assistant',
165
+ content: '💭 checking',
166
+ kind: 'final',
167
+ order: 1,
168
+ turnKey: 'run-1',
169
+ },
170
+ {
171
+ id: 'tool',
172
+ role: 'assistant',
173
+ content: '🔧 Read file',
174
+ kind: 'final',
175
+ order: 2,
176
+ turnKey: 'run-1',
177
+ },
178
+ {
179
+ id: 'answer',
180
+ role: 'assistant',
181
+ content: 'Done',
182
+ kind: 'progress',
183
+ order: 3,
184
+ turnKey: 'run-1',
185
+ },
186
+ ];
187
+ const finalized = (0, thread_chat_model_1.finalizeTurnMessageKinds)(messages, 'run-1');
188
+ strict_1.default.deepEqual(finalized.map((message) => [message.id, message.kind]), [
189
+ ['thought', 'progress'],
190
+ ['tool', 'progress'],
191
+ ['answer', 'final'],
192
+ ]);
193
+ });
194
+ (0, node_test_1.default)('settlePreviewMessages settles only previews for the requested turn', () => {
195
+ const messages = [
196
+ {
197
+ id: 'preview-1',
198
+ role: 'assistant',
199
+ content: 'Hel',
200
+ streamTargetContent: 'Hello',
201
+ kind: 'progress',
202
+ order: 1,
203
+ turnKey: 'run-1',
204
+ preview: true,
205
+ previewPlainText: true,
206
+ },
207
+ {
208
+ id: 'preview-2',
209
+ role: 'assistant',
210
+ content: 'Wor',
211
+ streamTargetContent: 'World',
212
+ kind: 'progress',
213
+ order: 2,
214
+ turnKey: 'run-2',
215
+ preview: true,
216
+ previewPlainText: true,
217
+ },
218
+ ];
219
+ const settled = (0, thread_chat_model_1.settlePreviewMessages)(messages, 'run-1');
220
+ strict_1.default.equal(settled[0]?.content, 'Hello');
221
+ strict_1.default.equal(settled[0]?.preview, false);
222
+ strict_1.default.equal(settled[1]?.content, 'Wor');
223
+ strict_1.default.equal(settled[1]?.preview, true);
224
+ });
225
+ (0, node_test_1.default)('advancePreviewContent moves monotonically toward the target content', () => {
226
+ strict_1.default.equal((0, thread_chat_model_1.advancePreviewContent)('', 'Hello'), 'H');
227
+ strict_1.default.equal((0, thread_chat_model_1.advancePreviewContent)('Hel', 'Hello'), 'Hello');
228
+ strict_1.default.equal((0, thread_chat_model_1.advancePreviewContent)('Mismatch', 'Hello'), 'H');
229
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kafca/agentdock",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "type": "module",
5
5
  "main": "dist-electron/electron/main.js",
6
6
  "bin": {