tide-commander 1.63.0 → 1.64.0

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 (42) hide show
  1. package/dist/assets/{BossLogsModal-BpNtzCTX.js → BossLogsModal-DLR42-IT.js} +1 -1
  2. package/dist/assets/{BossSpawnModal-ByiEkNZl.js → BossSpawnModal-DAgHl3R6.js} +1 -1
  3. package/dist/assets/{ControlsModal-BgeLGCGT.js → ControlsModal-gP0MLYlM.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-CaK9YJT4.js → DockerLogsModal-CFIev1fE.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-DcGJCiuP.js → EmbeddedEditor-DHavC0_4.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-CYIBkOXn.js → GmailOAuthSetup-C-lLv5zX.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-CY_EF39t.js → GoogleOAuthSetup-Y8xqNYab.js} +1 -1
  8. package/dist/assets/{IframeModal-DDFIBCq1.js → IframeModal-ChWDJAUG.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-CssC1pup.js → IntegrationsPanel-BIkMvY4c.js} +2 -2
  10. package/dist/assets/{LogViewerModal-C_f0Z-2y.js → LogViewerModal-A1BqkaSG.js} +1 -1
  11. package/dist/assets/{MonitoringModal-CkOkdUvI.js → MonitoringModal-B-exbiAO.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-BtqM8TkL.js → PM2LogsModal-W8QwyfIH.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-CIqWIO9m.js → RestoreArchivedAreaModal-DWBL-dIK.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-CJ_MZU-J.js → Scene2DCanvas-m8C3YWvc.js} +1 -1
  15. package/dist/assets/{SceneManager-DyX0JqRQ.js → SceneManager-C0mDJleN.js} +1 -1
  16. package/dist/assets/{SkillsPanel-DR7WNV5l.js → SkillsPanel-DyQC5vmc.js} +1 -1
  17. package/dist/assets/{SpawnModal-BCJDoSQg.js → SpawnModal-DRZb4yQv.js} +1 -1
  18. package/dist/assets/{SubordinateAssignmentModal-DAlLWBtv.js → SubordinateAssignmentModal-GspXu4uJ.js} +1 -1
  19. package/dist/assets/{TriggerManagerPanel-ChBctjk3.js → TriggerManagerPanel-C2-JZ9Td.js} +1 -1
  20. package/dist/assets/{WorkflowEditorPanel-Bk-QD7-7.js → WorkflowEditorPanel-BKZIKBbb.js} +1 -1
  21. package/dist/assets/{index-M-tYhxQp.js → index-8tA-hcUN.js} +3 -3
  22. package/dist/assets/{index-DwTs55lG.js → index-B-9UaF21.js} +1 -1
  23. package/dist/assets/{index-CVIkvsfJ.js → index-BTWI52EO.js} +1 -1
  24. package/dist/assets/{index-Dwt45QE5.js → index-BootSGxG.js} +1 -1
  25. package/dist/assets/{index-vx9STDIr.js → index-C2j4gLM5.js} +1 -1
  26. package/dist/assets/{index-DcxPBYWc.js → index-CJzaSVsT.js} +1 -1
  27. package/dist/assets/{index-COaYNxnC.js → index-CbxBgs9P.js} +1 -1
  28. package/dist/assets/{index-DoOLLVEG.js → index-QXsncANJ.js} +2 -2
  29. package/dist/assets/{main-5l14ueeE.js → main-BqGYHsUJ.js} +68 -68
  30. package/dist/assets/main-DpE84tMP.css +1 -0
  31. package/dist/assets/{web-BYu5GvMD.js → web-CrNLIUD2.js} +1 -1
  32. package/dist/assets/{web-BsMMNK0E.js → web-CxCQRl7w.js} +1 -1
  33. package/dist/index.html +2 -2
  34. package/dist/src/packages/server/data/builtin-skills/agent-tracking.js +11 -3
  35. package/dist/src/packages/server/data/builtin-skills/task-label.js +24 -17
  36. package/dist/src/packages/server/integrations/slack/slack-client.js +30 -0
  37. package/dist/src/packages/server/integrations/slack/slack-routes.js +17 -0
  38. package/dist/src/packages/server/integrations/slack/slack-skill.js +23 -0
  39. package/dist/src/packages/server/integrations/slack/slack-trigger-handler.js +14 -0
  40. package/dist/src/packages/server/routes/agents.js +15 -1
  41. package/package.json +1 -1
  42. package/dist/assets/main-a5zo94fK.css +0 -1
