cowork-os 0.3.21 → 0.3.25

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 (252) hide show
  1. package/README.md +372 -10
  2. package/connectors/README.md +20 -0
  3. package/connectors/asana-mcp/README.md +24 -0
  4. package/connectors/asana-mcp/dist/index.js +427 -0
  5. package/connectors/asana-mcp/package.json +15 -0
  6. package/connectors/asana-mcp/src/index.ts +553 -0
  7. package/connectors/asana-mcp/tsconfig.json +13 -0
  8. package/connectors/hubspot-mcp/README.md +35 -0
  9. package/connectors/hubspot-mcp/dist/index.js +454 -0
  10. package/connectors/hubspot-mcp/package.json +15 -0
  11. package/connectors/hubspot-mcp/src/index.ts +562 -0
  12. package/connectors/hubspot-mcp/tsconfig.json +13 -0
  13. package/connectors/jira-mcp/README.md +49 -0
  14. package/connectors/jira-mcp/dist/index.js +588 -0
  15. package/connectors/jira-mcp/package.json +15 -0
  16. package/connectors/jira-mcp/src/index.ts +711 -0
  17. package/connectors/jira-mcp/tsconfig.json +13 -0
  18. package/connectors/linear-mcp/README.md +22 -0
  19. package/connectors/linear-mcp/dist/index.js +402 -0
  20. package/connectors/linear-mcp/package.json +15 -0
  21. package/connectors/linear-mcp/src/index.ts +522 -0
  22. package/connectors/linear-mcp/tsconfig.json +13 -0
  23. package/connectors/okta-mcp/README.md +24 -0
  24. package/connectors/okta-mcp/dist/index.js +411 -0
  25. package/connectors/okta-mcp/package.json +15 -0
  26. package/connectors/okta-mcp/src/index.ts +520 -0
  27. package/connectors/okta-mcp/tsconfig.json +13 -0
  28. package/connectors/salesforce-mcp/README.md +47 -0
  29. package/connectors/salesforce-mcp/dist/index.js +584 -0
  30. package/connectors/salesforce-mcp/package.json +15 -0
  31. package/connectors/salesforce-mcp/src/index.ts +722 -0
  32. package/connectors/salesforce-mcp/tsconfig.json +13 -0
  33. package/connectors/servicenow-mcp/README.md +26 -0
  34. package/connectors/servicenow-mcp/dist/index.js +400 -0
  35. package/connectors/servicenow-mcp/package.json +15 -0
  36. package/connectors/servicenow-mcp/src/index.ts +500 -0
  37. package/connectors/servicenow-mcp/tsconfig.json +13 -0
  38. package/connectors/templates/mcp-connector/README.md +31 -0
  39. package/connectors/templates/mcp-connector/package.json +15 -0
  40. package/connectors/templates/mcp-connector/src/index.ts +330 -0
  41. package/connectors/templates/mcp-connector/tsconfig.json +13 -0
  42. package/connectors/zendesk-mcp/README.md +40 -0
  43. package/connectors/zendesk-mcp/dist/index.js +431 -0
  44. package/connectors/zendesk-mcp/package.json +15 -0
  45. package/connectors/zendesk-mcp/src/index.ts +543 -0
  46. package/connectors/zendesk-mcp/tsconfig.json +13 -0
  47. package/dist/electron/electron/agent/custom-skill-loader.js +31 -1
  48. package/dist/electron/electron/agent/daemon.js +189 -13
  49. package/dist/electron/electron/agent/executor.js +895 -78
  50. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  51. package/dist/electron/electron/agent/llm/azure-openai-provider.js +328 -0
  52. package/dist/electron/electron/agent/llm/bedrock-provider.js +49 -9
  53. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  54. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  55. package/dist/electron/electron/agent/llm/index.js +13 -1
  56. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  57. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  58. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  59. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  60. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  61. package/dist/electron/electron/agent/llm/provider-factory.js +350 -4
  62. package/dist/electron/electron/agent/llm/types.js +66 -1
  63. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  64. package/dist/electron/electron/agent/search/provider-factory.js +38 -2
  65. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  66. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  67. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  68. package/dist/electron/electron/agent/tools/file-tools.js +66 -3
  69. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  70. package/dist/electron/electron/agent/tools/grep-tools.js +90 -10
  71. package/dist/electron/electron/agent/tools/image-tools.js +11 -1
  72. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  73. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  74. package/dist/electron/electron/agent/tools/registry.js +548 -10
  75. package/dist/electron/electron/agent/tools/search-tools.js +28 -10
  76. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  77. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  78. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  79. package/dist/electron/electron/agents/agent-dispatch.js +63 -0
  80. package/dist/electron/electron/database/repositories.js +19 -5
  81. package/dist/electron/electron/database/schema.js +8 -0
  82. package/dist/electron/electron/gateway/channels/whatsapp.js +55 -0
  83. package/dist/electron/electron/gateway/index.js +75 -1
  84. package/dist/electron/electron/gateway/router.js +209 -154
  85. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  86. package/dist/electron/electron/ipc/handlers.js +763 -267
  87. package/dist/electron/electron/main.js +63 -0
  88. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  89. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  90. package/dist/electron/electron/memory/MemoryService.js +2 -1
  91. package/dist/electron/electron/preload.js +78 -1
  92. package/dist/electron/electron/settings/appearance-manager.js +18 -1
  93. package/dist/electron/electron/settings/box-manager.js +54 -0
  94. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  95. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  96. package/dist/electron/electron/settings/notion-manager.js +56 -0
  97. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  98. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  99. package/dist/electron/electron/utils/box-api.js +153 -0
  100. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  101. package/dist/electron/electron/utils/env-migration.js +19 -0
  102. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  103. package/dist/electron/electron/utils/notion-api.js +103 -0
  104. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  105. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  106. package/dist/electron/electron/utils/validation.js +98 -3
  107. package/dist/electron/electron/utils/x-cli.js +1 -1
  108. package/dist/electron/shared/channelMessages.js +284 -3
  109. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  110. package/dist/electron/shared/types.js +90 -1
  111. package/package.json +14 -3
  112. package/resources/skills/nano-banana-pro.json +4 -4
  113. package/resources/skills/openai-image-gen.json +3 -3
  114. package/resources/skills/scripts/gen.py +163 -0
  115. package/resources/skills/scripts/generate_image.py +91 -0
  116. package/src/electron/agent/custom-skill-loader.ts +34 -1
  117. package/src/electron/agent/daemon.ts +210 -14
  118. package/src/electron/agent/executor.ts +1124 -85
  119. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  120. package/src/electron/agent/llm/azure-openai-provider.ts +388 -0
  121. package/src/electron/agent/llm/bedrock-provider.ts +62 -9
  122. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  123. package/src/electron/agent/llm/groq-provider.ts +39 -0
  124. package/src/electron/agent/llm/index.ts +6 -0
  125. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  126. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  127. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  128. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  129. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  130. package/src/electron/agent/llm/provider-factory.ts +459 -6
  131. package/src/electron/agent/llm/types.ts +95 -1
  132. package/src/electron/agent/llm/xai-provider.ts +39 -0
  133. package/src/electron/agent/search/provider-factory.ts +43 -2
  134. package/src/electron/agent/tools/box-tools.ts +239 -0
  135. package/src/electron/agent/tools/builtin-settings.ts +36 -0
  136. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  137. package/src/electron/agent/tools/file-tools.ts +66 -3
  138. package/src/electron/agent/tools/gmail-tools.ts +240 -0
  139. package/src/electron/agent/tools/google-calendar-tools.ts +258 -0
  140. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  141. package/src/electron/agent/tools/grep-tools.ts +97 -12
  142. package/src/electron/agent/tools/image-tools.ts +11 -1
  143. package/src/electron/agent/tools/notion-tools.ts +330 -0
  144. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  145. package/src/electron/agent/tools/registry.ts +794 -10
  146. package/src/electron/agent/tools/search-tools.ts +29 -11
  147. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  148. package/src/electron/agent/tools/shell-tools.ts +11 -3
  149. package/src/electron/agent/tools/x-tools.ts +1 -1
  150. package/src/electron/agents/agent-dispatch.ts +79 -0
  151. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  152. package/src/electron/database/repositories.ts +58 -6
  153. package/src/electron/database/schema.ts +8 -0
  154. package/src/electron/gateway/channels/discord.ts +4 -0
  155. package/src/electron/gateway/channels/google-chat.ts +3 -0
  156. package/src/electron/gateway/channels/line.ts +3 -0
  157. package/src/electron/gateway/channels/matrix-client.ts +15 -0
  158. package/src/electron/gateway/channels/matrix.ts +31 -0
  159. package/src/electron/gateway/channels/mattermost.ts +3 -0
  160. package/src/electron/gateway/channels/signal.ts +3 -0
  161. package/src/electron/gateway/channels/slack.ts +9 -4
  162. package/src/electron/gateway/channels/teams.ts +4 -0
  163. package/src/electron/gateway/channels/telegram.ts +2 -0
  164. package/src/electron/gateway/channels/twitch.ts +2 -0
  165. package/src/electron/gateway/channels/types.ts +8 -0
  166. package/src/electron/gateway/channels/whatsapp.ts +66 -0
  167. package/src/electron/gateway/index.ts +95 -2
  168. package/src/electron/gateway/router.ts +231 -161
  169. package/src/electron/gateway/security.ts +21 -9
  170. package/src/electron/ipc/canvas-handlers.ts +10 -0
  171. package/src/electron/ipc/handlers.ts +848 -292
  172. package/src/electron/main.ts +35 -0
  173. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  174. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  175. package/src/electron/memory/MemoryService.ts +7 -1
  176. package/src/electron/preload.ts +200 -5
  177. package/src/electron/settings/appearance-manager.ts +20 -2
  178. package/src/electron/settings/box-manager.ts +58 -0
  179. package/src/electron/settings/dropbox-manager.ts +58 -0
  180. package/src/electron/settings/google-workspace-manager.ts +59 -0
  181. package/src/electron/settings/notion-manager.ts +60 -0
  182. package/src/electron/settings/onedrive-manager.ts +58 -0
  183. package/src/electron/settings/sharepoint-manager.ts +58 -0
  184. package/src/electron/utils/box-api.ts +184 -0
  185. package/src/electron/utils/dropbox-api.ts +171 -0
  186. package/src/electron/utils/env-migration.ts +22 -0
  187. package/src/electron/utils/gmail-api.ts +121 -0
  188. package/src/electron/utils/google-calendar-api.ts +115 -0
  189. package/src/electron/utils/google-workspace-api.ts +228 -0
  190. package/src/electron/utils/google-workspace-auth.ts +109 -0
  191. package/src/electron/utils/google-workspace-oauth.ts +232 -0
  192. package/src/electron/utils/notion-api.ts +126 -0
  193. package/src/electron/utils/onedrive-api.ts +137 -0
  194. package/src/electron/utils/sharepoint-api.ts +132 -0
  195. package/src/electron/utils/validation.ts +128 -1
  196. package/src/electron/utils/x-cli.ts +1 -1
  197. package/src/renderer/App.tsx +119 -8
  198. package/src/renderer/components/ActivityFeedItem.tsx +34 -17
  199. package/src/renderer/components/AgentWorkingStatePanel.tsx +7 -5
  200. package/src/renderer/components/AppearanceSettings.tsx +37 -2
  201. package/src/renderer/components/BlueBubblesSettings.tsx +18 -7
  202. package/src/renderer/components/BoxSettings.tsx +203 -0
  203. package/src/renderer/components/BrowserView.tsx +101 -0
  204. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  205. package/src/renderer/components/CanvasPreview.tsx +68 -1
  206. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  207. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  208. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  209. package/src/renderer/components/ControlPlaneSettings.tsx +2 -0
  210. package/src/renderer/components/DiscordSettings.tsx +18 -7
  211. package/src/renderer/components/DropboxSettings.tsx +202 -0
  212. package/src/renderer/components/EmailSettings.tsx +18 -7
  213. package/src/renderer/components/FileViewer.tsx +21 -13
  214. package/src/renderer/components/GoogleChatSettings.tsx +17 -7
  215. package/src/renderer/components/GoogleWorkspaceSettings.tsx +332 -0
  216. package/src/renderer/components/ImessageSettings.tsx +22 -11
  217. package/src/renderer/components/LineIcons.tsx +376 -0
  218. package/src/renderer/components/LineSettings.tsx +18 -7
  219. package/src/renderer/components/MCPSettings.tsx +56 -0
  220. package/src/renderer/components/MainContent.tsx +740 -76
  221. package/src/renderer/components/MatrixSettings.tsx +18 -7
  222. package/src/renderer/components/MattermostSettings.tsx +18 -7
  223. package/src/renderer/components/NodesSettings.tsx +58 -99
  224. package/src/renderer/components/NotificationPanel.tsx +25 -11
  225. package/src/renderer/components/NotionSettings.tsx +231 -0
  226. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  227. package/src/renderer/components/OnboardingModal.tsx +70 -1
  228. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  229. package/src/renderer/components/RightPanel.tsx +141 -28
  230. package/src/renderer/components/ScheduledTasksSettings.tsx +10 -62
  231. package/src/renderer/components/SearchSettings.tsx +118 -114
  232. package/src/renderer/components/Settings.tsx +1425 -651
  233. package/src/renderer/components/SharePointSettings.tsx +224 -0
  234. package/src/renderer/components/Sidebar.tsx +94 -19
  235. package/src/renderer/components/SignalSettings.tsx +18 -7
  236. package/src/renderer/components/SkillHubBrowser.tsx +144 -185
  237. package/src/renderer/components/SlackSettings.tsx +18 -7
  238. package/src/renderer/components/TaskQuickActions.tsx +11 -6
  239. package/src/renderer/components/TaskTimeline.tsx +58 -26
  240. package/src/renderer/components/TeamsSettings.tsx +18 -7
  241. package/src/renderer/components/TelegramSettings.tsx +18 -7
  242. package/src/renderer/components/ThemeIcon.tsx +16 -0
  243. package/src/renderer/components/TwitchSettings.tsx +18 -7
  244. package/src/renderer/components/VoiceSettings.tsx +30 -74
  245. package/src/renderer/components/WhatsAppSettings.tsx +48 -37
  246. package/src/renderer/components/WorkingStateHistory.tsx +7 -5
  247. package/src/renderer/components/WorkspaceSelector.tsx +42 -13
  248. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  249. package/src/renderer/styles/index.css +2333 -209
  250. package/src/shared/channelMessages.ts +367 -4
  251. package/src/shared/llm-provider-catalog.ts +217 -0
  252. package/src/shared/types.ts +251 -2
@@ -118,6 +118,7 @@ class MessageRouter {
118
118
  agentName: settings.agentName || 'CoWork',
119
119
  userName: settings.relationship?.userName,
120
120
  personality: settings.activePersonality || 'professional',
121
+ persona: settings.activePersona,
121
122
  emojiUsage: settings.responseStyle?.emojiUsage || 'minimal',
122
123
  quirks: settings.quirks || types_2.DEFAULT_QUIRKS,
123
124
  };
@@ -128,6 +129,28 @@ class MessageRouter {
128
129
  }
129
130
  return channelMessages_1.DEFAULT_CHANNEL_CONTEXT;
130
131
  }
132
+ normalizeSimpleChannelMessage(text, context) {
133
+ if (!text)
134
+ return text;
135
+ let normalized = text;
136
+ const signOff = context.quirks?.signOff?.trim();
137
+ if (signOff) {
138
+ const escaped = signOff.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
139
+ const signOffRegex = new RegExp(`(?:\\s|\\n)*${escaped}\\s*$`, 'i');
140
+ const withoutSignOff = normalized.replace(signOffRegex, '').trimEnd();
141
+ if (withoutSignOff.length > 0) {
142
+ normalized = withoutSignOff;
143
+ }
144
+ }
145
+ normalized = normalized.replace(/[ \t]+$/g, '');
146
+ if (normalized.endsWith(':')) {
147
+ normalized = normalized.slice(0, -1).trimEnd();
148
+ }
149
+ return normalized;
150
+ }
151
+ getUiCopy(key, replacements) {
152
+ return (0, channelMessages_1.getChannelUiCopy)(key, this.getMessageContext(), replacements);
153
+ }
131
154
  /**
132
155
  * Get or create the temp workspace for sessions without a workspace
133
156
  */