@@ -1 +1 @@
1
- import{bH as a}from"./main-5l14ueeE.js";import{ImpactStyle as i,NotificationType as r}from"./index-DoOLLVEG.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
1
+ import{bH as a}from"./main-BqGYHsUJ.js";import{ImpactStyle as i,NotificationType as r}from"./index-QXsncANJ.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
@@ -1 +1 @@
1
- import{bH as s}from"./main-5l14ueeE.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
1
+ import{bH as s}from"./main-BqGYHsUJ.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
package/dist/index.html CHANGED
@@ -22,10 +22,10 @@
22
22
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
23
23
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
24
24
  <title>Tide Commander</title>
25
- <script type="module" crossorigin src="/assets/main-5l14ueeE.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-BqGYHsUJ.js"></script>
26
26
  <link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
28
- <link rel="stylesheet" crossorigin href="/assets/main-a5zo94fK.css">
28
+ <link rel="stylesheet" crossorigin href="/assets/main-DpE84tMP.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="app"></div>
@@ -6,7 +6,7 @@ export const agentTracking = {
6
6
  assignedAgentClasses: ['*'],
7
7
  content: `# Agent Tracking Status (MANDATORY)
8
8
 
9
- **Every turn MUST end with a tracking-status PATCH curl as your final tool call. No exceptions — not for tiny replies, not for questions, not for refusals. Skipping it leaves the user's board stuck on stale \`working\`.**
9
+ **Every turn MUST end with a tracking-status PATCH curl as your final tool call. No exceptions — not for tiny replies, not for questions, not for refusals. Skipping it leaves the user's board stuck on stale \`writing\` or \`working\`.**
10
10
 
11
11
  ## When to Call
12
12
  - After finishing ANY reply (one-word answers included)
@@ -23,18 +23,26 @@ export const agentTracking = {
23
23
  \`\`\`
24
24
 
25
25
  ## Statuses
26
+ - \`writing\` — the "thinking" state, set AUTOMATICALLY at the start of each turn by the Task Label skill. You normally do NOT set this yourself; the combined opening PATCH (taskLabel + trackingStatus:'writing') covers it. The UI shows an animated typing-dots indicator while in this state.
27
+ - \`working\` — longer-running work in progress; set this mid-turn if you want to clear the typing-dots indicator before you're fully done (e.g. a long build, a tight loop of edits). Optional.
26
28
  - \`need-review\` — finished work awaiting user review (describe what)
27
29
  - \`blocked\` — cannot proceed (say WHO/WHAT blocks you)
28
30
  - \`can-clear-context\` — fully done, context safe to clear
29
31
  - \`waiting-subordinates\` — boss agent waiting on delegated work
30
- - \`working\` — set automatically; do not set manually
32
+
33
+ ## Status Lifecycle Within a Turn
34
+ 1. **Start of turn:** the Task Label skill sets \`writing\` (typing dots visible).
35
+ 2. **During the turn:** the \`writing\` state persists until you replace it. You MAY optionally PATCH to \`working\` once you start doing real work (reads/edits/bash) if the turn is long — this swaps typing dots for the cyan working indicator.
36
+ 3. **End of turn:** send the final tracking PATCH with the outcome status (\`need-review\`, \`blocked\`, \`can-clear-context\`, or \`waiting-subordinates\`). This replaces \`writing\`/\`working\` with the final state.
31
37
 
32
38
  ## Rules
33
39
  - Detail ≤ 80 chars
34
40
  - Tracking curl is the VERY LAST tool call — all user-facing text comes BEFORE it, nothing after
35
41
  - Don't pick \`can-clear-context\` if anything still needs user confirmation — use \`need-review\`
42
+ - Do NOT manually set \`writing\` outside the opening PATCH from the Task Label skill — it's only for turn-start.
36
43
 
37
44
  ## Final Check Before Ending a Turn
38
45
  1. Have I sent the PATCH this turn? If not — send now.
39
- 2. Is it my last tool call with no output after? If not — fix order.`,
46
+ 2. Is it my last tool call with no output after? If not — fix order.
47
+ 3. Have I replaced the opening \`writing\` status with a meaningful final status? If not — do it now.`,
40
48
  };