@@ -507,10 +530,10 @@ class MessageRouter {
507
530
  // Not a pairing code or pairing not required - send appropriate message
508
531
  let responseText;
509
532
  if (securityResult.pairingRequired) {
510
- responseText = this.config.pairingRequiredMessage;
533
+ responseText = this.getUiCopy('pairingRequired');
511
534
  }
512
535
  else {
513
- responseText = this.config.unauthorizedMessage;
536
+ responseText = this.getUiCopy('unauthorized');
514
537
  }
515
538
  try {
516
539
  await adapter.sendMessage({
@@ -549,9 +572,19 @@ class MessageRouter {
549
572
  if (!isNaN(num) && num > 0 && num <= workspaces.length) {
550
573
  const workspace = workspaces[num - 1];
551
574
  this.sessionManager.setSessionWorkspace(sessionId, workspace.id);
575
+ if (workspace.id !== types_1.TEMP_WORKSPACE_ID) {
576
+ try {
577
+ this.workspaceRepo.updateLastUsedAt(workspace.id);
578
+ }
579
+ catch (error) {
580
+ console.warn('Failed to update workspace last used time:', error);
581
+ }
582
+ }
583
+ const selectedText = this.getUiCopy('workspaceSelected', { workspaceName: workspace.name });
584
+ const exampleText = this.getUiCopy('workspaceSelectedExample');
552
585
  await adapter.sendMessage({
553
586
  chatId: message.chatId,
554
- text: `✅ *${workspace.name}* selected!\n\nYou can now send me tasks.\n\nExample: "Create a new React component called Button"`,
587
+ text: `${selectedText}\n\n${exampleText}`,
555
588
  parseMode: 'markdown',
556
589
  });
557
590
  return;
@@ -561,9 +594,19 @@ class MessageRouter {
561
594
  ws.name.toLowerCase().startsWith(text.toLowerCase()));
562
595
  if (matchedWorkspace) {
563
596
  this.sessionManager.setSessionWorkspace(sessionId, matchedWorkspace.id);
597
+ if (matchedWorkspace.id !== types_1.TEMP_WORKSPACE_ID) {
598
+ try {
599
+ this.workspaceRepo.updateLastUsedAt(matchedWorkspace.id);
600
+ }
601
+ catch (error) {
602
+ console.warn('Failed to update workspace last used time:', error);
603
+ }
604
+ }
605
+ const selectedText = this.getUiCopy('workspaceSelected', { workspaceName: matchedWorkspace.name });
606
+ const exampleText = this.getUiCopy('workspaceSelectedExample');
564
607
  await adapter.sendMessage({
565
608
  chatId: message.chatId,
566
- text: `✅ *${matchedWorkspace.name}* selected!\n\nYou can now send me tasks.\n\nExample: "Create a new React component called Button"`,
609
+ text: `${selectedText}\n\n${exampleText}`,
567
610
  parseMode: 'markdown',
568
611
  });
569
612
  return;
@@ -626,7 +669,7 @@ class MessageRouter {
626
669
  if (args.length === 0) {
627
670
  await adapter.sendMessage({
628
671
  chatId: message.chatId,
629
- text: '🔐 Please provide a pairing code.\n\nUsage: `/pair <code>`',
672
+ text: this.getUiCopy('pairingPrompt'),
630
673
  parseMode: 'markdown',
631
674
  });
632
675
  }
@@ -681,7 +724,7 @@ class MessageRouter {
681
724
  default:
682
725
  await adapter.sendMessage({
683
726
  chatId: message.chatId,
684
- text: `Unknown command: ${command}\n\nUse /help to see available commands.`,
727
+ text: this.getUiCopy('unknownCommand', { command }),
685
728
  replyTo: message.messageId,
686
729
  });
687
730
  }
@@ -691,21 +734,24 @@ class MessageRouter {
691
734
  */
692
735
  async handleStatusCommand(adapter, message, sessionId) {
693
736
  const session = this.sessionRepo.findById(sessionId);
694
- let statusText = '✅ Bot is online and ready.\n\n';
737
+ let statusText = `✅ ${this.getUiCopy('statusHeader')}\n\n`;
695
738
  if (session?.workspaceId) {
696
739
  const workspace = this.workspaceRepo.findById(session.workspaceId);
697
740
  if (workspace) {
698
- statusText += `📁 Current workspace: ${workspace.name}\n`;
699
- statusText += ` Path: ${workspace.path}\n`;
741
+ statusText += this.getUiCopy('workspaceCurrent', {
742
+ workspaceName: workspace.name,
743
+ workspacePath: workspace.path,
744
+ });
745
+ statusText += '\n';
700
746
  }
701
747
  }
702
748
  else {
703
- statusText += '⚠️ No workspace selected. Use /workspaces to see available workspaces.';
749
+ statusText += this.getUiCopy('statusNoWorkspace');
704
750
  }
705
751
  if (session?.taskId) {
706
752
  const task = this.taskRepo.findById(session.taskId);
707
753
  if (task) {
708
- statusText += `\n🔄 Active task: ${task.title} (${task.status})`;
754
+ statusText += `\n${this.getUiCopy('statusActiveTask', { taskTitle: task.title, status: task.status })}`;
709
755
  }
710
756
  }
711
757
  await adapter.sendMessage({
@@ -721,20 +767,19 @@ class MessageRouter {
721
767
  if (workspaces.length === 0) {
722
768
  await adapter.sendMessage({
723
769
  chatId: message.chatId,
724
- text: '📁 No workspaces configured yet.\n\nAdd a workspace in the CoWork desktop app first, or use:\n`/addworkspace /path/to/your/project`',
770
+ text: this.getUiCopy('workspacesNone'),
725
771
  parseMode: 'markdown',
726
772
  });
727
773
  return;
728
774
  }
729
775
  // WhatsApp and iMessage don't support inline keyboards - use text-based selection
730
776
  if (adapter.type === 'whatsapp' || adapter.type === 'imessage') {
731
- let text = '📁 *Available Workspaces*\n\n';
777
+ let text = `${this.getUiCopy('workspacesHeader')}\n\n`;
732
778
  workspaces.forEach((ws, index) => {
733
779
  text += `${index + 1}. *${ws.name}*\n \`${ws.path}\`\n\n`;
734
780
  });
735
781
  text += '━━━━━━━━━━━━━━━\n';
736
- text += 'Reply with the number or name to select.\n';
737
- text += 'Example: `1` or `myproject`';
782
+ text += this.getUiCopy('workspacesFooter');
738
783
  await adapter.sendMessage({
739
784
  chatId: message.chatId,
740
785
  text,
@@ -751,7 +796,7 @@ class MessageRouter {
751
796
  callbackData: `workspace:${ws.id}`,
752
797
  }]);
753
798
  }
754
- let text = '📁 *Available Workspaces*\n\nTap a workspace to select it:';
799
+ let text = `${this.getUiCopy('workspacesHeader')}\n\n${this.getUiCopy('workspacesSelectPrompt')}`;
755
800
  await adapter.sendMessage({
756
801
  chatId: message.chatId,
757
802
  text,
@@ -780,7 +825,10 @@ class MessageRouter {
780
825
  const displayName = isTempWorkspace ? 'Temporary Workspace (work in a folder for persistence)' : workspace.name;
781
826
  await adapter.sendMessage({
782
827
  chatId: message.chatId,
783
- text: `📁 Current workspace: *${displayName}*\n\`${workspace.path}\`\n\nUse \`/workspaces\` to see available workspaces.`,
828
+ text: this.getUiCopy('workspaceCurrent', {
829
+ workspaceName: displayName,
830
+ workspacePath: workspace.path,
831
+ }),
784
832
  parseMode: 'markdown',
785
833
  });
786
834
  return;
@@ -788,7 +836,7 @@ class MessageRouter {
788
836
  }
789
837
  await adapter.sendMessage({
790
838
  chatId: message.chatId,
791
- text: 'No workspace selected. Use `/workspaces` to see available workspaces.',
839
+ text: this.getUiCopy('workspaceNoneSelected'),
792
840
  parseMode: 'markdown',
793
841
  });
794
842
  return;
@@ -808,15 +856,26 @@ class MessageRouter {
808
856
  if (!workspace) {
809
857
  await adapter.sendMessage({
810
858
  chatId: message.chatId,
811
- text: `❌ Workspace not found: "${selector}"\n\nUse /workspaces to see available workspaces.`,
859
+ text: this.getUiCopy('workspaceNotFound', { selector }),
812
860
  });
813
861
  return;
814
862
  }
815
863
  // Update session workspace
816
864
  this.sessionManager.setSessionWorkspace(sessionId, workspace.id);
865
+ if (workspace.id !== types_1.TEMP_WORKSPACE_ID) {
866
+ try {
867
+ this.workspaceRepo.updateLastUsedAt(workspace.id);
868
+ }
869
+ catch (error) {
870
+ console.warn('Failed to update workspace last used time:', error);
871
+ }
872
+ }
817
873
  await adapter.sendMessage({
818
874
  chatId: message.chatId,
819
- text: `✅ Workspace set to: *${workspace.name}*\n\`${workspace.path}\`\n\nYou can now send messages to create tasks in this workspace.`,
875
+ text: this.getUiCopy('workspaceSet', {
876
+ workspaceName: workspace.name,
877
+ workspacePath: workspace.path,
878
+ }),
820
879
  parseMode: 'markdown',
821
880
  });
822
881
  }
@@ -827,7 +886,7 @@ class MessageRouter {
827
886
  if (args.length === 0) {
828
887
  await adapter.sendMessage({
829
888
  chatId: message.chatId,
830
- text: '📁 *Add Workspace*\n\nUsage: `/addworkspace <path>`\n\nExample:\n`/addworkspace /Users/john/projects/myapp`\n`/addworkspace ~/Documents`',
889
+ text: this.getUiCopy('workspaceAddUsage'),
831
890
  parseMode: 'markdown',
832
891
  });
833
892
  return;
@@ -847,7 +906,7 @@ class MessageRouter {
847
906
  if (!stats.isDirectory()) {
848
907
  await adapter.sendMessage({
849
908
  chatId: message.chatId,
850
- text: `❌ Path is not a directory: \`${workspacePath}\``,
909
+ text: this.getUiCopy('workspacePathNotDir', { workspacePath }),
851
910
  parseMode: 'markdown',
852
911
  });
853
912
  return;
@@ -856,7 +915,7 @@ class MessageRouter {
856
915
  catch {
857
916
  await adapter.sendMessage({
858
917
  chatId: message.chatId,
859
- text: `❌ Directory not found: \`${workspacePath}\``,
918
+ text: this.getUiCopy('workspacePathNotFound', { workspacePath }),
860
919
  parseMode: 'markdown',
861
920
  });
862
921
  return;
@@ -869,7 +928,10 @@ class MessageRouter {
869
928
  this.sessionManager.setSessionWorkspace(sessionId, existing.id);
870
929
  await adapter.sendMessage({
871
930
  chatId: message.chatId,
872
- text: `📁 Workspace already exists!\n\n✅ Selected: *${existing.name}*\n\`${existing.path}\``,
931
+ text: this.getUiCopy('workspaceAlreadyExists', {
932
+ workspaceName: existing.name,
933
+ workspacePath: existing.path,
934
+ }),
873
935
  parseMode: 'markdown',
874
936
  });
875
937
  return;
@@ -897,7 +959,10 @@ class MessageRouter {
897
959
  }
898
960
  await adapter.sendMessage({
899
961
  chatId: message.chatId,
900
- text: `✅ Workspace added and selected!\n\n📁 *${workspace.name}*\n\`${workspace.path}\`\n\nYou can now send messages to create tasks in this workspace.`,
962
+ text: this.getUiCopy('workspaceAdded', {
963
+ workspaceName: workspace.name,
964
+ workspacePath: workspace.path,
965
+ }),
901
966
  parseMode: 'markdown',
902
967
  });
903
968
  }
@@ -917,6 +982,7 @@ class MessageRouter {
917
982
  'anthropic': 'Claude',
918
983
  'bedrock': 'Claude',
919
984
  'openai': 'OpenAI',
985
+ 'azure': 'Azure OpenAI',
920
986
  'gemini': 'Gemini',
921
987
  'openrouter': 'OpenRouter',
922
988
  'ollama': 'Ollama',
@@ -946,6 +1012,18 @@ class MessageRouter {
946
1012
  }
947
1013
  break;
948
1014
  }
1015
+ case 'azure': {
1016
+ const deployments = (settings.azure?.deployments || []).filter(Boolean);
1017
+ currentModel = settings.azure?.deployment || deployments[0] || 'deployment-name';
1018
+ models = deployments.map((deployment) => ({
1019
+ key: deployment,
1020
+ displayName: deployment,
1021
+ }));
1022
+ if (currentModel && !models.some(m => m.key === currentModel)) {
1023
+ models.unshift({ key: currentModel, displayName: currentModel });
1024
+ }
1025
+ break;
1026
+ }
949
1027
  case 'gemini': {
950
1028
  currentModel = settings.gemini?.model || 'gemini-2.0-flash';
951
1029
  const cachedGemini = provider_factory_1.LLMProviderFactory.getCachedModels('gemini');
@@ -1207,6 +1285,12 @@ class MessageRouter {
1207
1285
  model: result.model.key,
1208
1286
  };
1209
1287
  break;
1288
+ case 'azure':
1289
+ newSettings.azure = {
1290
+ ...settings.azure,
1291
+ deployment: result.model.key,
1292
+ };
1293
+ break;
1210
1294
  case 'gemini':
1211
1295
  newSettings.gemini = {
1212
1296
  ...settings.gemini,
@@ -1255,10 +1339,11 @@ class MessageRouter {
1255
1339
  text += '*Available Providers:*\n';
1256
1340
  text += '1. anthropic - Anthropic API (direct)\n';
1257
1341
  text += '2. openai - OpenAI/ChatGPT\n';
1258
- text += '3. gemini - Google Gemini\n';
1259
- text += '4. openrouter - OpenRouter\n';
1260
- text += '5. bedrock - AWS Bedrock\n';
1261
- text += '6. ollama - Ollama (local)\n\n';
1342
+ text += '3. azure - Azure OpenAI\n';
1343
+ text += '4. gemini - Google Gemini\n';
1344
+ text += '5. openrouter - OpenRouter\n';
1345
+ text += '6. bedrock - AWS Bedrock\n';
1346
+ text += '7. ollama - Ollama (local)\n\n';
1262
1347
  text += '💡 Use `/provider <name>` to switch\n';
1263
1348
  text += 'Example: `/provider bedrock` or `/provider 2`';
1264
1349
  await adapter.sendMessage({
@@ -1277,16 +1362,19 @@ class MessageRouter {
1277
1362
  '2': 'openai',
1278
1363
  'openai': 'openai',
1279
1364
  'chatgpt': 'openai',
1280
- '3': 'gemini',
1365
+ '3': 'azure',
1366
+ 'azure': 'azure',
1367
+ 'azure-openai': 'azure',
1368
+ '4': 'gemini',
1281
1369
  'gemini': 'gemini',
1282
1370
  'google': 'gemini',
1283
- '4': 'openrouter',
1371
+ '5': 'openrouter',
1284
1372
  'openrouter': 'openrouter',
1285
1373
  'or': 'openrouter',
1286
- '5': 'bedrock',
1374
+ '6': 'bedrock',
1287
1375
  'bedrock': 'bedrock',
1288
1376
  'aws': 'bedrock',
1289
- '6': 'ollama',
1377
+ '7': 'ollama',
1290
1378
  'ollama': 'ollama',
1291
1379
  'local': 'ollama',
1292
1380
  };
@@ -1294,7 +1382,7 @@ class MessageRouter {
1294
1382
  if (!targetProvider) {
1295
1383
  await adapter.sendMessage({
1296
1384
  chatId: message.chatId,
1297
- text: `❌ Unknown provider: "${args[0]}"\n\n*Available providers:*\n1. anthropic\n2. openai\n3. gemini\n4. openrouter\n5. bedrock\n6. ollama\n\nUse \`/provider <name>\` or \`/provider <number>\``,
1385
+ text: `❌ Unknown provider: "${args[0]}"\n\n*Available providers:*\n1. anthropic\n2. openai\n3. azure\n4. gemini\n5. openrouter\n6. bedrock\n7. ollama\n\nUse \`/provider <name>\` or \`/provider <number>\``,
1298
1386
  parseMode: 'markdown',
1299
1387
  });
1300
1388
  return;
@@ -1337,7 +1425,7 @@ class MessageRouter {
1337
1425
  if (!workspace) {
1338
1426
  await adapter.sendMessage({
1339
1427
  chatId: message.chatId,
1340
- text: '❌ Workspace not found.',
1428
+ text: this.getUiCopy('workspaceNotFoundForShell'),
1341
1429
  });
1342
1430
  return;
1343
1431
  }
@@ -1362,7 +1450,7 @@ class MessageRouter {
1362
1450
  else {
1363
1451
  await adapter.sendMessage({
1364
1452
  chatId: message.chatId,
1365
- text: '❌ Invalid option. Use `/shell on` or `/shell off`',
1453
+ text: this.getUiCopy('shellInvalidOption'),
1366
1454
  parseMode: 'markdown',
1367
1455
  });
1368
1456
  return;
@@ -1470,7 +1558,7 @@ class MessageRouter {
1470
1558
  if (result.success) {
1471
1559
  await adapter.sendMessage({
1472
1560
  chatId: message.chatId,
1473
- text: '✅ Pairing successful! You can now use the bot.',
1561
+ text: this.getUiCopy('pairingSuccess'),
1474
1562
  replyTo: message.messageId,
1475
1563
  });
1476
1564
  this.emitEvent({
@@ -1483,7 +1571,9 @@ class MessageRouter {
1483
1571
  else {
1484
1572
  await adapter.sendMessage({
1485
1573
  chatId: message.chatId,
1486
- text: `❌ ${result.error || 'Invalid pairing code. Please try again.'}`,
1574
+ text: this.getUiCopy('pairingFailed', {
1575
+ error: result.error || 'Invalid pairing code. Please try again.',
1576
+ }),
1487
1577
  replyTo: message.messageId,
1488
1578
  });
1489
1579
  }
@@ -1512,8 +1602,8 @@ class MessageRouter {
1512
1602
  if (this.agentDaemon) {
1513
1603
  try {
1514
1604
  const statusMsg = isActive
1515
- ? '💬 Sending follow-up message...'
1516
- : '💬 Continuing conversation...';
1605
+ ? '💬 Got it — adding that to the current task...'
1606
+ : '💬 Picking up where we left off...';
1517
1607
  await adapter.sendMessage({
1518
1608
  chatId: message.chatId,
1519
1609
  text: statusMsg,
@@ -1531,7 +1621,7 @@ class MessageRouter {
1531
1621
  console.error('Error sending follow-up message:', error);
1532
1622
  await adapter.sendMessage({
1533
1623
  chatId: message.chatId,
1534
- text: '❌ Failed to send message. Use /newtask to start a new task.',
1624
+ text: this.getUiCopy('taskContinueFailed'),
1535
1625
  });
1536
1626
  }
1537
1627
  }
@@ -1545,7 +1635,7 @@ class MessageRouter {
1545
1635
  if (!this.agentDaemon) {
1546
1636
  await adapter.sendMessage({
1547
1637
  chatId: message.chatId,
1548
- text: '❌ Agent not available. Please try again later.',
1638
+ text: this.getUiCopy('agentUnavailable'),
1549
1639
  replyTo: message.messageId,
1550
1640
  });
1551
1641
  return;
@@ -1555,7 +1645,7 @@ class MessageRouter {
1555
1645
  if (!workspace) {
1556
1646
  await adapter.sendMessage({
1557
1647
  chatId: message.chatId,
1558
- text: '❌ Workspace not found. Please select a workspace with /workspace.',
1648
+ text: this.getUiCopy('workspaceMissingForTask'),
1559
1649
  replyTo: message.messageId,
1560
1650
  });
1561
1651
  return;
@@ -1585,8 +1675,8 @@ class MessageRouter {
1585
1675
  }
1586
1676
  // Send acknowledgment - concise for WhatsApp and iMessage
1587
1677
  const ackMessage = (adapter.type === 'whatsapp' || adapter.type === 'imessage')
1588
- ? `⏳ Working on it...`
1589
- : `🚀 Task Started: "${taskTitle}"\n\nI'll notify you when it's complete or if I need your input.`;
1678
+ ? this.getUiCopy('taskStartAckSimple')
1679
+ : this.getUiCopy('taskStartAck', { taskTitle });
1590
1680
  await adapter.sendMessage({
1591
1681
  chatId: message.chatId,
1592
1682
  text: ackMessage,
@@ -1616,7 +1706,9 @@ class MessageRouter {
1616
1706
  console.error('Error starting task:', error);
1617
1707
  await adapter.sendMessage({
1618
1708
  chatId: message.chatId,
1619
- text: `❌ Failed to start task: ${error instanceof Error ? error.message : 'Unknown error'}`,
1709
+ text: this.getUiCopy('taskStartFailed', {
1710
+ error: error instanceof Error ? error.message : 'Unknown error',
1711
+ }),
1620
1712
  });
1621
1713
  // Cleanup
1622
1714
  this.pendingTaskResponses.delete(task.id);
@@ -1634,6 +1726,9 @@ class MessageRouter {
1634
1726
  return;
1635
1727
  }
1636
1728
  try {
1729
+ if (pending.adapter.type === 'whatsapp') {
1730
+ text = this.normalizeSimpleChannelMessage(text, this.getMessageContext());
1731
+ }
1637
1732
  // Use draft streaming for Telegram when streaming content
1638
1733
  if (isStreaming && pending.adapter instanceof telegram_1.TelegramAdapter) {
1639
1734
  await pending.adapter.updateDraftStream(pending.chatId, text);
@@ -1685,6 +1780,9 @@ class MessageRouter {
1685
1780
  const isSimpleMessaging = pending.adapter.type === 'whatsapp' || pending.adapter.type === 'imessage';
1686
1781
  const msgCtx = this.getMessageContext();
1687
1782
  const message = (0, channelMessages_1.getCompletionMessage)(msgCtx, result, !isSimpleMessaging);
1783
+ const normalizedMessage = pending.adapter.type === 'whatsapp'
1784
+ ? this.normalizeSimpleChannelMessage(message, msgCtx)
1785
+ : message;
1688
1786
  // Finalize draft stream if using Telegram
1689
1787
  if (pending.adapter instanceof telegram_1.TelegramAdapter) {
1690
1788
  // Finalize the streaming draft with final message
@@ -1697,7 +1795,7 @@ class MessageRouter {
1697
1795
  else {
1698
1796
  // Split long messages (Telegram has 4096 char limit, WhatsApp/iMessage ~65k but keep it reasonable)
1699
1797
  const maxLen = isSimpleMessaging ? 4000 : 4000;
1700
- const chunks = this.splitMessage(message, maxLen);
1798
+ const chunks = this.splitMessage(normalizedMessage, maxLen);
1701
1799
  for (const chunk of chunks) {
1702
1800
  await pending.adapter.sendMessage({
1703
1801
  chatId: pending.chatId,
@@ -1813,7 +1911,7 @@ class MessageRouter {
1813
1911
  sessionId: pending.sessionId,
1814
1912
  });
1815
1913
  // Format approval message
1816
- let message = `🔐 *Approval Required*\n\n`;
1914
+ let message = `🔐 *${this.getUiCopy('approvalRequiredTitle')}*\n\n`;
1817
1915
  message += `**${approval.description}**\n\n`;
1818
1916
  if (approval.type === 'run_command' && approval.details?.command) {
1819
1917
  message += `\`\`\`\n${approval.details.command}\n\`\`\`\n\n`;
@@ -1840,8 +1938,8 @@ class MessageRouter {
1840
1938
  // Create inline keyboard with Approve/Deny buttons for Telegram/Discord
1841
1939
  const keyboard = [
1842
1940
  [
1843
- { text: '✅ Approve', callbackData: 'approve:' + approval.id },
1844
- { text: '❌ Deny', callbackData: 'deny:' + approval.id },
1941
+ { text: this.getUiCopy('approvalButtonApprove'), callbackData: 'approve:' + approval.id },
1942
+ { text: this.getUiCopy('approvalButtonDeny'), callbackData: 'deny:' + approval.id },
1845
1943
  ],
1846
1944
  ];
1847
1945
  try {
@@ -1867,7 +1965,7 @@ class MessageRouter {
1867
1965
  if (!approvalEntry) {
1868
1966
  await adapter.sendMessage({
1869
1967
  chatId: message.chatId,
1870
- text: '❌ No pending approval request.',
1968
+ text: this.getUiCopy('approvalNone'),
1871
1969
  });
1872
1970
  return;
1873
1971
  }
@@ -1877,14 +1975,14 @@ class MessageRouter {
1877
1975
  await this.agentDaemon?.respondToApproval(approvalId, true);
1878
1976
  await adapter.sendMessage({
1879
1977
  chatId: message.chatId,
1880
- text: '✅ Approved! Executing...',
1978
+ text: this.getUiCopy('approvalApproved'),
1881
1979
  });
1882
1980
  }
1883
1981
  catch (error) {
1884
1982
  console.error('Error responding to approval:', error);
1885
1983
  await adapter.sendMessage({
1886
1984
  chatId: message.chatId,
1887
- text: '❌ Failed to process approval.',
1985
+ text: this.getUiCopy('approvalFailed'),
1888
1986
  });
1889
1987
  }
1890
1988
  }
@@ -1898,7 +1996,7 @@ class MessageRouter {
1898
1996
  if (!approvalEntry) {
1899
1997
  await adapter.sendMessage({
1900
1998
  chatId: message.chatId,
1901
- text: '❌ No pending approval request.',
1999
+ text: this.getUiCopy('approvalNone'),
1902
2000
  });
1903
2001
  return;
1904
2002
  }
@@ -1908,14 +2006,14 @@ class MessageRouter {
1908
2006
  await this.agentDaemon?.respondToApproval(approvalId, false);
1909
2007
  await adapter.sendMessage({
1910
2008
  chatId: message.chatId,
1911
- text: '🛑 Denied. Action cancelled.',
2009
+ text: this.getUiCopy('approvalDenied'),
1912
2010
  });
1913
2011
  }
1914
2012
  catch (error) {
1915
2013
  console.error('Error responding to denial:', error);
1916
2014
  await adapter.sendMessage({
1917
2015
  chatId: message.chatId,
1918
- text: '❌ Failed to process denial.',
2016
+ text: this.getUiCopy('approvalFailed'),
1919
2017
  });
1920
2018
  }
1921
2019
  }
@@ -1926,7 +2024,7 @@ class MessageRouter {
1926
2024
  if (!this.agentDaemon) {
1927
2025
  await adapter.sendMessage({
1928
2026
  chatId: message.chatId,
1929
- text: '❌ Agent daemon not available.',
2027
+ text: this.getUiCopy('agentUnavailable'),
1930
2028
  });
1931
2029
  return;
1932
2030
  }
@@ -1936,7 +2034,10 @@ class MessageRouter {
1936
2034
  const result = await this.agentDaemon.clearStuckTasks();
1937
2035
  await adapter.sendMessage({
1938
2036
  chatId: message.chatId,
1939
- text: `✅ Queue cleared!\n\n• Running tasks cancelled: ${result.clearedRunning}\n• Queued tasks removed: ${result.clearedQueued}\n\nBrowser sessions and other resources have been cleaned up. You can now start new tasks.`,
2037
+ text: this.getUiCopy('queueCleared', {
2038
+ running: result.clearedRunning,
2039
+ queued: result.clearedQueued,
2040
+ }),
1940
2041
  });
1941
2042
  }
1942
2043
  else {
@@ -1955,7 +2056,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
1955
2056
  • \`/queue clear\` - Clear stuck tasks`;
1956
2057
  await adapter.sendMessage({
1957
2058
  chatId: message.chatId,
1958
- text: statusText,
2059
+ text: this.getUiCopy('queueStatus', { statusText }),
1959
2060
  parseMode: 'markdown',
1960
2061
  });
1961
2062
  }
@@ -2006,13 +2107,13 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2006
2107
  this.sessionRepo.update(sessionId, { state: 'idle', taskId: undefined });
2007
2108
  await adapter.sendMessage({
2008
2109
  chatId: message.chatId,
2009
- text: '🛑 Task cancelled.',
2110
+ text: this.getUiCopy('cancelled'),
2010
2111
  });
2011
2112
  }
2012
2113
  else {
2013
2114
  await adapter.sendMessage({
2014
2115
  chatId: message.chatId,
2015
- text: 'No active task to cancel.',
2116
+ text: this.getUiCopy('cancelNoActive'),
2016
2117
  });
2017
2118
  }
2018
2119
  }
@@ -2028,7 +2129,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2028
2129
  }
2029
2130
  await adapter.sendMessage({
2030
2131
  chatId: message.chatId,
2031
- text: '🆕 Ready for a new task!\n\nSend me a message describing what you want to do.',
2132
+ text: this.getUiCopy('newTaskReady'),
2032
2133
  });
2033
2134
  }
2034
2135
  /**
@@ -2038,7 +2139,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2038
2139
  if (args.length === 0) {
2039
2140
  await adapter.sendMessage({
2040
2141
  chatId: message.chatId,
2041
- text: '❌ Please specify a workspace name to remove.\n\nUsage: `/removeworkspace <name>`',
2142
+ text: this.getUiCopy('workspaceRemoveUsage'),
2042
2143
  parseMode: 'markdown',
2043
2144
  });
2044
2145
  return;
@@ -2049,7 +2150,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2049
2150
  if (!workspace) {
2050
2151
  await adapter.sendMessage({
2051
2152
  chatId: message.chatId,
2052
- text: `❌ Workspace "${workspaceName}" not found.\n\nUse /workspaces to see available workspaces.`,
2153
+ text: this.getUiCopy('workspaceNotFound', { selector: workspaceName }),
2053
2154
  });
2054
2155
  return;
2055
2156
  }
@@ -2063,7 +2164,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2063
2164
  this.workspaceRepo.delete(workspace.id);
2064
2165
  await adapter.sendMessage({
2065
2166
  chatId: message.chatId,
2066
- text: `✅ Workspace "${workspace.name}" removed successfully.`,
2167
+ text: this.getUiCopy('workspaceRemoved', { workspaceName: workspace.name }),
2067
2168
  });
2068
2169
  }
2069
2170
  /**
@@ -2085,14 +2186,14 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2085
2186
  if (!lastFailedTask) {
2086
2187
  await adapter.sendMessage({
2087
2188
  chatId: message.chatId,
2088
- text: '❌ No failed task found to retry.\n\nStart a new task by sending a message.',
2189
+ text: this.getUiCopy('retryNone'),
2089
2190
  });
2090
2191
  return;
2091
2192
  }
2092
2193
  // Re-submit the task by sending the original prompt as a new message
2093
2194
  await adapter.sendMessage({
2094
2195
  chatId: message.chatId,
2095
- text: `🔄 Retrying task...\n\nOriginal prompt: "${lastFailedTask.title}"`,
2196
+ text: this.getUiCopy('retrying', { taskTitle: lastFailedTask.title }),
2096
2197
  });
2097
2198
  // Create a synthetic message with the original prompt
2098
2199
  const retryMessage = {
@@ -2120,7 +2221,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2120
2221
  if (recentTasks.length === 0) {
2121
2222
  await adapter.sendMessage({
2122
2223
  chatId: message.chatId,
2123
- text: '📋 No task history found.\n\nStart a new task by sending a message.',
2224
+ text: this.getUiCopy('historyNone'),
2124
2225
  });
2125
2226
  return;
2126
2227
  }
@@ -2141,7 +2242,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2141
2242
  .join('\n\n');
2142
2243
  await adapter.sendMessage({
2143
2244
  chatId: message.chatId,
2144
- text: `📋 *Recent Tasks*\n\n${historyText}`,
2245
+ text: this.getUiCopy('historyHeader', { history: historyText }),
2145
2246
  parseMode: 'markdown',
2146
2247
  });
2147
2248
  }
@@ -2156,7 +2257,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2156
2257
  if (skills.length === 0) {
2157
2258
  await adapter.sendMessage({
2158
2259
  chatId: message.chatId,
2159
- text: '📚 No skills available.\n\nSkills are stored in:\n`~/Library/Application Support/cowork-os/skills/`',
2260
+ text: this.getUiCopy('skillsNone'),
2160
2261
  parseMode: 'markdown',
2161
2262
  });
2162
2263
  return;
@@ -2190,7 +2291,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2190
2291
  catch (error) {
2191
2292
  await adapter.sendMessage({
2192
2293
  chatId: message.chatId,
2193
- text: '❌ Failed to load skills.',
2294
+ text: this.getUiCopy('skillsLoadFailed'),
2194
2295
  });
2195
2296
  }
2196
2297
  }
@@ -2201,7 +2302,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2201
2302
  if (args.length === 0) {
2202
2303
  await adapter.sendMessage({
2203
2304
  chatId: message.chatId,
2204
- text: '❌ Please specify a skill ID.\n\nUsage: `/skill <id>`\n\nUse /skills to see available skills.',
2305
+ text: this.getUiCopy('skillSpecify'),
2205
2306
  parseMode: 'markdown',
2206
2307
  });
2207
2308
  return;
@@ -2214,7 +2315,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2214
2315
  if (!skill) {
2215
2316
  await adapter.sendMessage({
2216
2317
  chatId: message.chatId,
2217
- text: `❌ Skill "${skillId}" not found.\n\nUse /skills to see available skills.`,
2318
+ text: this.getUiCopy('skillNotFound', { skillId }),
2218
2319
  });
2219
2320
  return;
2220
2321
  }
@@ -2224,14 +2325,18 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2224
2325
  const statusText = newState ? '✅ enabled' : '❌ disabled';
2225
2326
  await adapter.sendMessage({
2226
2327
  chatId: message.chatId,
2227
- text: `${skill.icon || ''} *${skill.name}* is now ${statusText}`,
2328
+ text: this.getUiCopy('skillToggle', {
2329
+ emoji: skill.icon || '⚡',
2330
+ skillName: skill.name,
2331
+ statusText,
2332
+ }),
2228
2333
  parseMode: 'markdown',
2229
2334
  });
2230
2335
  }
2231
2336
  catch (error) {
2232
2337
  await adapter.sendMessage({
2233
2338
  chatId: message.chatId,
2234
- text: '❌ Failed to toggle skill.',
2339
+ text: this.getUiCopy('skillsLoadFailed'),
2235
2340
  });
2236
2341
  }
2237
2342
  }
@@ -2254,7 +2359,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2254
2359
  const row1 = [];
2255
2360
  const row2 = [];
2256
2361
  // Get configured providers for the keyboard
2257
- const providerOrder = ['anthropic', 'openai', 'gemini', 'bedrock', 'openrouter', 'ollama'];
2362
+ const providerOrder = ['anthropic', 'openai', 'azure', 'gemini', 'bedrock', 'openrouter', 'ollama'];
2258
2363
  for (let i = 0; i < providerOrder.length; i++) {
2259
2364
  const provider = providerOrder[i];
2260
2365
  const emoji = providerEmoji[provider] || '⚡';
@@ -2345,7 +2450,7 @@ ${status.queuedCount > 0 ? `Queued task IDs: ${status.queuedTaskIds.join(', ')}`
2345
2450
  const statusText = newDebug ? '✅ enabled' : '❌ disabled';
2346
2451
  await adapter.sendMessage({
2347
2452
  chatId: message.chatId,
2348
- text: `🐛 Debug mode is now ${statusText}`,
2453
+ text: this.getUiCopy('debugStatus', { statusText }),
2349
2454
  });
2350
2455
  }
2351
2456
  /**
@@ -2383,14 +2488,14 @@ Node.js: \`${nodeVersion}\`
2383
2488
  const workspace = this.workspaceRepo.findById(session.workspaceId);
2384
2489
  await adapter.sendMessage({
2385
2490
  chatId: message.chatId,
2386
- text: `👋 *Welcome back!*\n\nWorkspace: *${workspace?.name || 'Unknown'}*\n\nJust send me what you'd like me to do.\n\nType /help for commands.`,
2491
+ text: this.getUiCopy('welcomeBack', { workspaceName: workspace?.name || 'Unknown' }),
2387
2492
  parseMode: 'markdown',
2388
2493
  });
2389
2494
  }
2390
2495
  else if (workspaces.length === 0) {
2391
2496
  await adapter.sendMessage({
2392
2497
  chatId: message.chatId,
2393
- text: `👋 *Welcome to CoWork!*\n\nI'm your AI coding assistant.\n\nFirst, add a workspace:\n\`/addworkspace /path/to/project\`\n\nOr add one from the desktop app.`,
2498
+ text: this.getUiCopy('welcomeNoWorkspace'),
2394
2499
  parseMode: 'markdown',
2395
2500
  });
2396
2501
  }
@@ -2400,17 +2505,16 @@ Node.js: \`${nodeVersion}\`
2400
2505
  this.sessionManager.setSessionWorkspace(sessionId, workspace.id);
2401
2506
  await adapter.sendMessage({
2402
2507
  chatId: message.chatId,
2403
- text: `👋 *Welcome to CoWork!*\n\n✅ Workspace: *${workspace.name}*\n\nJust tell me what you'd like me to do!\n\nExamples:\n• "Add dark mode support"\n• "Fix the login bug"\n• "Create a new API endpoint"`,
2508
+ text: this.getUiCopy('welcomeSingleWorkspace', { workspaceName: workspace.name }),
2404
2509
  parseMode: 'markdown',
2405
2510
  });
2406
2511
  }
2407
2512
  else {
2408
2513
  // Multiple workspaces - show selection
2409
- let text = `👋 *Welcome to CoWork!*\n\nSelect a workspace to start:\n\n`;
2410
- workspaces.forEach((ws, index) => {
2411
- text += `${index + 1}. *${ws.name}*\n`;
2412
- });
2413
- text += `\nReply with a number (e.g., \`1\`)`;
2514
+ const workspaceList = workspaces
2515
+ .map((ws, index) => `${index + 1}. *${ws.name}*`)
2516
+ .join('\n');
2517
+ const text = this.getUiCopy('welcomeSelectWorkspace', { workspaceList });
2414
2518
  await adapter.sendMessage({
2415
2519
  chatId: message.chatId,
2416
2520
  text,
@@ -2422,7 +2526,7 @@ Node.js: \`${nodeVersion}\`
2422
2526
  // Standard welcome for Telegram/Discord
2423
2527
  await adapter.sendMessage({
2424
2528
  chatId: message.chatId,
2425
- text: this.config.welcomeMessage,
2529
+ text: this.getUiCopy('welcomeStandard'),
2426
2530
  });
2427
2531
  // Show workspaces if none selected
2428
2532
  if (!session?.workspaceId && workspaces.length > 0) {
@@ -2435,70 +2539,10 @@ Node.js: \`${nodeVersion}\`
2435
2539
  getHelpText(channelType) {
2436
2540
  // Compact help for WhatsApp (mobile-friendly)
2437
2541
  if (channelType === 'whatsapp') {
2438
- return `📚 *Commands*
2439
-
2440
- *Basics*
2441
- /workspaces - Select workspace
2442
- /status - Current status
2443
- /newtask - Fresh start
2444
-
2445
- *Tasks*
2446
- /cancel - Stop task
2447
- /approve or /yes - Approve action
2448
- /deny or /no - Reject action
2449
-
2450
- *Settings*
2451
- /shell on|off - Shell access
2452
- /models - Change AI model
2453
-
2454
- ━━━━━━━━━━━━━━━
2455
- 💡 Just send your task directly!
2456
- Example: "Add a login form"`;
2542
+ return this.getUiCopy('helpCompact');
2457
2543
  }
2458
2544
  // Full help for other channels
2459
- return `📚 *Available Commands*
2460
-
2461
- *Core*
2462
- /start - Start the bot
2463
- /help - Show this help message
2464
- /status - Check bot status and workspace
2465
- /version - Show version information
2466
-
2467
- *Workspaces*
2468
- /workspaces - List available workspaces
2469
- /workspace <name> - Select a workspace
2470
- /addworkspace <path> - Add a new workspace
2471
- /removeworkspace <name> - Remove a workspace
2472
-
2473
- *Tasks*
2474
- /newtask - Start a fresh task/conversation
2475
- /cancel - Cancel current task
2476
- /retry - Retry the last failed task
2477
- /history - Show recent task history
2478
- /approve - Approve pending action (or /yes, /y)
2479
- /deny - Reject pending action (or /no, /n)
2480
- /queue - View/clear task queue
2481
-
2482
- *Models*
2483
- /providers - List available AI providers
2484
- /provider <name> - Show or change provider
2485
- /models - List available AI models
2486
- /model <name> - Show or change model
2487
-
2488
- *Skills*
2489
- /skills - List available skills
2490
- /skill <name> - Toggle a skill on/off
2491
-
2492
- *Settings*
2493
- /settings - View current settings
2494
- /shell - Enable/disable shell commands
2495
- /debug - Toggle debug mode
2496
-
2497
- 💬 *Quick Start*
2498
- 1. \`/workspaces\` → \`/workspace <name>\`
2499
- 2. \`/shell on\` (if needed)
2500
- 3. Send your task message
2501
- 4. \`/newtask\` to start fresh`;
2545
+ return this.getUiCopy('helpFull');
2502
2546
  }
2503
2547
  /**
2504
2548
  * Handle callback query from inline keyboard button press
@@ -2561,7 +2605,7 @@ Example: "Add a login form"`;
2561
2605
  if (!workspace) {
2562
2606
  await adapter.sendMessage({
2563
2607
  chatId: query.chatId,
2564
- text: '❌ Workspace not found.',
2608
+ text: this.getUiCopy('workspaceNotFoundShort'),
2565
2609
  });
2566
2610
  return;
2567
2611
  }
@@ -2569,12 +2613,18 @@ Example: "Add a login form"`;
2569
2613
  this.sessionManager.setSessionWorkspace(sessionId, workspace.id);
2570
2614
  // Update the original message with the selection
2571
2615
  if (adapter.editMessageWithKeyboard) {
2572
- await adapter.editMessageWithKeyboard(query.chatId, query.messageId, `✅ Workspace selected: *${workspace.name}*\n\`${workspace.path}\`\n\nYou can now send messages to create tasks.`);
2616
+ await adapter.editMessageWithKeyboard(query.chatId, query.messageId, this.getUiCopy('workspaceSet', {
2617
+ workspaceName: workspace.name,
2618
+ workspacePath: workspace.path,
2619
+ }));
2573
2620
  }
2574
2621
  else {
2575
2622
  await adapter.sendMessage({
2576
2623
  chatId: query.chatId,
2577
- text: `✅ Workspace set to: *${workspace.name}*\n\`${workspace.path}\``,
2624
+ text: this.getUiCopy('workspaceSet', {
2625
+ workspaceName: workspace.name,
2626
+ workspacePath: workspace.path,
2627
+ }),
2578
2628
  parseMode: 'markdown',
2579
2629
  });
2580
2630
  }
@@ -2619,6 +2669,9 @@ Example: "Add a login form"`;
2619
2669
  case 'openai':
2620
2670
  newSettings.openai = { ...settings.openai, model: modelKey };
2621
2671
  break;
2672
+ case 'azure':
2673
+ newSettings.azure = { ...settings.azure, deployment: modelKey };
2674
+ break;
2622
2675
  case 'gemini':
2623
2676
  newSettings.gemini = { ...settings.gemini, model: modelKey };
2624
2677
  break;
@@ -2656,7 +2709,7 @@ Example: "Add a login form"`;
2656
2709
  .find(([, data]) => data.sessionId === sessionId);
2657
2710
  if (!approvalEntry) {
2658
2711
  if (adapter.editMessageWithKeyboard) {
2659
- await adapter.editMessageWithKeyboard(query.chatId, query.messageId, '❌ No pending approval request (may have expired).');
2712
+ await adapter.editMessageWithKeyboard(query.chatId, query.messageId, this.getUiCopy('approvalNone'));
2660
2713
  }
2661
2714
  return;
2662
2715
  }
@@ -2664,7 +2717,9 @@ Example: "Add a login form"`;
2664
2717
  this.pendingApprovals.delete(approvalId);
2665
2718
  try {
2666
2719
  await this.agentDaemon?.respondToApproval(approvalId, approved);
2667
- const statusText = approved ? '✅ Approved! Executing...' : '🛑 Denied. Action cancelled.';
2720
+ const statusText = approved
2721
+ ? this.getUiCopy('approvalApproved')
2722
+ : this.getUiCopy('approvalDenied');
2668
2723
  if (adapter.editMessageWithKeyboard) {
2669
2724
  await adapter.editMessageWithKeyboard(query.chatId, query.messageId, statusText);
2670
2725
  }
@@ -2679,7 +2734,7 @@ Example: "Add a login form"`;
2679
2734
  console.error('Error responding to approval:', error);
2680
2735
  await adapter.sendMessage({
2681
2736
  chatId: query.chatId,
2682
- text: '❌ Failed to process response.',
2737
+ text: this.getUiCopy('responseFailed'),
2683
2738
  });
2684
2739
  }
2685
2740
  }