@@ -1,23 +1,23 @@
1
1
  export const taskLabel = {
2
2
  slug: 'task-label',
3
3
  name: 'Task Label',
4
- description: 'Generate a brief task label for scene display',
4
+ description: 'Generate a brief task label and mark writing status at the start of every turn',
5
5
  allowedTools: ['Bash(curl:*)'],
6
6
  assignedAgentClasses: ['*'],
7
- content: `# Task Label (MANDATORY - Execute FIRST)
7
+ content: `# Task Label + Writing Status (MANDATORY - Execute FIRST)
8
8
 
9
- **IMPORTANT: You MUST generate a brief task label describing your current task.**
9
+ **IMPORTANT: Every new turn begins with a single PATCH that (a) sets a brief task label and (b) flips your tracking status to \`writing\`. This makes the UI show a typing indicator while you plan/think before any real work happens.**
10
10
 
11
11
  ## Steps (do this FIRST before anything else):
12
12
  1. Read the user's request
13
13
  2. Generate a 1-5 word summary of the task (e.g., "Fix auth bug", "Add dark mode", "Refactor API calls", "Update tests")
14
- 3. Call the API to set your task label:
14
+ 3. Call the API to set BOTH your task label AND writing status in one PATCH:
15
15
 
16
16
  \`PATCH /api/agents/YOUR_AGENT_ID\`
17
17
 
18
- **Body:**
18
+ **Body (combined):**
19
19
  \`\`\`json
20
- {"taskLabel":"YOUR 1-5 WORD LABEL"}
20
+ {"taskLabel":"YOUR 1-5 WORD LABEL","trackingStatus":"writing","trackingStatusDetail":"Short sentence describing what you're about to do"}
21
21
  \`\`\`
22
22
 
23
23
  4. Then proceed with the actual task
@@ -26,14 +26,18 @@ export const taskLabel = {
26
26
  - Keep labels between 1-5 words maximum
27
27
  - Use action verbs: Fix, Add, Update, Refactor, Debug, Implement, Remove, etc.
28
28
  - Be specific but concise (e.g., "Fix login redirect" not "Work on stuff")
29
+ - \`trackingStatusDetail\` ≤ 80 chars — a tiny preview of the plan, shown under the typing dots
30
+
31
+ ## Why \`writing\`?
32
+ \`writing\` is the "thinking" status. It renders a typing-dots indicator in the tracking board so the user knows you've received their request and are forming a plan. It will be replaced automatically by your normal end-of-turn tracking PATCH (see the Agent Tracking skill), which sets the final status (\`need-review\`, \`blocked\`, \`can-clear-context\`, etc.).
29
33
 
30
34
  ## CRITICAL: Execution Order
31
35
 
32
- YOUR VERY FIRST TOOL CALL, before ANY other tool — no Read, no Grep, no Glob, no Bash, no Agent, no TodoWrite, no WebSearch, NOTHING — MUST be the PATCH curl to /api/agents/YOUR_AGENT_ID with the taskLabel body. This is non-negotiable and has zero exceptions.
36
+ YOUR VERY FIRST TOOL CALL, before ANY other tool — no Read, no Grep, no Glob, no Bash, no Agent, no TodoWrite, no WebSearch, NOTHING — MUST be the combined PATCH above. This is non-negotiable and has zero exceptions.
33
37
 
34
38
  ### Pre-flight checklist (run mentally before your first response):
35
- - (a) Have I set the task label yet for this task?
36
- - (b) If NO — the ONLY acceptable first action is the taskLabel PATCH curl. Stop. Do that first.
39
+ - (a) Have I sent the combined taskLabel + writing PATCH yet for this task?
40
+ - (b) If NO — the ONLY acceptable first action is that PATCH curl. Stop. Do that first.
37
41
  - (c) If YES — proceed with normal work.
38
42
 
39
43
  ### WRONG — never do this:
@@ -47,24 +51,27 @@ Grep("somePattern") ← VIOLATION
47
51
  # WRONG: Planning before labeling
48
52
  TodoWrite([...]) ← VIOLATION
49
53
 
50
- # WRONG: Batching the label with other calls (it gets buried)
54
+ # WRONG: Two separate PATCHes (one for label, one for writing)
55
+ PATCH taskLabel only → PATCH trackingStatus only ← VIOLATION (combine them)
56
+
57
+ # WRONG: Batching the label PATCH with other tool calls
51
58
  PATCH taskLabel + Grep + Read ← VIOLATION
52
59
  \`\`\`
53
60
 
54
61
  ### CORRECT — always do this:
55
62
  \`\`\`
56
- # First turn: ONLY the taskLabel PATCH, nothing else
63
+ # First turn: ONLY the combined PATCH, nothing else
57
64
  curl -s -X PATCH http://localhost:5174/api/agents/YOUR_AGENT_ID \\
58
65
  -H "Content-Type: application/json" -H "X-Auth-Token: abcd" \\
59
- -d '{"taskLabel":"Fix login redirect"}'
66
+ -d '{"taskLabel":"Fix login redirect","trackingStatus":"writing","trackingStatusDetail":"Tracing the bad redirect on mobile Safari"}'
60
67
 
61
68
  # Second turn onward: proceed with all actual work (Read, Grep, etc.)
62
69
  \`\`\`
63
70
 
64
71
  ### Additional enforcement rules:
65
- - The taskLabel PATCH MUST be the sole call in the first tool-call batch — never parallelized with any other tool call.
66
- - This rule applies to EVERY new task, including follow-up tasks in the same session. When scope changes, update the label immediately as the first action before proceeding.
67
- - If you skip the label and realize mid-task — stop, emit the PATCH now, then continue. Late is better than never, but first is always required.
68
- - If you fail to set the label as your first action, you are violating a mandatory instruction.
69
- - There are ZERO exceptions to this rule — every single task begins with setting the label.`,
72
+ - The opening PATCH MUST be the sole call in the first tool-call batch — never parallelized with any other tool call.
73
+ - This rule applies to EVERY new task, including follow-up tasks in the same session. When scope changes, update BOTH fields immediately as the first action before proceeding.
74
+ - If you skip the PATCH and realize mid-task — stop, emit the PATCH now, then continue. Late is better than never, but first is always required.
75
+ - If you fail to send this PATCH as your first action, you are violating a mandatory instruction.
76
+ - There are ZERO exceptions to this rule — every single task begins with this combined PATCH.`,
70
77
  };
@@ -241,6 +241,36 @@ export async function sendMessage(params) {
241
241
  });
242
242
  return { ts, channel };
243
243
  }
244
+ /**
245
+ * Add an emoji reaction to a message. Requires `reactions:write`.
246
+ * `already_reacted` responses are swallowed silently.
247
+ */
248
+ export async function addReaction(params) {
249
+ if (!webClient)
250
+ throw new Error('Slack not connected');
251
+ const name = normalizeEmojiName(params.name);
252
+ try {
253
+ await webClient.reactions.add({
254
+ channel: params.channel,
255
+ timestamp: params.ts,
256
+ name,
257
+ });
258
+ }
259
+ catch (err) {
260
+ const slackErr = err.data?.error;
261
+ if (slackErr === 'already_reacted')
262
+ return;
263
+ throw err;
264
+ }
265
+ }
266
+ /** Map raw emoji chars to Slack slugs; strip surrounding colons if caller passed `:eyes:`. */
267
+ function normalizeEmojiName(input) {
268
+ const trimmed = input.trim().replace(/^:|:$/g, '');
269
+ // Any eye-related emoji char collapses to the common `eyes` slug.
270
+ if (trimmed === '👁' || trimmed === '👁️' || trimmed === '👀')
271
+ return 'eyes';
272
+ return trimmed;
273
+ }
244
274
  export async function getChannelMessages(params) {
245
275
  if (!webClient)
246
276
  throw new Error('Slack not connected');
@@ -290,6 +290,23 @@ router.post('/files/:id/download', async (req, res) => {
290
290
  res.status(500).json({ error: `Failed to download file: ${err instanceof Error ? err.message : err}` });
291
291
  }
292
292
  });
293
+ // POST /api/slack/reactions/add — Add an emoji reaction to a message
294
+ // Body: { channel, ts, name } — name is the Slack slug without colons (e.g. "eyes").
295
+ router.post('/reactions/add', async (req, res) => {
296
+ try {
297
+ const { channel, ts, name } = req.body;
298
+ if (!channel || !ts || !name) {
299
+ res.status(400).json({ error: 'channel, ts, and name are required' });
300
+ return;
301
+ }
302
+ await slackClient.addReaction({ channel, ts, name });
303
+ res.json({ success: true });
304
+ }
305
+ catch (err) {
306
+ log.error(`Slack reactions.add error: ${err}`);
307
+ res.status(500).json({ error: `Failed to add reaction: ${err instanceof Error ? err.message : err}` });
308
+ }
309
+ });
293
310
  // GET /api/slack/status — Get connection status
294
311
  router.get('/status', (_req, res) => {
295
312
  const config = loadConfig();
@@ -169,6 +169,29 @@ Messages returned by \`/messages\` and \`/thread\` now include an optional \`fil
169
169
 
170
170
  **Pitfall — do NOT \`curl\` \`url_private\` directly.** Slack's \`url_private\` (and \`url_private_download\`) only return the actual file bytes when the request sends \`Authorization: Bearer <bot-token>\`; without it Slack serves an HTML sign-in page. Use the proxy endpoints above — they attach the bot token server-side so agents never need to handle the token.
171
171
 
172
+ ## Reactions
173
+
174
+ Add an emoji reaction to a Slack message. Requires the bot token to have **\`reactions:write\`**.
175
+
176
+ \`\`\`bash
177
+ curl -s -X POST http://localhost:5174/api/slack/reactions/add \\
178
+ -H "Content-Type: application/json" \\
179
+ -d '{"channel":"C0123456789","ts":"1234567890.123456","name":"eyes"}'
180
+ \`\`\`
181
+
182
+ Fields:
183
+ - \`channel\` — Slack channel id (required)
184
+ - \`ts\` — message timestamp (required; looks like \`1234567890.123456\`)
185
+ - \`name\` — emoji slug without colons (e.g. \`eyes\`, \`+1\`, \`white_check_mark\`). Raw eye emoji chars (\`👁\`, \`👀\`) are auto-normalized to \`eyes\`.
186
+
187
+ \`already_reacted\` responses are silently ignored.
188
+
189
+ ### Auto-react on triggers
190
+
191
+ When a Slack trigger fires on an incoming message, the bot automatically reacts with :eyes: (👀) as a visual acknowledgement that it saw the message. This happens fire-and-forget — a failed reaction never blocks the trigger.
192
+
193
+ Disable the auto-ack by setting \`SLACK_REACT_ON_TRIGGER=false\` (accepts \`false\`/\`0\`/\`no\`/\`off\`) in the server environment.
194
+
172
195
  ## Check Connection Status
173
196
 
174
197
  \`\`\`bash
@@ -5,10 +5,24 @@
5
5
  */
6
6
  import * as slackClient from './slack-client.js';
7
7
  let unsubscribe = null;
8
+ /** Env toggle: set SLACK_REACT_ON_TRIGGER=false (or 0/no/off) to disable the auto-:eyes: ack. */
9
+ function reactOnTriggerEnabled() {
10
+ const raw = (process.env.SLACK_REACT_ON_TRIGGER ?? '').toLowerCase().trim();
11
+ if (!raw)
12
+ return true;
13
+ return !['false', '0', 'no', 'off'].includes(raw);
14
+ }
8
15
  export const slackTriggerHandler = {
9
16
  triggerType: 'slack',
10
17
  async startListening(onEvent) {
18
+ const autoReact = reactOnTriggerEnabled();
11
19
  unsubscribe = slackClient.onMessage((message) => {
20
+ // Fire-and-forget :eyes: reaction to ack that the bot saw it. Failure MUST NOT block triggers.
21
+ if (autoReact) {
22
+ slackClient
23
+ .addReaction({ channel: message.channel, ts: message.ts, name: 'eyes' })
24
+ .catch(() => { });
25
+ }
12
26
  onEvent({
13
27
  source: 'slack',
14
28
  type: 'message',
@@ -623,7 +623,21 @@ router.patch('/:id', (req, res) => {
623
623
  res.status(404).json({ error: 'Agent not found' });
624
624
  return;
625
625
  }
626
- res.json(updated);
626
+ // Slim response: agents PATCH this endpoint several times per turn (taskLabel, trackingStatus),
627
+ // and returning the full agent — which includes multi-KB `lastAssignedTask`/`currentTask` strings —
628
+ // bloated agent context. Full agent state is already broadcast to clients via the WS `agent_updated`
629
+ // channel, so the response only needs to confirm the fields an agent typically cares about.
630
+ res.json({
631
+ id: updated.id,
632
+ name: updated.name,
633
+ status: updated.status,
634
+ trackingStatus: updated.trackingStatus,
635
+ trackingStatusDetail: updated.trackingStatusDetail,
636
+ taskLabel: updated.taskLabel,
637
+ lastActivity: updated.lastActivity,
638
+ isBoss: updated.isBoss,
639
+ ok: true,
640
+ });
627
641
  });
628
642
  // DELETE /api/agents/:id - Delete agent
629
643
  router.delete('/:id', (req, res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.63.0",
3
+ "version": "1.64.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",