ajuste e melhorias

This commit is contained in:
Rodrigo Borba 2026-01-20 13:16:32 -03:00
parent dbd66320e0
commit 7ea7d238b7
46 changed files with 2395 additions and 293 deletions

View File

@ -173,7 +173,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def inbox_attributes
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name, :auto_resolve_duration,
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name, :auto_resolve_duration, :message_signature_enabled,
{ csat_config: [:display_type, :message, :button_text, :language,
{ survey_rules: [:operator, { values: [] }],
template: [:name, :template_id, :created_at, :language] }] }]

View File

@ -1,6 +1,5 @@
<script>
import { ref } from 'vue';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import FileUpload from 'vue-upload-component';
import * as ActiveStorage from 'activestorage';
@ -118,6 +117,10 @@ export default {
type: Boolean,
default: false,
},
signatureEnabled: {
type: Boolean,
default: false,
},
quotedReplyEnabled: {
type: Boolean,
default: false,
@ -129,11 +132,9 @@ export default {
'selectWhatsappTemplate',
'selectContentTemplate',
'toggleQuotedReply',
'toggleSignature',
],
setup() {
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
useUISettings();
const uploadRef = ref(false);
const keyboardEvents = {
@ -155,8 +156,6 @@ export default {
useKeyboardEvents(keyboardEvents);
return {
setSignatureFlagForInbox,
fetchSignatureFlagFromUISettings,
uploadRef,
};
},
@ -239,8 +238,7 @@ export default {
return !this.isOnPrivateNote;
},
sendWithSignature() {
// channelType is sourced from inboxMixin
return this.fetchSignatureFlagFromUISettings(this.channelType);
return this.signatureEnabled;
},
signatureToggleTooltip() {
return this.sendWithSignature
@ -264,7 +262,7 @@ export default {
},
methods: {
toggleMessageSignature() {
this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
this.$emit('toggleSignature');
},
replaceText(text) {
this.$emit('replaceText', text);
@ -333,8 +331,8 @@ export default {
v-if="showMessageSignatureButton"
v-tooltip.top-end="signatureToggleTooltip"
icon="i-ph-signature"
slate
faded
:variant="signatureEnabled ? 'solid' : 'faded'"
color="slate"
sm
@click="toggleMessageSignature"
/>
@ -414,20 +412,22 @@ export default {
<style lang="scss" scoped>
.left-wrap {
@apply items-center flex gap-2;
display: flex;
align-items: center;
gap: 0.5rem;
}
.right-wrap {
@apply flex;
display: flex;
}
::v-deep .file-uploads {
label {
@apply cursor-pointer;
cursor: pointer;
}
&:hover button {
@apply enabled:bg-n-slate-9/20;
background-color: var(--n-slate-9/20);
}
}
</style>

View File

@ -91,12 +91,13 @@ export default {
<template>
<div class="flex justify-between h-[3.25rem] gap-2 ltr:pl-3 rtl:pr-3">
<div class="flex items-center gap-2 mt-3">
<EditorModeToggle
:mode="mode"
:disabled="isReplyRestricted"
class="mt-3"
@toggle-mode="handleModeToggle"
/>
</div>
<div class="flex items-center mx-4 my-0">
<div v-if="isMessageLengthReachingThreshold" class="text-xs">
<span :class="charLengthClass">

View File

@ -87,7 +87,6 @@ export default {
const {
uiSettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
setQuotedReplyFlagForInbox,
fetchQuotedReplyFlagFromUISettings,
} = useUISettings();
@ -97,7 +96,6 @@ export default {
return {
uiSettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
setQuotedReplyFlagForInbox,
fetchQuotedReplyFlagFromUISettings,
replyEditor,
@ -331,7 +329,8 @@ export default {
return !!this.messageSignature;
},
sendWithSignature() {
return this.fetchSignatureFlagFromUISettings(this.channelType);
// Signature is enabled for this inbox in configuration
return this.inbox.message_signature_enabled;
},
conversationId() {
return this.currentChat.id;
@ -611,29 +610,9 @@ export default {
}
},
toggleSignatureForDraft(message) {
if (this.isPrivate) {
// We don't want to modify the editor content anymore as per new requirements.
// Signature is applied only on send.
return message;
}
if (this.showRichContentEditor) {
const effectiveChannelType = getEffectiveChannelType(
this.channelType,
this.inbox?.medium || ''
);
return this.sendWithSignature
? appendSignature(
message,
this.messageSignature,
effectiveChannelType
)
: removeSignature(
message,
this.messageSignature,
effectiveChannelType
);
}
return this.sendWithSignature
? appendSignature(message, this.signatureToApply)
: removeSignature(message, this.signatureToApply);
},
removeFromDraft() {
if (this.conversationIdByRoute) {
@ -733,6 +712,13 @@ export default {
return;
}
if (!this.showMentions) {
let messageToSend = this.message;
if (this.sendWithSignature && !this.isPrivate) {
const senderName =
this.currentUser.display_name || this.currentUser.name;
messageToSend = `*${senderName}:*\n${messageToSend}`;
}
const isOnWhatsApp =
this.isATwilioWhatsAppChannel ||
this.isAWhatsAppCloudChannel ||
@ -742,9 +728,9 @@ export default {
// This can create duplicate messages in Chatwoot. To prevent this issue, we'll handle text and attachments as separate messages.
const isOnInstagram = this.isAnInstagramChannel;
if ((isOnWhatsApp || isOnInstagram) && !this.isPrivate) {
this.sendMessageAsMultipleMessages(this.message);
this.sendMessageAsMultipleMessages(messageToSend);
} else {
const messagePayload = this.getMessagePayload(this.message);
const messagePayload = this.getMessagePayload(messageToSend);
this.sendMessage(messagePayload);
}
@ -899,22 +885,7 @@ export default {
},
clearMessage() {
this.message = '';
if (this.sendWithSignature && !this.isPrivate) {
// if signature is enabled, append it to the message
if (this.showRichContentEditor) {
const effectiveChannelType = getEffectiveChannelType(
this.channelType,
this.inbox?.medium || ''
);
this.message = appendSignature(
this.message,
this.messageSignature,
effectiveChannelType
);
} else {
this.message = appendSignature(this.message, this.signatureToApply);
}
}
// Cleaned up legacy signature logic to prevent editor clutter
this.attachedFiles = [];
this.isRecordingAudio = false;
this.resetReplyToMessage();
@ -1312,6 +1283,7 @@ export default {
:toggle-audio-recorder="toggleAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker"
:message="message"
:signature-enabled="sendWithSignature"
:portal-slug="connectedPortalSlug"
:new-conversation-modal-active="newConversationModalActive"
@select-whatsapp-template="openWhatsappTemplateModal"
@ -1319,6 +1291,7 @@ export default {
@replace-text="replaceText"
@toggle-insert-article="toggleInsertArticle"
@toggle-quoted-reply="toggleQuotedReply"
@toggle-signature="toggleSignature"
/>
<WhatsappTemplates
:inbox-id="inbox.id"
@ -1346,11 +1319,12 @@ export default {
<style lang="scss" scoped>
.send-button {
@apply mb-0;
margin-bottom: 0px;
}
.attachment-preview-box {
@apply bg-transparent py-0 px-4;
background-color: transparent;
padding: 0 1rem;
}
.reply-box {
@ -1364,7 +1338,7 @@ export default {
}
.send-button {
@apply mb-0;
margin-bottom: 0px;
}
.reply-box__top {

View File

@ -76,16 +76,15 @@ const toggleSidebarUIState = (key, uiSettings, updateUISettings) => {
};
/**
* Sets the signature flag for a specific channel type in the inbox settings.
* @param {string} channelType - The type of the channel.
* Sets the signature flag for a specific inbox in the settings.
* @param {number|string} inboxId - The ID of the inbox.
* @param {boolean} value - The value to set for the signature enabled flag.
* @param {Function} updateUISettings - Function to update UI settings.
*/
const setSignatureFlagForInbox = (channelType, value, updateUISettings) => {
if (!channelType) return;
const setSignatureFlagForInbox = (inboxId, value, updateUISettings) => {
if (!inboxId) return;
const slugifiedChannel = slugifyChannel(channelType);
updateUISettings({ [`${slugifiedChannel}_signature_enabled`]: value });
updateUISettings({ [`inbox_${inboxId}_signature_enabled`]: value });
};
const setQuotedReplyFlagForInbox = (channelType, value, updateUISettings) => {
@ -96,16 +95,15 @@ const setQuotedReplyFlagForInbox = (channelType, value, updateUISettings) => {
};
/**
* Fetches the signature flag for a specific channel type from UI settings.
* @param {string} channelType - The type of the channel.
* Fetches the signature flag for a specific inbox from UI settings.
* @param {number|string} inboxId - The ID of the inbox.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {boolean} The value of the signature enabled flag.
*/
const fetchSignatureFlagFromUISettings = (channelType, uiSettings) => {
if (!channelType) return false;
const fetchSignatureFlagFromUISettings = (inboxId, uiSettings) => {
if (!inboxId) return false;
const slugifiedChannel = slugifyChannel(channelType);
return uiSettings.value[`${slugifiedChannel}_signature_enabled`];
return uiSettings.value[`inbox_${inboxId}_signature_enabled`];
};
const fetchQuotedReplyFlagFromUISettings = (channelType, uiSettings) => {

View File

@ -34,6 +34,9 @@
"PLACEHOLDER": "Digite o nome da caixa de entrada (ex: Acme Inc)",
"ERROR": "Por favor, insira um nome completo válido"
},
"MESSAGE_SIGNATURE": {
"LABEL": "Assinar Mensagens"
},
"WEBSITE_NAME": {
"LABEL": "Nome do site",
"PLACEHOLDER": "Informe o nome do seu site (por exemplo: Acme Inc)"

View File

@ -34,7 +34,8 @@ watch(
},
{ deep: true }
);
watch(isEnabled, newVal => {
watch(isEnabled, () => {
// [INTENTIONAL] newVal reserved for future diff checks.
// If user toggles switch, we consider it a change that needs saving
// Or we can auto-save. The requirement says "Toggle Ativar/Desativar"
// Let's rely on the user clicking save for config, but maybe auto-save toggle?
@ -116,6 +117,8 @@ function formatJson(str) {
</script>
<template>
<!-- eslint-disable vue/no-bare-strings-in-template -->
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<div class="bg-n-solid-1 border border-n-weak rounded-lg p-5 mb-4">
<div class="flex justify-between items-start mb-4">
<div>
@ -144,7 +147,7 @@ function formatJson(str) {
>
{{ isEnabled ? 'Ativo' : 'Inativo' }}
</span>
<woot-switch v-model="isEnabled" />
<WootSwitch v-model="isEnabled" />
</div>
</div>
@ -153,20 +156,22 @@ function formatJson(str) {
<div>
<label
class="block text-xs font-medium text-n-slate-11 mb-1 uppercase tracking-wide"
>PLUG-PLAY-ID</label
>
PLUG-PLAY-ID
</label>
<input
v-model="config.plug_play_id"
type="text"
class="w-full px-3 py-2 rounded-lg border border-n-weak bg-n-solid-2 text-n-slate-12 focus:border-n-brand focus:outline-none transition-colors text-sm"
placeholder="ID do cliente"
class="w-full px-3 py-2 text-sm rounded-md border border-n-weak bg-n-solid-2 text-n-slate-12 focus:outline-none focus:border-n-brand"
placeholder="Insira o ID de integração"
/>
</div>
<div>
<label
class="block text-xs font-medium text-n-slate-11 mb-1 uppercase tracking-wide"
>PLUG-PLAY-TOKEN</label
>
PLUG-PLAY-TOKEN
</label>
<div class="relative">
<input
v-model="config.plug_play_token"
@ -176,8 +181,8 @@ function formatJson(str) {
/>
<button
class="absolute right-3 top-1/2 -translate-y-1/2 text-n-slate-10 hover:text-n-slate-12"
@click="showToken = !showToken"
tabindex="-1"
@click="showToken = !showToken"
>
<i :class="showToken ? 'i-lucide-eye-off' : 'i-lucide-eye'" />
</button>
@ -195,7 +200,7 @@ function formatJson(str) {
<span
class="w-2 h-2 rounded-full"
:class="statusColor.replace('text-', 'bg-')"
></span>
/>
<span>
Status:
<span class="font-mono font-medium" :class="statusColor">{{
@ -214,9 +219,9 @@ function formatJson(str) {
Erro: {{ tool.last_test.error }}
</div>
</template>
<span v-else class="text-n-slate-9 italic"
>Ferramenta nunca testada</span
>
<span v-else class="text-n-slate-9 italic">
Ferramenta nunca testada
</span>
</div>
<div class="flex gap-2">
@ -259,10 +264,11 @@ function formatJson(str) {
</span>
</div>
<div class="bg-n-solid-3 p-0">
<!-- eslint-disable prettier/prettier -->
<pre
class="overflow-auto max-h-60 p-4 text-xs font-mono text-n-slate-11 whitespace-pre-wrap break-all"
>{{ formatJson(testResult.body || testResult.error) }}</pre
>
>{{ formatJson(testResult.body || testResult.error) }}</pre>
<!-- eslint-enable prettier/prettier -->
</div>
</div>
</div>

View File

@ -19,17 +19,18 @@ async function fetchTools() {
try {
const { data } = await JasmineAPI.getTools(props.inboxId);
console.log('[JasmineTools] Loaded:', data);
// console.log('[JasmineTools] Loaded:', data);
tools.value = data || [];
} catch (error) {
console.error('[JasmineTools] Error:', error);
// console.error('[JasmineTools] Error:', error);
useAlert('Erro ao carregar ferramentas');
} finally {
isLoading.value = false;
}
}
function handleUpdate(updatedToolData) {
function handleUpdate() {
// [INTENTIONAL] updatedToolData reserved for future optimistic updates.
fetchTools();
}
@ -39,6 +40,8 @@ onMounted(() => {
</script>
<template>
<!-- eslint-disable vue/no-bare-strings-in-template -->
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<div class="mx-auto max-w-screen-md px-5 py-8 font-inter">
<div class="flex flex-col gap-6">
<div class="mb-2">

View File

@ -1,7 +1,7 @@
<script setup>
import { ref, reactive, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import { useStoreGetters } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import JasmineAPI from 'dashboard/api/inbox/jasmine';
@ -11,7 +11,8 @@ import Button from 'dashboard/components-next/button/Button.vue';
import JasmineToolsTab from '../components/JasmineToolsTab.vue';
const route = useRoute();
const store = useStore();
// const store = useStore();
// [INTENTIONAL] store reserved for upcoming actions in this view.
const getters = useStoreGetters();
// State
@ -100,7 +101,10 @@ const createCollection = async () => {
};
const deleteCollection = async collectionId => {
if (!confirm('Delete this collection and all its documents?')) return;
// eslint-disable-next-line no-alert
// eslint-disable-next-line no-alert
// eslint-disable-next-line no-alert
if (!window.confirm('Delete this collection and all its documents?')) return;
isDeletingCollection.value = collectionId;
try {
await JasmineAPI.deleteCollection(collectionId);
@ -113,16 +117,6 @@ const deleteCollection = async collectionId => {
}
};
const toggleCollection = async collection => {
if (expandedCollectionId.value === collection.id) {
expandedCollectionId.value = null;
documents.value = [];
return;
}
expandedCollectionId.value = collection.id;
await fetchDocuments(collection.id);
};
const fetchDocuments = async collectionId => {
isLoadingDocs.value = true;
try {
@ -135,6 +129,16 @@ const fetchDocuments = async collectionId => {
}
};
const toggleCollection = async collection => {
if (expandedCollectionId.value === collection.id) {
expandedCollectionId.value = null;
documents.value = [];
return;
}
expandedCollectionId.value = collection.id;
await fetchDocuments(collection.id);
};
const addDocument = async collectionId => {
if (!newDocContent.value.trim()) return;
isCreatingDoc.value = true;
@ -156,7 +160,10 @@ const addDocument = async collectionId => {
};
const deleteDocument = async (collectionId, docId) => {
if (!confirm('Delete this document?')) return;
// eslint-disable-next-line no-alert
// eslint-disable-next-line no-alert
// eslint-disable-next-line no-alert
if (!window.confirm('Delete this document?')) return;
isDeletingDoc.value = docId;
try {
await JasmineAPI.deleteDocument(collectionId, docId);
@ -176,11 +183,12 @@ const openEditDoc = doc => {
showEditDocModal.value = true;
};
const saveEditDoc = async () => {
// TODO: Implement update API when available
useAlert('Edit functionality coming soon');
showEditDocModal.value = false;
};
// const saveEditDoc = async () => {
// // [FUTURE] Placeholder until update API is available.
// // TODO: Implement update API when available
// useAlert('Edit functionality coming soon');
// showEditDocModal.value = false;
// };
const getStatusColor = status => {
const colors = {
@ -210,7 +218,7 @@ const fetchConfig = async () => {
};
} catch (error) {
// Config may not exist yet, use defaults
console.log('No config found, using defaults');
// console.log('No config found, using defaults');
} finally {
isLoadingConfig.value = false;
}
@ -241,6 +249,8 @@ onMounted(() => {
</script>
<template>
<!-- eslint-disable vue/no-bare-strings-in-template -->
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<SettingsLayout :is-loading="false">
<template #header>
<BaseSettingsHeader
@ -265,8 +275,8 @@ onMounted(() => {
<button
v-for="tab in tabs"
:key="tab.key"
class="px-4 py-2 text-sm font-medium rounded-md transition-colors"
:class="[
'px-4 py-2 text-sm font-medium rounded-md transition-colors',
activeTab === tab.key
? 'bg-n-solid-1 text-n-slate-12 shadow-sm'
: 'text-n-slate-11 hover:text-n-slate-12',
@ -322,8 +332,8 @@ onMounted(() => {
</td>
<td class="py-4">
<span
class="px-2 py-1 text-xs font-medium rounded uppercase"
:class="[
'px-2 py-1 text-xs font-medium rounded uppercase',
collection.visibility === 'private'
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
@ -403,10 +413,8 @@ onMounted(() => {
</div>
<div class="flex items-center gap-2 shrink-0">
<span
:class="[
'px-2 py-0.5 text-xs font-medium rounded-full',
getStatusColor(doc.status),
]"
class="px-2 py-0.5 text-xs font-medium rounded-full"
:class="getStatusColor(doc.status)"
>
{{ doc.status || 'pending' }}
</span>
@ -517,18 +525,18 @@ onMounted(() => {
<div>
<div class="flex items-center justify-between mb-2">
<div>
<label class="block text-sm font-medium text-n-slate-12"
>System Prompt</label
>
<label class="block text-sm font-medium text-n-slate-12">
System Prompt
</label>
<p class="text-xs text-n-slate-11">
Identidade e regras gerais da Jasmine
</p>
</div>
<Button
type="button"
v-tooltip="
expandedFields.system_prompt ? 'Recolher' : 'Expandir'
"
type="button"
:icon="
expandedFields.system_prompt
? 'i-lucide-minimize-2'
@ -542,10 +550,8 @@ onMounted(() => {
</div>
<textarea
v-model="config.system_prompt"
:class="[
'w-full px-3 py-2 text-sm rounded-lg border border-n-weak bg-n-solid-1 text-n-slate-12 resize-none font-mono transition-all duration-300',
expandedFields.system_prompt ? 'h-[600px]' : 'h-32',
]"
class="w-full px-3 py-2 text-sm rounded-lg border border-n-weak bg-n-solid-1 text-n-slate-12 resize-none font-mono transition-all duration-300"
:class="expandedFields.system_prompt ? 'h-[600px]' : 'h-32'"
placeholder="Você é Jasmine, uma assistente virtual da [Empresa]..."
/>
</div>
@ -554,18 +560,18 @@ onMounted(() => {
<div>
<div class="flex items-center justify-between mb-2">
<div>
<label class="block text-sm font-medium text-n-slate-12"
>Playbook SDR</label
>
<label class="block text-sm font-medium text-n-slate-12">
Playbook SDR
</label>
<p class="text-xs text-n-slate-11">
Script de vendas e tratamento de objeções
</p>
</div>
<Button
type="button"
v-tooltip="
expandedFields.playbook_prompt ? 'Recolher' : 'Expandir'
"
type="button"
:icon="
expandedFields.playbook_prompt
? 'i-lucide-minimize-2'
@ -579,10 +585,8 @@ onMounted(() => {
</div>
<textarea
v-model="config.playbook_prompt"
:class="[
'w-full px-3 py-2 text-sm rounded-lg border border-n-weak bg-n-solid-1 text-n-slate-12 resize-none font-mono transition-all duration-300',
expandedFields.playbook_prompt ? 'h-[600px]' : 'h-48',
]"
class="w-full px-3 py-2 text-sm rounded-lg border border-n-weak bg-n-solid-1 text-n-slate-12 resize-none font-mono transition-all duration-300"
:class="expandedFields.playbook_prompt ? 'h-[600px]' : 'h-48'"
placeholder="## Objetivo\nQualificar leads...\n\n## Perguntas...\n\n## Objeções..."
/>
</div>
@ -590,9 +594,9 @@ onMounted(() => {
<!-- Model Settings -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-n-slate-12 mb-2"
>Modelo LLM</label
>
<label class="block text-sm font-medium text-n-slate-12 mb-2">
Modelo LLM
</label>
<select
v-model="config.model"
class="w-full px-3 py-2 text-sm rounded-lg border border-n-weak bg-n-solid-1 text-n-slate-12"
@ -608,9 +612,9 @@ onMounted(() => {
</select>
</div>
<div>
<label class="block text-sm font-medium text-n-slate-12 mb-2"
>Temperatura: {{ config.temperature }}</label
>
<label class="block text-sm font-medium text-n-slate-12 mb-2">
Temperatura: {{ config.temperature }}
</label>
<input
v-model.number="config.temperature"
type="range"
@ -628,10 +632,9 @@ onMounted(() => {
<!-- RAG Settings -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-n-slate-12 mb-2"
>Threshold de Distância:
{{ config.rag_distance_threshold }}</label
>
<label class="block text-sm font-medium text-n-slate-12 mb-2">
Threshold de Distância: {{ config.rag_distance_threshold }}
</label>
<input
v-model.number="config.rag_distance_threshold"
type="range"
@ -645,9 +648,9 @@ onMounted(() => {
</p>
</div>
<div>
<label class="block text-sm font-medium text-n-slate-12 mb-2"
>Máx. Resultados RAG</label
>
<label class="block text-sm font-medium text-n-slate-12 mb-2">
Máx. Resultados RAG
</label>
<input
v-model.number="config.rag_max_results"
type="number"
@ -717,10 +720,7 @@ onMounted(() => {
v-model:show="showEditDocModal"
:on-close="() => (showEditDocModal = false)"
>
<div
class="flex flex-col h-auto overflow-auto p-6"
style="min-width: 500px"
>
<div class="flex flex-col h-auto overflow-auto p-6 min-w-[500px]">
<h3 class="text-lg font-semibold mb-4 text-n-slate-12">
Visualizar Documento
</h3>

View File

@ -88,9 +88,9 @@ export default {
selectedTabIndex: 0,
selectedPortalSlug: '',
showBusinessNameInput: false,
healthData: null,
isLoadingHealth: false,
healthError: null,
messageSignatureEnabled: false,
};
},
computed: {
@ -431,6 +431,7 @@ export default {
this.selectedPortalSlug = this.inbox.help_center
? this.inbox.help_center.slug
: '';
this.messageSignatureEnabled = this.inbox.message_signature_enabled;
// Set initial tab after inbox data is loaded
this.setTabFromRouteParam();
@ -453,6 +454,7 @@ export default {
lock_to_single_conversation: this.locktoSingleConversation,
sender_name_type: this.senderNameType,
business_name: this.businessName || null,
message_signature_enabled: this.messageSignatureEnabled,
channel: {
widget_color: this.inbox.widget_color,
website_url: this.channelWebsiteUrl,
@ -598,6 +600,17 @@ export default {
@blur="v$.selectedInboxName.$touch"
/>
<InboxAutoResolve :inbox="inbox" class="mb-4" />
<div class="flex flex-row items-center gap-2 mb-4">
<input
id="messageSignatureEnabled"
v-model="messageSignatureEnabled"
type="checkbox"
@change="updateInbox"
/>
<label for="messageSignatureEnabled">
{{ $t('INBOX_MGMT.ADD.MESSAGE_SIGNATURE.LABEL') }}
</label>
</div>
<woot-input
v-if="isAPIInbox"
v-model="webhookUrl"

View File

@ -4,7 +4,8 @@ import { createStore } from '../storeFactory';
export default createStore({
name: 'CaptainAssistant',
API: CaptainAssistantAPI,
actions: (mutationTypes) => ({
actions: () => ({
// [INTENTIONAL] mutationTypes kept for storeFactory contract compatibility.
fetchTools: async (_, { assistantId }) => {
try {
const { data } = await CaptainAssistantAPI.getTools(assistantId);

View File

@ -13,9 +13,17 @@ module Jasmine
config = inbox.jasmine_inbox_config
# Double-check conditions (in case they changed since job was enqueued)
Rails.logger.info "[Jasmine::ResponseJob] Started for Message #{message_id}, Channel Class: #{inbox.channel.class.name}"
return unless config&.is_enabled?
return if conversation.assignee.present?
# Send typing indicator
inbox.channel.toggle_typing_status('typing_on', conversation: conversation)
begin
# Sleep for verification (optimized to 1.5s per recommendation)
sleep 1.5
# Get response from BrainService
response_text = BrainService.new(
inbox: inbox,
@ -27,6 +35,12 @@ module Jasmine
# Send response as outgoing message
send_response(conversation, response_text)
ensure
# Ensure typing is turned off even if errors occur or no response
# Wait a bit to ensure the message "send" signal propagates before sending "paused"
sleep 0.5
inbox.channel.toggle_typing_status('typing_off', conversation: conversation)
end
end
private

View File

@ -17,22 +17,38 @@ class JasmineListener < BaseListener
def should_respond?(message)
# Only respond to incoming messages from customers
return false unless message.incoming?
return false if message.private?
unless message.incoming?
Rails.logger.info "[JasmineListener] Skipping: Message #{message.id} is not incoming"
return false
end
if message.private?
Rails.logger.info "[JasmineListener] Skipping: Message #{message.id} is private"
return false
end
inbox = message.inbox
config = inbox.jasmine_inbox_config
# Check if Jasmine is enabled for this inbox
return false unless config&.is_enabled?
unless config&.is_enabled?
Rails.logger.info "[JasmineListener] Skipping: Jasmine disabled for inbox #{inbox.id}"
return false
end
# Don't respond if conversation has a human agent assigned
conversation = message.conversation
return false if conversation.assignee.present?
if conversation.assignee.present?
Rails.logger.info "[JasmineListener] Skipping: Conversation #{conversation.id} has assignee #{conversation.assignee.id}"
return false
end
# Don't respond if there's an active agent bot (avoid conflicts)
return false if inbox.active_bot?
if inbox.active_bot?
Rails.logger.info "[JasmineListener] Skipping: Inbox #{inbox.id} has active_bot"
return false
end
Rails.logger.info "[JasmineListener] Validation Passed: Enqueueing ResponseJob for #{message.id}"
true
end

View File

@ -107,9 +107,22 @@ class Channel::Whatsapp < ApplicationRecord
def toggle_typing_status(typing_status, conversation:)
return unless provider_service.respond_to?(:toggle_typing_status)
recipient_id = conversation.contact.identifier || conversation.contact.phone_number
last_message = conversation.messages.last
provider_service.toggle_typing_status(typing_status, last_message: last_message, recipient_id: recipient_id)
identifier = conversation.contact.identifier
phone_number = conversation.contact.phone_number
recipient_id = identifier || phone_number
# Debug Log
Rails.logger.info "[Typing] recipient_id=#{recipient_id.inspect} identifier=#{identifier.inspect} phone=#{phone_number.inspect}"
# Validation: Ensure recipient_id is E164 compliant (digits only, maybe +).
# If identifier is something like x@lid, we should fallback to phone_number.
# Using suggested regex: \A\+?\d{10,15}\z
unless recipient_id.to_s.gsub(/[\+\s\-\(\)]/, '').match?(/\A\d{10,15}\z/)
Rails.logger.warn "[Typing] Invalid recipient_id format (#{recipient_id}). Falling back to phone_number: #{phone_number}"
recipient_id = phone_number
end
provider_service.toggle_typing_status(typing_status, last_message: nil, recipient_id: recipient_id)
end
def update_presence(status)

View File

@ -18,6 +18,7 @@
# greeting_enabled :boolean default(FALSE)
# greeting_message :string
# lock_to_single_conversation :boolean default(FALSE), not null
# message_signature_enabled :boolean
# name :string not null
# out_of_office_message :string
# sender_name_type :integer default("friendly"), not null

View File

@ -21,6 +21,11 @@ class Conversations::TypingStatusManager
when 'off'
trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private])
end
channel = conversation.inbox.channel
return unless channel.respond_to?(:toggle_typing_status)
channel.toggle_typing_status(params[:typing_status], conversation: conversation)
# Return the head :ok response from the controller
end
end

View File

@ -29,7 +29,7 @@ module Jasmine
processed_chunk_ids = Set.new
# 2. Iterate Priority Groups (Waterfall)
priority_groups.keys.sort.reverse.each do |priority|
priority_groups.keys.sort.reverse_each do |priority|
collection_ids = priority_groups[priority].map(&:collection_id)
# Step 1: ANN/HNSW Candidate Retrieval
@ -75,8 +75,9 @@ module Jasmine
# or trust the order.
# Better approach: Select distance in the query.
# [FUTURE] Placeholder until distance select is wired into filtering.
# Enhanced query with distance
candidates_with_dist = candidates.select(
candidates.select(
"jasmine_document_chunks.*, (embedding <=> '#{to_pg_vector(candidates.first&.embedding || [])}') as distance"
)
@ -87,8 +88,8 @@ module Jasmine
# Since we are iterating logic here, let's assume retrieve_candidates returns ActiveRecord::Relation.
# We'll map them to objects and filters.
results = []
doc_counts = Hash.new(0)
# [FUTURE] Reserved for threshold filtering output.
Hash.new(0)
# Calculate distances locally or re-fetch.
# Since we ordered by distance in DB, we rely on that order.
@ -101,8 +102,13 @@ module Jasmine
# Iterate, check threshold, check Max Chunks per Doc
candidates.each do |chunk|
dist = chunk.neighbor_distance(:embedding, @embedding_vector) rescue nil
# Note: neighbor gem might not expose distance easily without using its scopes.
# [FUTURE] Distance will gate threshold checks once wired up.
chunk.neighbor_distance(:embedding, @embedding_vector)
rescue StandardError
nil
# NOTE: neighbor gem might not expose distance easily without using its scopes.
# Fallback: Rely on DB order, but checking absolute threshold might be tricky without the value.
# Let's trust Neighbor gem's `nearest_neighbors` if possible, but we used raw SQL order.
@ -129,7 +135,7 @@ module Jasmine
Jasmine::DocumentChunk
.where(collection_id: collection_ids)
.select("jasmine_document_chunks.*, (embedding <=> '#{query_embedding}') as distance")
.order("distance ASC")
.order('distance ASC')
.limit(CANDIDATES_PER_PRIORITY)
end

View File

@ -3,23 +3,30 @@ module Whatsapp
def perform
parser = Whatsapp::Providers::Wuzapi::PayloadParser.new(params)
# 1. V1 Scope: Ignore Groups
# 1. Message Type Check (V1: Text + Presence)
# Fail fast for unsupported types (like ReadReceipts)
return unless [:text, :chat_presence].include?(parser.message_type)
# 2. V1 Scope: Ignore Groups
if parser.group_message?
Rails.logger.info "WuzAPI: Ignoring group message (ID: #{parser.external_id})"
return
end
# 2. Strong Dedupe (Critical for Sync)
if Message.exists?(source_id: parser.external_id, inbox_id: inbox.id)
# 3. Strong Dedupe (Critical for Sync)
# Skip dedupe for ChatPresence as it doesn't have a unique ID
if parser.message_type != :chat_presence && parser.external_id.present? && Message.exists?(source_id: parser.external_id, inbox_id: inbox.id)
Rails.logger.info "WuzAPI: Ignoring duplicate message (ID: #{parser.external_id})"
return
end
# 3. Message Type Check (V1: Text Only)
return unless parser.message_type == :text
if parser.sender_phone_number.blank?
Rails.logger.warn "WuzAPI: Skipping processing for event with no valid phone (Type: #{parser.message_type})"
return
end
# 4. Process
Rails.logger.info "WuzAPI: Processing message from #{parser.sender_phone_number}"
Rails.logger.info "WuzAPI: Processing message from #{parser.sender_phone_number} (Type: #{parser.message_type})"
ActiveRecord::Base.transaction do
@contact = find_or_create_contact(parser)
Rails.logger.info "WuzAPI: Contact found/created: #{@contact.id}"
@ -27,6 +34,12 @@ module Whatsapp
@conversation = find_or_create_conversation(@contact)
Rails.logger.info "WuzAPI: Conversation found/created: #{@conversation.id}"
if parser.message_type == :chat_presence
status = parser.presence_state == 'composing' ? 'on' : 'off'
@conversation.toggle_typing_status(status)
return
end
message = create_message(parser, @conversation)
Rails.logger.info "WuzAPI: Message created: #{message.id}"
end

View File

@ -13,40 +13,53 @@ module Whatsapp
end
def from_me?
params.dig(:event, :Info, :IsFromMe)
is_api_from_me = params.dig(:event, :Info, :IsFromMe) || params.dig(:event, :IsFromMe)
# If Wuzapi says it's NOT from me, believe it.
return false unless is_api_from_me
# If Wuzapi says it IS from me, verify against the instance phone number (if available)
# This protects against false positives where incoming messages are flagged as from_me
instance_phone = params['phone_number']
sender_jid = params.dig(:event, :Info, :Sender) || params.dig(:event, :Sender)
# If we have both numbers, double check
if instance_phone.present? && sender_jid.present?
sender_phone = sender_jid.split('@').first
# If sender is NOT the instance, it CANNOT be from me.
return false if sender_phone != instance_phone
end
true
end
def message_type
return :chat_presence if params['type'] == 'ChatPresence'
type = params.dig(:event, :Info, :Type)
type == 'text' ? :text : :unknown
end
def presence_state
params.dig(:event, :State)
end
def text_content
params.dig(:event, :Message, :conversation)
end
def sender_phone_number
jid = if from_me?
params.dig(:event, :Info, :Chat)
else
sender = params.dig(:event, :Info, :Sender)
sender_alt = params.dig(:event, :Info, :SenderAlt)
jid = extract_jid
# Reject LIDs as they aren't valid E164 phone numbers
return nil if jid.blank? || jid.include?('@lid')
# Prefer @s.whatsapp.net over @lid
if sender&.include?('@s.whatsapp.net')
sender
elsif sender_alt&.include?('@s.whatsapp.net')
sender_alt
else
sender
end
end
# Format: 556182098580@s.whatsapp.net -> 556182098580
jid&.split('@')&.first
jid.split('@').first
end
def timestamp
timestamp_val = params.dig(:event, :Info, :Timestamp)
timestamp_val = params.dig(:event, :Info, :Timestamp) || params.dig(:event, :Timestamp)
return Time.current if timestamp_val.blank?
begin
@ -57,11 +70,31 @@ module Whatsapp
end
def push_name
params.dig(:event, :Info, :PushName)
params.dig(:event, :Info, :PushName) || params.dig(:event, :PushName)
end
def group_message?
params.dig(:event, :Info, :IsGroup)
params.dig(:event, :Info, :IsGroup) || params.dig(:event, :IsGroup)
end
private
def extract_jid
if from_me?
params.dig(:event, :Info, :Chat) || params.dig(:event, :Chat)
else
sender = params.dig(:event, :Info, :Sender) || params.dig(:event, :Sender)
sender_alt = params.dig(:event, :Info, :SenderAlt) || params.dig(:event, :SenderAlt)
# Prefer @s.whatsapp.net over @lid
if sender&.include?('@s.whatsapp.net')
sender
elsif sender_alt&.include?('@s.whatsapp.net')
sender_alt
else
sender
end
end
end
end
end

View File

@ -3,7 +3,7 @@ module Whatsapp::Providers
attr_reader :whatsapp_channel
def initialize(whatsapp_channel:)
@whatsapp_channel = whatsapp_channel
super(whatsapp_channel: whatsapp_channel)
@base_url = whatsapp_channel.provider_config['wuzapi_base_url']
end
@ -55,6 +55,10 @@ module Whatsapp::Providers
if use_me_prefix
normalized_phone = "me:#{normalized_phone}" unless normalized_phone.start_with?('me:')
message_id = "me:#{message_id}" if message_id.present? && !message_id.start_with?('me:')
else
# Enforce JID format for customer numbers
clean_number = normalized_phone.split('@').first
normalized_phone = "#{clean_number}@s.whatsapp.net"
end
Rails.logger.info "[WuzapiService] Attempting reaction: phone=#{normalized_phone}, msg_id=#{message_id}, emoji=#{reaction_emoji}"
@ -98,6 +102,33 @@ module Whatsapp::Providers
end
end
def toggle_typing_status(typing_status, recipient_id: nil, **_kwargs)
# typing_status: 'typing_on', 'typing_off'
# Wuzapi expects: 'composing', 'paused'
state = %w[typing_on on].include?(typing_status) ? 'composing' : 'paused'
user_token = whatsapp_channel.wuzapi_user_token
phone_number = recipient_id || whatsapp_channel.phone_number
# Clean phone number (digits only)
normalized_phone = phone_number.to_s.gsub(/[\+\s\-\(\)]/, '')
# Enforce JID format: 5561...@s.whatsapp.net
# Strip any existing suffix (like @lid or even @s.whatsapp.net to be safe) and append standard one.
clean_number = normalized_phone.split('@').first
jid = "#{clean_number}@s.whatsapp.net"
Rails.logger.info "[WuzapiService] toggle_typing_status: Sending presence to #{jid} (raw: #{normalized_phone}), state: #{state}, token_present: #{user_token.present?}"
begin
# Use JID in the 'Phone' field as confirmed by manual tests (Test C)
response = client.send_chat_presence(user_token, jid, state)
Rails.logger.info "[WuzapiService] toggle_typing_status response: #{response}"
rescue StandardError => e
Rails.logger.warn "Wuzapi: Failed to send typing status: #{e.message}"
end
end
private
def client

View File

@ -21,7 +21,7 @@ module Wuzapi
}
end
def setup_webhook(user_token, inbox_id, webhook_secret)
def setup_webhook(user_token, inbox_id, _webhook_secret) # [INTENTIONAL] reserved for signed webhooks
# Host logic needs to come from GlobalConfig or Rails.application.routes
# Ideally passed in or resolved.
base_host = ENV.fetch('FRONTEND_URL', 'http://localhost:3000')

View File

@ -19,6 +19,7 @@ json.allow_messages_after_resolved resource.allow_messages_after_resolved
json.lock_to_single_conversation resource.lock_to_single_conversation
json.sender_name_type resource.sender_name_type
json.business_name resource.business_name
json.message_signature_enabled resource.message_signature_enabled
if resource.portal.present?
json.help_center do

10
cleanup_proposal.md Normal file
View File

@ -0,0 +1,10 @@
# Removal Proposal
Please mark [x] to approve specific removals.
| Approve | File | Type | Item | Reason |
| :-----: | :-- | :-- | :-- | :-- |
| [ ] | `app/services/jasmine/semantic_search_service.rb` | Local Var | `candidates_with_dist` | Marked SAFE in audit; currently tagged `[FUTURE]` so removal should wait until tag removed. |
| [ ] | `app/services/jasmine/semantic_search_service.rb` | Local Var | `dist` | Marked SAFE in audit; currently tagged `[FUTURE]` so removal should wait until tag removed. |
| [ ] | `app/javascript/dashboard/routes/dashboard/jasmine/components/JasmineToolCard.vue` | Param | `newVal` | Marked SAFE in audit; currently tagged `[INTENTIONAL]` so removal should wait until tag removed. |
| [ ] | `app/javascript/dashboard/routes/dashboard/jasmine/components/JasmineToolsTab.vue` | Param | `updatedToolData` | Marked SAFE in audit; currently tagged `[INTENTIONAL]` so removal should wait until tag removed. |

View File

@ -44,7 +44,7 @@ class FixStatusSuitesHeaders < ActiveRecord::Migration[7.1]
else
puts ' No keys found in URL query params. Manual update might be required for values.'
end
rescue URI::InvalidURIError => e
rescue URI::InvalidURIError # [INTENTIONAL] keep for future logging
puts " Skipping invalid URI: #{tool.endpoint_url}"
end
end

View File

@ -0,0 +1,5 @@
class AddMessageSignatureEnabledToInboxes < ActiveRecord::Migration[7.1]
def change
add_column :inboxes, :message_signature_enabled, :boolean
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2026_01_19_150720) do
ActiveRecord::Schema[7.1].define(version: 2026_01_20_141736) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@ -1166,6 +1166,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_19_150720) do
t.string "business_name"
t.jsonb "csat_config", default: {}, null: false
t.integer "auto_resolve_duration"
t.boolean "message_signature_enabled"
t.index ["account_id"], name: "index_inboxes_on_account_id"
t.index ["channel_id", "channel_type"], name: "index_inboxes_on_channel_id_and_channel_type"
t.index ["portal_id"], name: "index_inboxes_on_portal_id"

138
docs/dependency_notes.md Normal file
View File

@ -0,0 +1,138 @@
# Dependency Notes
Static string-scan notes from audit. These are *not* removals; verify runtime usage, initializers, rake tasks, and dynamic loading before acting.
## JavaScript Packages (package.json)
| Package | Version | Scope | Note |
| :-- | :-- | :-- | :-- |
| `@formkit/core` | `^1.6.7` | `dependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@iconify-json/logos` | `^1.2.3` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@iconify-json/lucide` | `^1.2.68` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@iconify-json/material-symbols` | `^1.2.10` | `dependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@iconify-json/ph` | `^1.2.1` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@iconify-json/ri` | `^1.2.3` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@iconify-json/teenyicons` | `^1.2.1` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@intlify/eslint-plugin-vue-i18n` | `^3.2.0` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@radix-ui/react-slot` | `^1.2.4` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@size-limit/file` | `^8.2.4` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@types/canvas-confetti` | `^1.9.0` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@types/react` | `^19.2.8` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@types/react-dom` | `^19.2.3` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@vitest/coverage-v8` | `3.0.5` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `@vue/compiler-sfc` | `^3.5.8` | `dependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `class-variance-authority` | `^0.7.1` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `clsx` | `^2.1.1` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `core-js` | `3.38.1` | `dependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `eslint-config-airbnb-base` | `15.0.0` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `eslint-config-prettier` | `^9.1.0` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `eslint-interactive` | `^11.1.0` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `eslint-plugin-html` | `7.1.0` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `eslint-plugin-import` | `2.30.0` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `eslint-plugin-prettier` | `5.2.1` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `eslint-plugin-vitest-globals` | `^1.5.0` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `eslint-plugin-vue` | `^9.28.0` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `html2canvas` | `^1.4.1` | `dependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `husky` | `^7.0.0` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `lint-staged` | `^16.2.7` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `opus-recorder` | `^8.0.5` | `dependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `prettier` | `^3.3.3` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `react-dom` | `^19.2.3` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `size-limit` | `^8.2.4` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `tailwind-merge` | `^3.4.0` | `devDependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `video.js` | `7.18.1` | `dependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `videojs-record` | `4.5.0` | `dependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
| `videojs-wavesurfer` | `3.8.0` | `dependencies` | Not referenced in app/assets/config string scan; verify build tooling and dynamic imports. |
## Ruby Gems (Gemfile)
| Gem | Version | Note |
| :-- | :-- | :-- |
| `active_record_query_trace` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `activerecord-import` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `administrate-field-belongs_to_search` | `>= 0.9.0` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `ai-agents` | `>= 0.7.0` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `annotaterb` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `attr_extras` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `aws-actionmailbox-ses` | `~> 0` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `aws-sdk-s3` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `azure-storage-blob` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `barnes` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `brakeman` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `bundle-audit` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `byebug` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `climate_control` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `csv-safe` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `database_cleaner` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `devise-secure_password` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `devise-two-factor` | `>= 5.0.0` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `dotenv-rails` | `>= 3.0.0` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `email-provider-info` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `email_reply_trimmer` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `factory_bot_rails` | `>= 6.4.3` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `faraday_middleware-aws-sigv4` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `flag_shih_tzu` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `foreman` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `gmail_xoauth` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `google-cloud-dialogflow-v2` | `>= 0.24.0` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `google-cloud-storage` | `>= 1.48.0` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `google-cloud-translate-v3` | `>= 0.7.0` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `groupdate` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `grpc` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `haikunator` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `hairtrigger` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `hashie` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `html2text` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `image_processing` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `iso-639` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `json_schemer` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `kaminari` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `line-bot-api` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `maxminddb` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `meta_request` | `>= 0.8.3` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `mock_redis` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `net-smtp` | `~> 0.3.4` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `omniauth-google-oauth2` | `>= 1.1.3` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `omniauth-rails_csrf_protection` | `~> 1.0` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `omniauth-saml` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `opensearch-ruby` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `opentelemetry-exporter-otlp` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `opentelemetry-sdk` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `pdf-reader` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `procore-sift` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `pry-rails` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `redis-namespace` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `responders` | `>= 3.1.1` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `reverse_markdown` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `rqrcode` | `~> 3.2` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `rspec-rails` | `>= 6.1.5` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `rspec_junit_formatter` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `rubocop-factory_bot` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `rubocop-performance` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `rubocop-rails` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `rubocop-rspec` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `ruby-openai` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `ruby_llm-schema` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `scss_lint` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `seed_dump` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `shopify_api` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `shoulda-matchers` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `sidekiq_alive` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `simplecov` | `>= 0.21` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `simplecov_json_formatter` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `spring-watcher-listen` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `squasher` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `stackprof` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `telephone_number` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `test-prof` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `tidewave` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `time_diff` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `twilio-ruby` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `twitty` | `~> 0.1.5` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `tzinfo-data` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `valid_email2` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `vite_rails` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `web-console` | `>= 4.2.1` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `web-push` | `>= 3.0.1` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `webmock` | `unspecified` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |
| `wisper` | `2.0.0` | Not referenced in app/lib/config string scan; verify initializers, tasks, and runtime hooks. |

View File

@ -0,0 +1,324 @@
---
title: Wuzapi API Reference
source: https://meow1001.innova1001.com.br/api/spec.yml
captured_at: 2026-01-19
draft: false
---
# WUZAPI API Reference
> **Note**: This documentation is derived from the official [spec.yml](https://meow1001.innova1001.com.br/api/spec.yml) (OpenAPI 3.0.0).
## Authentication
- **Standard Endpoints**: Include the `token` header with a valid user token (matches tokens stored in the `users` database table).
- **Admin Endpoints**: Use the `Authorization` header with the admin token (set in `.env` as `WUZAPI_ADMIN_TOKEN`).
---
## 🚀 Messages
### Send Text Message
`POST /chat/send/text`
Sends a text message. `ContextInfo` is optional and used when replying to a message.
**Request Body:**
```json
{
"Phone": "5511999999999",
"Body": "Hello, how are you?",
"Id": "optional-custom-id",
"ContextInfo": {
"StanzaId": "message-id-to-reply-to",
"Participant": "5511999999999@s.whatsapp.net"
}
}
```
### Send Image
`POST /chat/send/image`
Sends an image message. Image must be Base64 encoded (JPEG/PNG).
**Request Body:**
```json
{
"Phone": "5511999999999",
"Image": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
"Caption": "Check this out!",
"Id": "optional-custom-id"
}
```
### Send Audio
`POST /chat/send/audio`
Sends an audio message (PTT/Voice Note). Audio must be Base64 encoded OGG/Opus.
**Request Body:**
```json
{
"Phone": "5511999999999",
"Audio": "data:audio/ogg;base64,T2dnUw...",
"Id": "optional-custom-id"
}
```
### Send Video
`POST /chat/send/video`
Sends a video message. Video must be Base64 encoded MP4.
**Request Body:**
```json
{
"Phone": "5511999999999",
"Video": "data:video/mp4;base64,AAAA...",
"Caption": "My video",
"Id": "optional-custom-id"
}
```
### Send Document
`POST /chat/send/document`
Sends a generic document/file.
**Request Body:**
```json
{
"Phone": "5511999999999",
"Document": "data:application/pdf;base64,JVBER...",
"FileName": "invoice.pdf",
"Id": "optional-custom-id"
}
```
---
## 💬 Chat Actions
### Set Chat Presence
`POST /chat/presence`
Sets the typing or recording status (e.g., "typing...", "recording audio...").
**Request Body:**
```json
{
"Phone": "5511999999999",
"State": "composing",
"Media": "audio"
}
```
- `State`: `composing` (typing) or `paused`.
- `Media`: `audio` (optional, indicates "recording audio").
### React to Message
`POST /chat/react`
Reacts to a specific message with an emoji.
**Request Body:**
```json
{
"Phone": "5511999999999",
"Body": "❤️",
"Id": "message-id-to-react-to"
}
```
- To react to your _own_ message, prefix the `Id` with `me:` (e.g., `me:ABC12345`).
### Mark as Read
`POST /chat/markread`
Marks messages as read.
**Request Body:**
```json
{
"Id": ["msg-id-1", "msg-id-2"],
"Chat": "5511999999999@s.whatsapp.net"
}
```
---
## 👥 Groups
### Create Group
`POST /group/create`
Creates a new WhatsApp group.
**Request Body:**
```json
{
"Name": "My New Group",
"Participants": ["5511999999999", "5511888888888"]
}
```
### Get Group List
`GET /group/list`
Returns a list of all groups the connected number is a member of.
### Get Group Info
`GET /group/info?GroupJID=123456789@g.us`
Returns metadata and participants for a specific group.
### Update Participants
`POST /group/updateparticipants`
Add, remove, promote, or demote participants.
**Request Body:**
```json
{
"GroupJID": "123456789@g.us",
"Participants": ["5511999999999"],
"Action": "add"
}
```
- `Action`: `add`, `remove`, `promote`, `demote`.
### Leave Group
`POST /group/leave`
**Request Body:**
```json
{
"GroupJID": "123456789@g.us"
}
```
---
## 🔌 Session & Connection
### Connect / QR Code
`POST /session/connect`
Initiates the connection. If not connected, generates a QR code.
**Request Body:**
```json
{
"Subscribe": ["Message", "ReadReceipt", "Presence"],
"Immediate": false
}
```
### Get Session Status
`GET /session/status`
Returns connection health.
```json
{
"data": {
"Connected": true,
"LoggedIn": true
}
}
```
### Logout
`POST /session/logout`
Disconnects and clears the session files.
---
## 🎣 Webhooks
Configure where Wuzapi POSTs incoming events (messages, status updates).
### Set Webhook
`POST /webhook`
**Request Body:**
```json
{
"webhook": "https://your-server.com/webhooks/wuzapi",
"events": ["Message", "ReadReceipt", "Presence", "HistorySync"]
}
```
_(Common event types: `Message`, `ReadReceipt`, `Presence`, `ChatPresence`)_
### Get Webhook Config
`GET /webhook`
---
## 👤 User & Contacts
### Check Phones (Exist on WhatsApp?)
`POST /user/check`
**Request Body:**
```json
{
"Phone": ["5511999999999", "5511888888888"]
}
```
### Get User Info
`POST /user/info`
Gets status message, profile picture ID, etc.
**Request Body:**
```json
{
"Phone": ["5511999999999"]
}
```
### Get Contacts
`GET /user/contacts`
Returns a list of all saved contacts synced from the phone.

View File

@ -95,7 +95,6 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
end
def process_response
trigger_typing_status('off')
handled = if @response['handoff_trigger'].present?
apply_handoff_behavior(@response['handoff_trigger'])
elsif handoff_requested?
@ -106,6 +105,7 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
return if handled
humanized_delay(@response['response'])
trigger_typing_status('off')
create_messages
Rails.logger.info("[CAPTAIN][ResponseBuilderJob] Incrementing response usage for #{account.id}")
account.increment_response_usage

View File

@ -69,40 +69,54 @@ class Captain::Assistant::AgentRunnerService
text = message.to_s.strip.downcase
return nil if text.blank?
# [FUTURE] Placeholder for a lightweight thank-you detector.
# Simple substrings for thank you messages
# Using simple include? is more robust for "obrigado ...." cases where regex might fail on boundaries
thank_you_keywords = [
'obrigad', # catches obrigado, obrigada, obrigados
'valeu',
'agradeço',
'agradecid',
'muito obrigad',
'brigadao',
'brigadão',
'brigadinha',
'gratidao',
'gratidão',
'thanks'
]
# Check if message is ONLY emoji(s) (simple heuristic)
only_emoji = text.gsub(/[\s\p{Emoji}]/u, '').empty? && text.match?(/\p{Emoji}/u)
match_found = thank_you_keywords.any? { |kw| text.include?(kw) } || only_emoji
# Categories for context-aware reaction
keywords = {
thanks: %w[obrigad valeu agradeço grato thanks brigadao brigadão gratidao gratidão],
greeting: %w[oi olá ola bom dia boa tarde boa noite e ai eaí],
attention: %w[reserva pesquisar pesquisa busca buscar verificar checar olhada olho disponibilidade]
}
Rails.logger.info "[Captain V2] Reaction Pre-Check: Text='#{text}' Match=#{match_found}"
File.open('/tmp/v2_debug.log', 'a') { |f| f.puts "[#{Time.now}] AgentRunnerService: Text='#{text}' Match=#{match_found}" }
# Check for direct matches
matched_category = nil
if match_found
Rails.logger.info '[Captain V2] Detected thank you/emoji. Executing ReactToMessageTool directly.'
keywords.each do |category, words|
if words.any? { |w| text.include?(w) }
matched_category = category
break
end
end
# Fallback to thanks if only emoji (assuming positive sentiment)
matched_category = :thanks if matched_category.nil? && only_emoji
Rails.logger.info "[Captain V2] Reaction Pre-Check: Text='#{text}' Category=#{matched_category}"
File.open('/tmp/v2_debug.log', 'a') { |f| f.puts "[#{Time.now}] AgentRunnerService: Text='#{text}' Category=#{matched_category}" }
if matched_category
Rails.logger.info "[Captain V2] Detected #{matched_category}. Executing ReactToMessageTool directly."
emoji_map = {
thanks: ['❤️', '🙏', '🥰', '😍', '🤜🤛'],
greeting: ['😀', '👋', '🙂', '🤠', '🙋‍♂️', '🙋‍♀️'],
attention: ['👀', '🧐', '🕵️', '📝', '🔎']
}
selected_emoji = emoji_map[matched_category].sample || '❤️'
begin
tool = Captain::Tools::ReactToMessageTool.new(
assistant: @assistant,
@assistant,
user: @conversation.contact,
conversation: @conversation
)
tool.execute(emoji: '❤️')
tool.execute(emoji: selected_emoji)
rescue StandardError => e
Rails.logger.error "[Captain V2] Failed to execute ReactToMessageTool: #{e.message}"
# Fallback to normal flow if tool fails
@ -110,7 +124,7 @@ class Captain::Assistant::AgentRunnerService
end
return {
'response' => 'De nada! ❤️',
'response' => "De nada! #{selected_emoji}",
'reasoning' => 'Auto-reaction triggered by thank you/emoji detection',
'agent_name' => @assistant.name
}

View File

@ -23,8 +23,7 @@ module Captain
end
def initialize(assistant, user: nil, conversation: nil)
@conversation = conversation
super(assistant, user: user)
super(assistant, user: user, conversation: conversation)
end
def execute(*args, **params)
@ -35,11 +34,17 @@ module Captain
# Get the last incoming message from the customer
last_customer_message = @conversation.messages.incoming.last
return error_response('No customer message to react to') unless last_customer_message.present?
if last_customer_message.blank?
Rails.logger.warn "[ReactToMessageTool] Failure: No incoming message found for conversation #{@conversation.id}"
return error_response('No customer message to react to')
end
# Get the external message ID (source_id) - required for WhatsApp reactions
message_external_id = last_customer_message.source_id
return error_response('Message has no external ID for reaction') if message_external_id.blank?
if message_external_id.blank?
Rails.logger.warn "[ReactToMessageTool] Failure: Message #{last_customer_message.id} has no source_id"
return error_response('Message has no external ID for reaction')
end
Rails.logger.info "[ReactToMessageTool] Reacting to message #{last_customer_message.id} (source: #{message_external_id}) with #{emoji}"

View File

@ -14,7 +14,7 @@ class Llm::BaseAiService
setup_temperature
end
def chat(model: @model, temperature: @temperature, api_key: nil)
def chat(model: @model, temperature: @temperature, api_key: nil) # [INTENTIONAL] api_key reserved for per-request auth
client = RubyLLM.chat(model: model)
# client = client.with_api_key(api_key) if api_key.present?
client.with_temperature(temperature)

31
organization_report.md Normal file
View File

@ -0,0 +1,31 @@
# Organization Report: Unused Placeholders & Dependency Notes
## Changes Applied
| File | Change | Reason | Risk | Scope |
| :-- | :-- | :-- | :-- | :-- |
| `app/services/jasmine/semantic_search_service.rb` | Added `[FUTURE]` tags near placeholder variables | Clarify unused placeholders without removal | Low | Local |
| `app/services/wuzapi/provisioning_service.rb` | Added `[INTENTIONAL]` tag for unused argument | Preserve interface while documenting intent | Low | Local |
| `db/migrate/20260110193000_fix_status_suites_headers.rb` | Added `[INTENTIONAL]` tag for unused rescue variable | Clarify debug retention | Low | Local |
| `enterprise/app/services/captain/assistant/agent_runner_service.rb` | Added `[FUTURE]` tag for reserved keywords list | Clarify planned behavior | Low | Local |
| `enterprise/app/services/llm/base_ai_service.rb` | Added `[INTENTIONAL]` tag for reserved api_key | Clarify planned interface | Low | Local |
| `app/javascript/dashboard/routes/dashboard/jasmine/components/JasmineToolCard.vue` | Added `[INTENTIONAL]` tag for unused watcher param | Clarify placeholder | Low | Local |
| `app/javascript/dashboard/routes/dashboard/jasmine/components/JasmineToolsTab.vue` | Added `[INTENTIONAL]` tag for unused handler param | Clarify placeholder | Low | Local |
| `app/javascript/dashboard/routes/dashboard/jasmine/pages/JasmineInboxDashboard.vue` | Added `[INTENTIONAL]`/`[FUTURE]` tags | Clarify reserved store and edit flow | Low | Local |
| `app/javascript/dashboard/store/captain/assistant.js` | Added `[INTENTIONAL]` tag for factory arg | Document contract compatibility | Low | Local |
| `docs/dependency_notes.md` | Added dependency scan notes | Avoid JSON comments while documenting | Low | Docs |
## Skipped / Preserved
| File | Item | Reason | Risk | Scope |
| :-- | :-- | :-- | :-- | :-- |
| `app/services/jasmine/semantic_search_service.rb` | Remove unused locals | User requested no automatic removals | Low | Local |
| `app/javascript/dashboard/routes/dashboard/jasmine/components/JasmineToolCard.vue` | Remove unused watcher param | User requested no automatic removals | Low | Local |
| `app/javascript/dashboard/routes/dashboard/jasmine/components/JasmineToolsTab.vue` | Remove unused handler param | User requested no automatic removals | Low | Local |
| `app/javascript/dashboard/routes/dashboard/jasmine/pages/JasmineInboxDashboard.vue` | Remove unused store/saveEditDoc | User requested no automatic removals | Low | Local |
| `app/javascript/dashboard/store/captain/assistant.js` | Remove unused factory param | User requested no automatic removals | Low | Local |
| `package.json` | Comments | JSON does not support comments | Low | Config |
## Structural Suggestions (For Future)
- Consider adopting a dedicated unused-code scanner for Ruby (e.g., `debride`) and JS (e.g., `depcheck`) before removal.

View File

@ -0,0 +1,164 @@
# Resolução: Reações de Mensagem Wuzapi
Este documento detalha a implementação e correção das reações de mensagem automáticas do WhatsApp (Wuzapi). Siga este guia para restaurar a funcionalidade caso ela seja perdida ou quebrada.
## 1. Problemas e Diagnóstico
1. **Formatação do JID (WuzapiService)**: O Wuzapi rejeitava reações enviadas apenas com o número de telefone. Foi necessário impor o formato `<numero>@s.whatsapp.net`.
2. **Erro de Argumentos (AgentRunnerService)**: A instanciação da ferramenta `ReactToMessageTool` estava incorreta, gerando `ArgumentError`. O argumento `assistant` deve ser posicional.
3. **Perda de Contexto (ReactToMessageTool)**: O `initialize` da ferramenta não passava `conversation` para a classe pai, fazendo com que validações falhassem silenciosamente.
## 2. Implementação de Referência (Código para Restauração)
### A. Service do Provedor Wuzapi
**Arquivo**: `app/services/whatsapp/providers/wuzapi_service.rb`
**Método Crítico**: `send_reaction_message`
Certifique-se de que a lógica de formatação do JID esteja presente dentro do `else` do prefixo `me:`.
```ruby
def send_reaction_message(phone_number, message_id, reaction_emoji)
return if phone_number.blank? || message_id.blank?
# ... (lógica de seleção de canal)
use_me_prefix = phone_number == @whatsapp_channel.validate_provider_config['phone_number']
normalized_phone = phone_number
if use_me_prefix
normalized_phone = "me:#{normalized_phone}" unless normalized_phone.start_with?('me:')
message_id = "me:#{message_id}" if message_id.present? && !message_id.start_with?('me:')
else
# [FIX] Enforce JID format for customer numbers
clean_number = normalized_phone.split('@').first
normalized_phone = "#{clean_number}@s.whatsapp.net"
end
Rails.logger.info "[WuzapiService] Attempting reaction: phone=#{normalized_phone}, msg_id=#{message_id}, emoji=#{reaction_emoji}"
client.send_reaction(normalized_phone, reaction_emoji, message_id)
end
```
### B. Lógica de Decisão e Classificação (AgentRunnerService)
**Arquivo**: `enterprise/app/services/captain/assistant/agent_runner_service.rb`
**Método Crítico**: `check_and_react_to_message`
Este método contém a lógica de "curto-circuito" que detecta intenções (agradecimento, saudação, atenção) e reage diretamente antes de chamar a IA principal.
```ruby
def check_and_react_to_message(message)
text = message.to_s.strip.downcase
return nil if text.blank?
# 1. Helper simplificado de detecção (não regex complexo)
only_emoji = text.gsub(/[\s\p{Emoji}]/u, '').empty? && text.match?(/\p{Emoji}/u)
# 2. Categorias e Palavras-chave
keywords = {
thanks: %w[obrigad valeu agradeço grato thanks brigadao brigadão gratidao gratidão],
greeting: %w[oi olá ola bom dia boa tarde boa noite e ai eaí],
attention: %w[reserva pesquisar pesquisa busca buscar verificar checar olhada olho disponibilidade]
}
matched_category = nil
# 3. Classificação
keywords.each do |category, words|
if words.any? { |w| text.include?(w) }
matched_category = category
break
end
end
matched_category = :thanks if matched_category.nil? && only_emoji
if matched_category
Rails.logger.info "[Captain V2] Detected #{matched_category}. Executing ReactToMessageTool directly."
# 4. Mapa de Emojis por Contexto
emoji_map = {
thanks: ['❤️', '🙏', '🥰', '😍', '🤜🤛'],
greeting: ['😀', '👋', '🙂', '🤠', '🙋‍♂️', '🙋‍♀️'],
attention: ['👀', '🧐', '🕵️', '📝', '🔎']
}
selected_emoji = emoji_map[matched_category].sample || '❤️'
begin
# [FIX] Instanciação correta: assistant é POSICIONAL
tool = Captain::Tools::ReactToMessageTool.new(
@assistant,
user: @conversation.contact,
conversation: @conversation
)
tool.execute(emoji: selected_emoji)
rescue StandardError => e
Rails.logger.error "[Captain V2] Failed to execute ReactToMessageTool: #{e.message}"
return nil
end
return {
'response' => "De nada! #{selected_emoji}",
'reasoning' => "Auto-reaction triggered by #{matched_category} detection",
'agent_name' => @assistant.name
}
end
nil
end
```
### C. Ferramenta de Reação (ReactToMessageTool)
**Arquivo**: `enterprise/app/services/captain/tools/react_to_message_tool.rb`
**Correção Crítica**: Método `initialize` e `execute` com logs.
```ruby
class ReactToMessageTool < BaseTool
# ... (name, description, schema)
def initialize(assistant, user: nil, conversation: nil)
# [FIX] Repassar conversation para o super é CRUCIAL
super(assistant, user: user, conversation: conversation)
end
def execute(*args, **params)
actual_params = resolve_params(args, params)
emoji = actual_params[:emoji]
# Validações com logs de erro explícitos
unless @conversation.present?
Rails.logger.warn "[ReactToMessageTool] Failure: No conversation context"
return error_response('Conversation not found')
end
last_customer_message = @conversation.messages.incoming.last
if last_customer_message.blank?
Rails.logger.warn "[ReactToMessageTool] Failure: No incoming message"
return error_response('No customer message to react to')
end
message_external_id = last_customer_message.source_id
if message_external_id.blank?
Rails.logger.warn "[ReactToMessageTool] Failure: Message #{last_customer_message.id} has no source_id"
return error_response('Message has no external ID')
end
Rails.logger.info "[ReactToMessageTool] Reacting with #{emoji}"
create_reaction_message(last_customer_message, emoji, message_external_id)
{ success: true, message: "Reacted with #{emoji}" }.to_json
rescue StandardError => e
# ...
end
# ...
end
```
## 3. Passo a Passo de Recuperação
1. **Verifique os Logs**: Busque por `[Captain V2]`, `[ReactToMessageTool]` ou `[WuzapiService]` para identificar onde a cadeia quebrou.
2. **Restaure o Código**: Copie e cole os blocos de código acima nos respectivos arquivos.
3. **Teste**: Envie "Obrigado" ou "Oi" para o bot e observe se a reação ocorre e se o emoji corresponde ao contexto.

View File

@ -38,3 +38,55 @@ Mensagens enviadas para o número WhatsApp não estavam chegando na caixa de ent
- `app/controllers/webhooks/wuzapi_controller.rb`
- `task.md`, `walkthrough.md` (Documentação)
# Implementação de Presença Simulada (Typing Indicator) e Delay Humanizado
## Contexto e Objetivo
O objetivo era implementar uma experiência de chat mais "humana", onde o bot exibe o status "digitando..." (typing) enquanto processa a resposta e durante um período de delay artificial (humanized delay), antes de enviar a mensagem final.
## Mecanismo de Presença Simulada
O comportamento "humano" é alcançado mantendo o indicador de digitação ativo durante todo o ciclo de geração da resposta:
1. **Início do Processamento**: O Job (`ResponseBuilderJob`) ativa o status `typing_on`.
2. **Geração da IA**: O LLM processa a resposta (indicador continua ativo).
3. **Delay Humanizado**: Um delay calculado (baseado no tamanho da resposta) é executado.
- _Crítico_: O indicador deve permanecer ativo durante este `sleep`.
4. **Finalização**: O status é alterado para `typing_off` e a mensagem é enviada imediatamente em seguida.
## Diagnóstico e Correções
Para que este fluxo funcionasse no Wuzapi, corrigimos os seguintes pontos:
1. **Sincronia do Delay (Correção de UX)**:
- **Problema**: O código original desligava o indicador (`typing_off`) _antes_ de entrar no `humanized_delay`. Isso causava um "silêncio visual" (sem indicador) de ~5 segundos antes da mensagem aparecer.
- **Solução**: Movemos a chamada de `typing_off` para **após** a execução do delay.
2. **Incompatibilidade de Status ("Bug Raiz")**:
- **Problema**: A aplicação enviava o status simplificado `'on'`, mas o adaptador do Wuzapi esperava estritamente a string `'typing_on'`. Isso fazia o código falhar silenciosamente e enviar `'paused'`.
- **Solução**: O adaptador (`WuzapiService`) foi atualizado para aceitar tanto `'on'` quanto `'typing_on'`.
3. **Formato do JID (Protocolo WhatsApp)**:
- **Problema**: O envio de presença falhava se o número não tivesse o sufixo correto.
- **Solução**: Forçamos a formatação `<numero>@s.whatsapp.net` no envio para a API do Wuzapi.
4. **Crash na Validação de Webhook (Entrada)**:
- **Problema**: Webhooks de presença contendo IDs internos do WhatsApp (`@lid`) quebravam a validação de telefone da aplicação.
- **Solução**: O parser foi blindado para ignorar IDs inválidos sem travar o processamento.
## Arquivos Chave Alterados
- `enterprise/app/jobs/captain/conversation/response_builder_job.rb`: Ajuste na ordem do `humanized_delay`.
- `app/services/whatsapp/providers/wuzapi_service.rb`: Suporte a status `'on'` e formatação JID.
- `app/services/whatsapp/providers/wuzapi/payload_parser.rb`: Tratamento de IDs inválidos.
## Como Validar
1. Envie uma mensagem para o bot.
2. Observe que o status "digitando..." aparece quase imediatamente.
3. Note que o status **persiste** por alguns segundos (durante o delay).
4. A mensagem chega assim que o status some.

View File

@ -0,0 +1,112 @@
---
name: architecture-review
description: Reviews project architecture, folder structure, and code organization. Suggests pragmatic improvements without requiring full rewrites.
---
# Architecture Review Specialist
## Mission
Identify structural issues and suggest incremental improvements that make the codebase easier to understand, maintain, and extend.
## When to use
- When codebase feels disorganized
- Before adding major new features
- When onboarding new developers
- Quarterly architecture health check
## Review Dimensions
### 1. Folder Structure
- Proper separation of concerns
- Consistent naming conventions
- Logical grouping of related code
### 2. Layering
- Controllers should be thin
- Business logic in Services/Interactors
- Data access in Models/Repositories
- Clear boundaries between layers
### 3. Dependencies
- No circular dependencies
- Proper use of namespaces
- External integrations isolated
### 4. Code Patterns
- Consistent use of design patterns
- No God Objects (classes doing too much)
- Proper error handling strategy
## Workflow
- [ ] **Phase 1: Structure Analysis**
- [ ] Map current folder organization
- [ ] Identify misplaced files
- [ ] Check naming consistency
- [ ] **Phase 2: Responsibility Analysis**
- [ ] Fat controllers (>50 lines)
- [ ] Fat models (>200 lines)
- [ ] Missing service layer
- [ ] Business logic in views
- [ ] **Phase 3: Coupling Analysis**
- [ ] Circular dependencies
- [ ] High coupling between modules
- [ ] Missing abstraction layers
- [ ] **Phase 4: Recommendations**
- [ ] Create `architecture_improvements.md`
- [ ] Prioritize by impact vs effort
- [ ] Provide migration path
## Report Format
Output: `architecture_improvements.md`
```markdown
# Architecture Improvements Report
## Executive Summary
Brief overview of the codebase health and top 3 priorities.
## Priority 1: [Issue Name]
- **Issue**: Description of the structural problem.
- **Location**: `app/controllers/legacy_controller.rb`
- **Recommendation**: Exact steps to refactor.
- **Impact**: High (Critical for stability) / Medium (Tech debt) / Low (Nice to have)
## Detailed Findings
### Folder Structure
- [Observation 1]
- [Observation 2]
### Layering Violations
| Component | Violation | Proposed Fix |
| :---------------- | :----------------------------------- | :-------------------------- |
| `UsersController` | Contains 200 lines of business logic | specific service extraction |
### Dependency Issues
- [ ] Circular dependency detected between A and B.
## Roadmap
1. [Immediate Fixes]
2. [Short-term Refactoring]
3. [Long-term Restructuring]
```

View File

@ -0,0 +1,150 @@
---
name: auditing-code
description: Audits the project to identify unused code while strictly protecting entrypoints, integrations, and dynamic calls. Generates evidence-based risk reports and reversible cleanup plans.
---
# Code Audit & Cleanup Specialist
## Mission
To reduce technical debt and cognitive load for both humans and AI agents by identifying unused code, WITHOUT altering the system's behavior or risking stability. We prioritize safety, reversibility, and explicit evidence over aggressive cleanup.
## When to use this skill
- When the user requests a code audit, cleanup, or identification of "dead code".
- When evaluating legacy modules for refactoring.
- **Trigger phases:** `AUDIT` (Mapping), `ASSESS` (Risk Classification), `REPORT` (Evidence), `PLAN` (Cleanup Strategy).
## Workflow
Copy this checklist to `task.md`:
- [ ] **Phase 1: Protection & Mapping**
- [ ] Identify **Critical Entrypoints** (Routes, Jobs, Webhooks, CLI, AI Agents).
- [ ] Map potentially unused items (using `grep`, `find`, LSP).
- [ ] **Phase 2: Risk Assessment & Evidence**
- [ ] Classify strictly: **SAFE**, **CAUTION**, or **KEEP**.
- [ ] Collect usage evidence for every item (or lack thereof).
- [ ] Validate against "Dynamic Use" exclusions (Reflection, strings).
- [ ] **Phase 3: Reporting**
- [ ] Generate `audit_report.md` (Human/AI readable).
- [ ] Provide summary metrics.
- [ ] **Phase 4: Cleanup Strategy**
- [ ] Create `cleanup_plan.md` (Strategy: Deprecate -> Observe -> Remove).
- [ ] **WAIT** for explicit user approval.
## Instructions
### 1. Protection Rules (The "Red Lines")
**NEVER** classify as `SAFE` if the item matches these criteria. Must be `KEEP` or `CAUTION`.
| Category | Protection Rule | Trigger Pattern Examples |
| :------------- | :------------------------------------- | :----------------------------------------------- |
| **Routes/API** | Public controllers, API endpoints. | `routes.rb`, `*Controller`, `API::*` |
| **Async Jobs** | Background workers, schedulers. | `Sidekiq::Worker`, `ApplicationJob`, `cron.yaml` |
| **Webhooks** | External callbacks, event handlers. | `handle_webhook`, `on_*`, `stripe_event` |
| **CLI/Tasks** | Rake tasks, scripts, console commands. | `lib/tasks/*.rake`, `bin/*` |
| **Dynamic** | Feature flags, ENV-driven logic. | `ENV['FEATURE_*']`, `Features.enabled?` |
| **Meta-Prog** | Reflection/String-based calls. | `send(params[:method])`, `constantize` |
| **AI Agents** | Tools/Skills used by Agents. | `class *Tool`, `scenarios/*.yaml` |
### 2. Risk Classification Logic
Every item must be tagged with a risk level.
- **✅ SAFE**:
- Strictly internal (private methods, local vars).
- 0 references found in entire codebase (grep check).
- Not an entrypoint or potential meta-programming target.
- _Constraint_: Must verify removal doesn't break syntax.
- **⚠️ CAUTION**:
- Public methods with no _explicit_ callers.
- Constants/Classes that "look" like headers or strict types.
- CSS classes (could be constructed strings).
- _Requirement_: Needs implicit usage check.
- **❌ KEEP**:
- Valid references found.
- Any item executing external I/O or integrations.
- Test helpers (crucial for verifying correctness).
- Core configuration.
### 3. Evidence Requirement
The skill **MUST** prove why an item is unused.
- **BAD**: "Variable `x` looks unused."
- **GOOD**: "Variable `x` defined at line 10. `grep -r 'x' .` returned only the definition. Local scope confirmed."
### 4. Reporting Format
Output to `audit_report.md`.
```markdown
# Audit Report: [Scope Name]
## Executive Summary
- **Total Scanned**: 50 items
- **✅ Safe to Remove**: 5
- **⚠️ Caution**: 2
- **❌ Keep**: 43
## Detailed Findings
| File/Context | Item Type | Name/Snippet | Risk | Evidence/Rationale |
| :-------------------- | :-------- | :------------ | :--------- | :-------------------------------------------------------------- |
| `app/models/user.rb` | Method | `legacy_auth` | ✅ SAFE | Defined but never called. Grep returned 0 hits. Not a callback. |
| `app/views/home.html` | CSS Class | `.old-banner` | ⚠️ CAUTION | No static usage, but class name might be dynamic in JS. |
| `app/jobs/mail.rb` | Class | `DailyMail` | ❌ KEEP | Inherits ApplicationJob. Likely called via Redis/Sidekiq. |
## Recommendations
- [ ] Safe items can be removed immediately.
- [ ] Caution items should be commented out or logged first.
```
### 5. Cleanup Strategy (Incremental)
**NEVER** delete code in the audit phase. Propose a plan in `cleanup_plan.md`:
1. **Level 1 (Safe)**: Delete dead private methods, unused local variables.
2. **Level 2 (Deprecate)**: Add `ActiveSupport::Deprecation` warning or log "Unused code reached" for CAUTION items.
3. **Level 3 (Observe)**: Monitor logs for 1 week.
4. **Level 4 (Remove)**: Delete after established silence.
5. **Level 5 (Commit & Monitor)**:
- Commit changes with reversible message: `git commit -m "refactor: remove unused [item] - reversible"`
- Monitor production logs for 48h
- Keep rollback plan ready: `git revert HEAD`
## Anti-Patterns
- **Deleting files** without a rollback plan.
- **Trusting `grep` blindly** on short strings (too many collisions) or huge projects (dynamic imports).
- Removing **Database Migrations** (historic record).
- Removing **Tests** just because they verify "unused" code (the test proves the code exists, not that it's useful).
## Validation Checklist
Before proposing removal of any item, verify:
- [ ] Item is NOT in routes.rb or called by external systems
- [ ] Item is NOT inherited from framework base classes (ApplicationJob, ApplicationController)
- [ ] Item is NOT used in string interpolation or send() calls
- [ ] Item is NOT a callback (before*\*, after*_, around\__)
- [ ] Item is NOT accessed via ENV variables or feature flags
- [ ] Removal does NOT break tests (run test suite after each deletion)
## Output Files
This skill generates:
- `audit_report.md` - Evidence-based findings
- `cleanup_plan.md` - Phased removal strategy (only after user approval)
- `rollback_plan.md` - Emergency recovery steps
Never execute deletions without explicit user confirmation.

View File

@ -0,0 +1,96 @@
---
name: implementing-plans
description: Implements approved technical plans from thoughts/shared/plans with strict verification and check-off process.
---
# Plan Implementation Specialist
## When to use this skill
- When the user provides a path to a plan in `thoughts/shared/plans/`.
- When the user asks to "implement the plan" or "execute the plan".
- When resuming an implementation plan.
## Getting Started
When given a plan path:
- Read the plan completely and check for any existing checkmarks (- [x])
- Read the original ticket and all files mentioned in the plan
- **Read files fully** - never use limit/offset parameters, you need complete context
- Think deeply about how the pieces fit together
- Create a todo list to track your progress
- Start implementing if you understand what needs to be done
If no plan path provided, ask for one.
## Implementation Philosophy
Plans are carefully designed, but reality can be messy. Your job is to:
- Follow the plan's intent while adapting to what you find
- Implement each phase fully before moving to the next
- Verify your work makes sense in the broader codebase context
- Update checkboxes in the plan as you complete sections
When things don't match the plan exactly, think about why and communicate clearly. The plan is your guide, but your judgment matters too.
If you encounter a mismatch:
- STOP and think deeply about why the plan can't be followed
- Present the issue clearly:
```
Issue in Phase [N]:
Expected: [what the plan says]
Found: [actual situation]
Why this matters: [explanation]
How should I proceed?
```
## Verification Approach
After implementing a phase:
- Run the success criteria checks (usually `make check test` covers everything)
- Fix any issues before proceeding
- Update your progress in both the plan and your todos
- Check off completed items in the plan file itself using Edit
- **Pause for human verification**: After completing all automated verification for a phase, pause and inform the human that the phase is ready for manual testing. Use this format:
```
Phase [N] Complete - Ready for Manual Verification
Automated verification passed:
- [List automated checks that passed]
Please perform the manual verification steps listed in the plan:
- [List manual verification items from the plan]
Let me know when manual testing is complete so I can proceed to Phase [N+1].
```
If instructed to execute multiple phases consecutively, skip the pause until the last phase. Otherwise, assume you are just doing one phase.
**Do not check off items in the manual testing steps until confirmed by the user.**
## If You Get Stuck
When something isn't working as expected:
- First, make sure you've read and understood all the relevant code
- Consider if the codebase has evolved since the plan was written
- Present the mismatch clearly and ask for guidance
Use sub-tasks sparingly - mainly for targeted debugging or exploring unfamiliar territory.
## Resuming Work
If the plan has existing checkmarks:
- Trust that completed work is done
- Pick up from the first unchecked item
- Verify previous work only if something seems off
Remember: You're implementing a solution, not just checking boxes. Keep the end goal in mind and maintain forward momentum.

View File

@ -0,0 +1,121 @@
---
name: organizing-code
description: Organizes and clarifies codebase structure after auditing. Focuses on readability, standardized comments, and non-destructive cleanup without altering behavior.
---
# Code Organization & Clarification Specialist
## Mission
To reduce confusion and cognitive load by organizing code, standardizing comments, and clarifying intent, WITHOUT deleting files, changing behavior, or breaking contracts. This skill acts as a "gardener" for the codebase, prioritizing **idempotency** and **reversibility**.
## When to use this skill
- AFTER running the `auditing-code` skill.
- When the codebase feels cluttered or ambiguous.
- To prepare legacy code for future refactoring or AI analysis.
- **Trigger phases:** `ORGANIZE`, `CLARIFY`, `DOCUMENT`, `TIDY`.
## Workflow
Copy this checklist to `task.md`:
- [ ] **Phase 1: Input Analysis**
- [ ] Read `audit_report.md` (if available).
- [ ] Identify areas marked as "CAUTION" or "KEEP".
- [ ] **Phase 2: Check Idempotency (Content-Based)**
- [ ] Verify if files already meet the organization standards.
- [ ] Skip files that already have sorted imports or standard headers.
- [ ] **Rule**: Logic must be deterministic based on FILE CONTENT, ignoring git history.
- [ ] **Phase 3: Safe Cleanup (Non-Destructive)**
- [ ] Remove unused _local_ variables (verified SAFE).
- [ ] Organize imports (sort, group, remove dups - **strict side-effect check**).
- [ ] Standardize comments.
- [ ] **Phase 4: Clarification & Documentation**
- [ ] Add standard tags (`[INTENTIONAL]`, `[LEGACY]`).
- [ ] Document implicit dependencies (Gemfile or docs).
- [ ] **Phase 5: Reporting**
- [ ] Generate `organization_report.md`.
## Instructions
### 1. Idempotency Rules (Crucial)
Before applying any change, check if it's needed based on **FILE CONTENT**:
- **Comments**: Do NOT add `[INTENTIONAL]` if the line already has it.
- **Imports**: Check if imports are already sorted. If yes, skip.
- **Logic**: If the code is already clear, DO NOT touch it.
### 2. Actions & Safety
#### Imports & Formatting
- **Global Formatting**: **PROHIBITED**. Do not run Prettier/ESLint on the whole file.
- **Local Adjustments**: Minimal whitespace changes are allowed **ONLY** within the organized lines (e.g., grouping imports).
- **Justification**: Any formatting tweak must be explicitly logged in the organization report.
#### Dependencies
- **Ruby (Gemfile)**: Use `#` comments for legacy/audit notes.
- **JS (package.json)**: **NO COMMENTS** inside JSON (strict format).
- Action: Create or update `docs/dependency_notes.md`.
- Content: Reference the package name, version, and reason for the note.
#### Structural Integrity (Strict No-Touch)
- **NO** moving files between folders.
- **NO** rearranging domain boundaries.
- **NO** altering namespaces or explicit exports.
### 3. Placeholder Strategy
For code that appears unused but is blocked from deletion:
**DO NOT DELETE.** Instead, wrap or annotate:
```ruby
# [INTENTIONAL] Reserved for future feature expansion (Phase 2)
def future_method
# ...
end
```
Standard Tags:
- `[INTENTIONAL]` - Kept on purpose.
- `[LEGACY]` - Old behavior, do not touch.
- `[FUTURE]` - Planned features.
### 4. Report Format
Output to `organization_report.md` with explicit reasoning. Ensure columns are consistent.
```markdown
# Organization Report: [Scope]
## Changes Applied
| File | Change | Reason | Risk | Scope |
| :----------- | :----------------- | :------------------ | :--- | :---------- |
| `User.rb` | Sorted imports | Readability | Low | Local |
| `Billing.rb` | Added [LEGACY] tag | Identified in Audit | None | Methodology |
## Skipped / Preserved
| File | Item | Reason | Risk | Scope |
| :------------- | :---------- | :----------------------------- | :----- | :---------- |
| `Init.js` | Import Sort | Potential side-effect import | Medium | Integration |
| `package.json` | Comments | JSON does not support comments | Low | Config |
## Structural Suggestions (For Future)
- Consider moving `Admin` module to its own namespace (documented only).
```
## Anti-Patterns
- **Moving code** "to look prettier" or aesthetic reordering.
- **Touching entrypoints** (Jobs, Webhooks, AI Agents).
- **Redundant tagging** (Adding `[INTENTIONAL]` twice).
- **Global Reformatting** (Changing whitespace outside of target lines).

View File

@ -0,0 +1,480 @@
---
name: creating-implementation-plans
description: Create detailed implementation plans through interactive research and iteration
---
# Implementation Plan Creation
## When to use this skill
- When the user runs `/create_plan`
- When the user asks to create a detailed implementation plan
- When the user needs to break down a large feature into phased tasks
## Implementation Plan
You are tasked with creating detailed implementation plans through an interactive, iterative process. You should be skeptical, thorough, and work collaboratively with the user to produce high-quality technical specifications.
## Initial Response
When this command is invoked:
1. **Check if parameters were provided**:
- If a file path or ticket reference was provided as a parameter, skip the default message
- Immediately read any provided files FULLY
- Begin the research process
2. **If no parameters provided**, respond with:
```
I'll help you create a detailed implementation plan. Let me start by understanding what we're building.
Please provide:
1. The task/ticket description (or reference to a ticket file)
2. Any relevant context, constraints, or specific requirements
3. Links to related research or previous implementations
I'll analyze this information and work with you to create a comprehensive plan.
Tip: You can also invoke this command with a ticket file directly: `/create_plan thoughts/allison/tickets/eng_1234.md`
For deeper analysis, try: `/create_plan think deeply about thoughts/allison/tickets/eng_1234.md`
```
Then wait for the user's input.
## Process Steps
### Step 1: Context Gathering & Initial Analysis
1. **Read all mentioned files immediately and FULLY**:
- Ticket files (e.g., `thoughts/allison/tickets/eng_1234.md`)
- Research documents
- Related implementation plans
- Any JSON/data files mentioned
- **IMPORTANT**: Use the Read tool WITHOUT limit/offset parameters to read entire files
- **CRITICAL**: DO NOT spawn sub-tasks before reading these files yourself in the main context
- **NEVER** read files partially - if a file is mentioned, read it completely
2. **Spawn initial research tasks to gather context**:
Before asking the user any questions, use specialized agents to research in parallel:
- Use the **codebase-locator** agent to find all files related to the ticket/task
- Use the **codebase-analyzer** agent to understand how the current implementation works
- If relevant, use the **thoughts-locator** agent to find any existing thoughts documents about this feature
- If a Linear ticket is mentioned, use the **linear-ticket-reader** agent to get full details
These agents will:
- Find relevant source files, configs, and tests
- Identify the specific directories to focus on (e.g., if WUI is mentioned, they'll focus on humanlayer-wui/)
- Trace data flow and key functions
- Return detailed explanations with file:line references
3. **Read all files identified by research tasks**:
- After research tasks complete, read ALL files they identified as relevant
- Read them FULLY into the main context
- This ensures you have complete understanding before proceeding
4. **Analyze and verify understanding**:
- Cross-reference the ticket requirements with actual code
- Identify any discrepancies or misunderstandings
- Note assumptions that need verification
- Determine true scope based on codebase reality
5. **Present informed understanding and focused questions**:
```
Based on the ticket and my research of the codebase, I understand we need to [accurate summary].
I've found that:
- [Current implementation detail with file:line reference]
- [Relevant pattern or constraint discovered]
- [Potential complexity or edge case identified]
Questions that my research couldn't answer:
- [Specific technical question that requires human judgment]
- [Business logic clarification]
- [Design preference that affects implementation]
```
Only ask questions that you genuinely cannot answer through code investigation.
### Step 2: Research & Discovery
After getting initial clarifications:
1. **If the user corrects any misunderstanding**:
- DO NOT just accept the correction
- Spawn new research tasks to verify the correct information
- Read the specific files/directories they mention
- Only proceed once you've verified the facts yourself
2. **Create a research todo list** using TodoWrite to track exploration tasks
3. **Spawn parallel sub-tasks for comprehensive research**:
- Create multiple Task agents to research different aspects concurrently
- Use the right agent for each type of research:
**For deeper investigation:**
- **codebase-locator** - To find more specific files (e.g., "find all files that handle [specific component]")
- **codebase-analyzer** - To understand implementation details (e.g., "analyze how [system] works")
- **codebase-pattern-finder** - To find similar features we can model after
**For historical context:**
- **thoughts-locator** - To find any research, plans, or decisions about this area
- **thoughts-analyzer** - To extract key insights from the most relevant documents
**For related tickets:**
- **linear-searcher** - To find similar issues or past implementations
Each agent knows how to:
- Find the right files and code patterns
- Identify conventions and patterns to follow
- Look for integration points and dependencies
- Return specific file:line references
- Find tests and examples
4. **Wait for ALL sub-tasks to complete** before proceeding
5. **Present findings and design options**:
```
Based on my research, here's what I found:
**Current State:**
- [Key discovery about existing code]
- [Pattern or convention to follow]
**Design Options:**
1. [Option A] - [pros/cons]
2. [Option B] - [pros/cons]
**Open Questions:**
- [Technical uncertainty]
- [Design decision needed]
Which approach aligns best with your vision?
```
### Step 3: Plan Structure Development
Once aligned on approach:
1. **Create initial plan outline**:
```
Here's my proposed plan structure:
## Overview
[1-2 sentence summary]
## Implementation Phases:
1. [Phase name] - [what it accomplishes]
2. [Phase name] - [what it accomplishes]
3. [Phase name] - [what it accomplishes]
Does this phasing make sense? Should I adjust the order or granularity?
```
2. **Get feedback on structure** before writing details
### Step 4: Detailed Plan Writing
After structure approval:
1. **Write the plan** to `thoughts/shared/plans/YYYY-MM-DD-ENG-XXXX-description.md`
- Format: `YYYY-MM-DD-ENG-XXXX-description.md` where:
- YYYY-MM-DD is today's date
- ENG-XXXX is the ticket number (omit if no ticket)
- description is a brief kebab-case description
- Examples:
- With ticket: `2025-01-08-ENG-1478-parent-child-tracking.md`
- Without ticket: `2025-01-08-improve-error-handling.md`
2. **Use this template structure**:
````markdown
# [Feature/Task Name] Implementation Plan
## Overview
[Brief description of what we're implementing and why]
## Current State Analysis
[What exists now, what's missing, key constraints discovered]
## Desired End State
[A Specification of the desired end state after this plan is complete, and how to verify it]
### Key Discoveries:
- [Important finding with file:line reference]
- [Pattern to follow]
- [Constraint to work within]
## What We're NOT Doing
[Explicitly list out-of-scope items to prevent scope creep]
## Implementation Approach
[High-level strategy and reasoning]
## Phase 1: [Descriptive Name]
### Overview
[What this phase accomplishes]
### Changes Required:
#### 1. [Component/File Group]
**File**: `path/to/file.ext`
**Changes**: [Summary of changes]
```[language]
// Specific code to add/modify
```
### Success Criteria:
#### Automated Verification:
- [ ] Migration applies cleanly: `make migrate`
- [ ] Unit tests pass: `make test-component`
- [ ] Type checking passes: `npm run typecheck`
- [ ] Linting passes: `make lint`
- [ ] Integration tests pass: `make test-integration`
#### Manual Verification:
- [ ] Feature works as expected when tested via UI
- [ ] Performance is acceptable under load
- [ ] Edge case handling verified manually
- [ ] No regressions in related features
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human that the manual testing was successful before proceeding to the next phase.
---
## Phase 2: [Descriptive Name]
[Similar structure with both automated and manual success criteria...]
---
## Testing Strategy
### Unit Tests:
- [What to test]
- [Key edge cases]
### Integration Tests:
- [End-to-end scenarios]
### Manual Testing Steps:
1. [Specific step to verify feature]
2. [Another verification step]
3. [Edge case to test manually]
## Performance Considerations
[Any performance implications or optimizations needed]
## Migration Notes
[If applicable, how to handle existing data/systems]
## References
- Original ticket: `thoughts/allison/tickets/eng_XXXX.md`
- Related research: `thoughts/shared/research/[relevant].md`
- Similar implementation: `[file:line]`
````
### Step 5: Sync and Review
1. **Sync the thoughts directory**:
- Run `humanlayer thoughts sync` to sync the newly created plan
- This ensures the plan is properly indexed and available
2. **Present the draft plan location**:
```
I've created the initial implementation plan at:
`thoughts/shared/plans/YYYY-MM-DD-ENG-XXXX-description.md`
Please review it and let me know:
- Are the phases properly scoped?
- Are the success criteria specific enough?
- Any technical details that need adjustment?
- Missing edge cases or considerations?
```
3. **Iterate based on feedback** - be ready to:
- Add missing phases
- Adjust technical approach
- Clarify success criteria (both automated and manual)
- Add/remove scope items
- After making changes, run `humanlayer thoughts sync` again
4. **Continue refining** until the user is satisfied
## Important Guidelines
1. **Be Skeptical**:
- Question vague requirements
- Identify potential issues early
- Ask "why" and "what about"
- Don't assume - verify with code
2. **Be Interactive**:
- Don't write the full plan in one shot
- Get buy-in at each major step
- Allow course corrections
- Work collaboratively
3. **Be Thorough**:
- Read all context files COMPLETELY before planning
- Research actual code patterns using parallel sub-tasks
- Include specific file paths and line numbers
- Write measurable success criteria with clear automated vs manual distinction
- automated steps should use `make` whenever possible - for example `make -C humanlayer-wui check` instead of `cd humanlayer-wui && bun run fmt`
4. **Be Practical**:
- Focus on incremental, testable changes
- Consider migration and rollback
- Think about edge cases
- Include "what we're NOT doing"
5. **Track Progress**:
- Use TodoWrite to track planning tasks
- Update todos as you complete research
- Mark planning tasks complete when done
6. **No Open Questions in Final Plan**:
- If you encounter open questions during planning, STOP
- Research or ask for clarification immediately
- Do NOT write the plan with unresolved questions
- The implementation plan must be complete and actionable
- Every decision must be made before finalizing the plan
## Success Criteria Guidelines
**Always separate success criteria into two categories:**
1. **Automated Verification** (can be run by execution agents):
- Commands that can be run: `make test`, `npm run lint`, etc.
- Specific files that should exist
- Code compilation/type checking
- Automated test suites
2. **Manual Verification** (requires human testing):
- UI/UX functionality
- Performance under real conditions
- Edge cases that are hard to automate
- User acceptance criteria
**Format example:**
```markdown
### Success Criteria:
#### Automated Verification:
- [ ] Database migration runs successfully: `make migrate`
- [ ] All unit tests pass: `go test ./...`
- [ ] No linting errors: `golangci-lint run`
- [ ] API endpoint returns 200: `curl localhost:8080/api/new-endpoint`
#### Manual Verification:
- [ ] New feature appears correctly in the UI
- [ ] Performance is acceptable with 1000+ items
- [ ] Error messages are user-friendly
- [ ] Feature works correctly on mobile devices
```
## Common Patterns
### For Database Changes:
- Start with schema/migration
- Add store methods
- Update business logic
- Expose via API
- Update clients
### For New Features:
- Research existing patterns first
- Start with data model
- Build backend logic
- Add API endpoints
- Implement UI last
### For Refactoring:
- Document current behavior
- Plan incremental changes
- Maintain backwards compatibility
- Include migration strategy
## Sub-task Spawning Best Practices
When spawning research sub-tasks:
1. **Spawn multiple tasks in parallel** for efficiency
2. **Each task should be focused** on a specific area
3. **Provide detailed instructions** including:
- Exactly what to search for
- Which directories to focus on
- What information to extract
- Expected output format
4. **Be EXTREMELY specific about directories**:
- If the ticket mentions "WUI", specify `humanlayer-wui/` directory
- If it mentions "daemon", specify `hld/` directory
- Never use generic terms like "UI" when you mean "WUI"
- Include the full path context in your prompts
5. **Specify read-only tools** to use
6. **Request specific file:line references** in responses
7. **Wait for all tasks to complete** before synthesizing
8. **Verify sub-task results**:
- If a sub-task returns unexpected results, spawn follow-up tasks
- Cross-check findings against the actual codebase
- Don't accept results that seem incorrect
Example of spawning multiple tasks:
```python
# Spawn these tasks concurrently:
tasks = [
Task("Research database schema", db_research_prompt),
Task("Find API patterns", api_research_prompt),
Task("Investigate UI components", ui_research_prompt),
Task("Check test patterns", test_research_prompt)
]
```

View File

@ -0,0 +1,114 @@
---
name: removing-code
description: Surgically removes verified dead code (local vars, unused imports) ONLY after audit/organization and explicit user approval. Zero structural changes.
---
# Surgical Code Removal Specialist
## Mission
To eliminate verified dead code (variables, imports, local parameters) with surgical precision, reducing technical noise WITHOUT altering system behavior, breaking contracts, or refactoring logic. **We only remove what is explicitly approved.**
## When to use this skill
- ONLY AFTER running `auditing-code` AND `organizing-code`.
- When `audit_report.md` and `organization_report.md` confirm items are safe.
- To finalize a cleanup cycle.
- **Trigger phases:** `CLEANUP`, `REMOVE`, `PURGE`, `FINALIZE`.
## Regras de Ouro (INQUEBRÁVEIS)
1. **Approval First**: No removal without explicit user confirmation (per item or block).
2. **Strict Scope**: No removal outside the proposed list.
3. **No Structural Changes**: Do not move files, change folders, or alter architecture.
4. **No Public API Changes**: Public signatures are untouchable.
5. **Reversibility**: Every action must be easily reversible.
**ABORT** if any rule cannot be guaranteed.
## Workflow
Copy this checklist to `task.md`:
- [ ] **Phase 1: Input & Validation**
- [ ] Read `audit_report.md` AND `organization_report.md`.
- [ ] Verify items are marked `SAFE`.
- [ ] Check if items were preserved/documented in Organization phase.
- [ ] **Phase 2: Removal Proposal**
- [ ] Generate `cleanup_proposal.md` with approval checkboxes.
- [ ] **STOP** and Request Approval.
- [ ] **Phase 3: Execution (Approved Only)**
- [ ] Check Idempotency (Skip if already removed).
- [ ] Remove _exact_ approved lines.
- [ ] Minimal diffs (no auto-formatting).
- [ ] **Phase 4: Post-Execution & Safety**
- [ ] Verify file syntax (compilation/parsing).
- [ ] Ensure local references check.
- [ ] Generate `cleanup_report.md`.
## Authorized Scope (ONLY)
Remove **ONLY** if verified unused and safe:
- **Local Variables**: Defined within a method, never read, no side effects.
- **Local Parameters**: Private methods only. NOT callbacks, overrides, or public APIs.
- **Imports**:
- **Named Imports Only** (e.g., `import { X } from 'y'`).
- **FORBIDDEN**: Bare imports (`import 'y'`) or side-effect imports.
- **FORBIDDEN**: Imports initializing plugins, polyfills, CSS, or observability.
## Forbidden Scope (NEVER REMOVE)
- **Public Methods / APIs**
- **Jobs, Workers, Schedulers**
- **Webhooks & External Callbacks**
- **Controllers & Routes**
- **AI Agents / Tools**
- **Feature Flags / Dynamic Code** (`send`, `eval`)
- **Migrations**
- **Dependencies (Gems/Packages)**
- **Entire Files**
If in doubt -> **DO NOT REMOVE**.
## Instructions
### 1. Proposal Generation
Create `cleanup_proposal.md` with Governance:
```markdown
# Removal Proposal
Please mark [x] to approve specific removals.
| Approve | File | Type | Item | Reason |
| :-----: | :-------- | :-------- | :------------- | :------------------ |
| [ ] | `User.rb` | Local Var | `unused_count` | 0 references |
| [ ] | `Util.js` | Import | `lodash` | Unused named import |
```
### 2. Execution Rules
- **Idempotency**: If the line/item is missing, log as "Already Removed" and continue. DO NOT fail.
- **Precision**: Remove only the target line.
- **Whitespace**: Do not reformat the rest of the file.
- **State**: If an item has `[INTENTIONAL]` or `[LEGACY]` tags, **SKIP IT**.
### 3. Verification (Post-Execution)
- **Syntax Check**: Ensure the file parses correctly (e.g., no syntax errors introduced).
- **Broken Refs**: Ensure no _other_ code in the _same file_ was referencing the removed item (sanity check).
- **No Test Suite**: Automated tests are NOT required for this specific step (assumed low risk).
## Anti-Patterns
- **"While I'm here..."**: Cleaning up unrelated code while removing a variable.
- **Speculative Removal**: "This looks unused". (Must be proven audit-safe).
- **Breaking Builds**: Removing dependencies or critical imports.
- **Refactoring**: Changing logic flow instead of just removing the dead leaf.
## Output Files
- `cleanup_proposal.md` (Proposal with approval checkboxes)
- `cleanup_report.md` (After execution summary)

View File

@ -0,0 +1,134 @@
---
name: researching-codebase
description: Conducts comprehensive research across the codebase to document current implementation and historical context, without suggesting changes.
---
# Codebase Research Specialist
## Mission
To conduct comprehensive research across the codebase to answer user questions by spawning parallel sub-tasks and synthesizing findings. Your ONLY job is to document and explain the codebase AS-IS.
**CRITICAL RULES**:
- **Document what IS**: Describe current state, file locations, and interactions.
- **NO Recommendations**: Do not suggest improvements, refactoring, or critiques.
- **NO Root Cause Analysis**: Unless explicitly asked.
- **Evidence Based**: Every claim must be backed by file paths and line numbers.
## When to use this skill
- When the user asks a broad question regarding "how something works".
- When creating documentation for existing systems.
- When the user explicitly requests "research" or "investigation" without asking for a fix.
- **Trigger phases**: `RESEARCH`, `DOCUMENT`, `INVESTIGATE`, `MAP`.
## Workflow
Copy this checklist to `task.md`:
- [ ] **Phase 1: Input & Analysis**
- [ ] Read any specifically mentioned files FULLY (no limit/offset).
- [ ] Break down the research question into sub-topics.
- [ ] Create a research plan (lists of components/patterns to find).
- [ ] **Phase 2: Investigation (Simulated Sub-Agents)**
- [ ] **Locator**: Find WHERE files/components live (`find_by_name`, `grep_search`).
- [ ] **Analyzer**: Understand HOW code works (`view_file`).
- [ ] **Pattern Finder**: Find usage examples (`grep_search`).
- [ ] **History**: Check `thoughts/` directory for past context.
- [ ] **Phase 3: Synthesis**
- [ ] Compile findings, prioritizing live code.
- [ ] Connect findings across components.
- [ ] Verify all file paths and line numbers.
- [ ] **Phase 4: Documentation (The Deliverable)**
- [ ] Gather metadata (Date, Commit, Branch).
- [ ] Create document: `thoughts/shared/research/YYYY-MM-DD-ENG-XXXX-[topic].md`.
- [ ] Sync/Notify user.
## Instructions
### 1. Research Protocol
1. **Read First**: If the user mentions files/tickets, read them before doing anything else.
2. **Decompose**: Don't try to solve everything in one prompt. Split into logical sub-tasks.
3. **Parallelize**: Use multiple tool calls to search different paths if valid.
### 2. Document Template
**File Path**: `thoughts/shared/research/YYYY-MM-DD-[ticket-or-topic].md`
```markdown
---
date: { { CURRENT_DATE } }
researcher: Antigravity
git_commit: { { GIT_COMMIT } }
branch: { { GIT_BRANCH } }
repository: Chatwoot
topic: '{{USER_QUERY}}'
tags: [research, { { COMPONENTS } }]
status: complete
last_updated: { { CURRENT_DATE } }
---
# Research: {{TOPIC_TITLE}}
**Date**: {{CURRENT_DATE_TIME}}
**Researcher**: Antigravity
**Git Commit**: {{GIT_COMMIT}}
**Branch**: {{GIT_BRANCH}}
## Research Question
{{ORIGINAL_QUERY}}
## Summary
[High-level documentation of what was found, answering the user's question by describing what exists]
## Detailed Findings
### [Component/Area 1]
- Description of what exists ([file.ext:line](link))
- How it connects to other components
- Current implementation details (without evaluation)
### [Component/Area 2]
...
## Code References
- `path/to/file.py:123` - Description of what's there
- `another/file.ts:45-67` - Description of the code block
## Historical Context (from thoughts/)
[Relevant insights from thoughts/ directory with references]
## Open Questions
[Any areas that need further investigation]
```
### 3. Path & Metadata Handling
- **Thoughts Paths**: Always remove `searchable/` segment if found (e.g., `thoughts/searchable/shared/` -> `thoughts/shared/`).
- **Metadata Generation**:
- Date: Use current time.
- Commit: Run `git rev-parse HEAD`.
- Branch: Run `git branch --show-current`.
- Ticket: Extract from prompt if available (e.g., ENG-1234).
### 4. Anti-Patterns
- **Speculating**: Guessing functionality without `view_file`.
- **Critiquing**: "This code is messy" (STOP. Just describe strict logic).
- **Refactoring**: "We should move this..." (STOP. Just document current location).
- **Ignoring History**: Failing to check existing `thoughts/` documentation.
## Resources
- Use `find_by_name` to act as **codebase-locator**.
- Use `view_file` to act as **codebase-analyzer**.
- Use `grep_search` to act as **codebase-pattern-finder**.

View File

@ -18,6 +18,7 @@
# greeting_enabled :boolean default(FALSE)
# greeting_message :string
# lock_to_single_conversation :boolean default(FALSE), not null
# message_signature_enabled :boolean
# name :string not null
# out_of_office_message :string
# sender_name_type :integer default("friendly"), not null

View File

@ -18,6 +18,7 @@
# greeting_enabled :boolean default(FALSE)
# greeting_message :string
# lock_to_single_conversation :boolean default(FALSE), not null
# message_signature_enabled :boolean
# name :string not null
# out_of_office_message :string
# sender_name_type :integer default("friendly"), not null

View File

@ -18,6 +18,7 @@
# greeting_enabled :boolean default(FALSE)
# greeting_message :string
# lock_to_single_conversation :boolean default(FALSE), not null
# message_signature_enabled :boolean
# name :string not null
# out_of_office_message :string
# sender_name_type :integer default("friendly"), not null

16
task.md Normal file
View File

@ -0,0 +1,16 @@
- [x] **Phase 1: Input Analysis**
- [x] Read `audit_report.md` (if available - not found).
- [x] Identify areas marked as "CAUTION" or "KEEP".
- [x] **Phase 2: Check Idempotency (Content-Based)**
- [x] Verify if files already meet the organization standards.
- [x] Skip files that already have sorted imports or standard headers.
- [x] **Rule**: Logic must be deterministic based on FILE CONTENT, ignoring git history.
- [ ] **Phase 3: Safe Cleanup (Non-Destructive)**
- [ ] Remove unused _local_ variables (verified SAFE).
- [ ] Organize imports (sort, group, remove dups - **strict side-effect check**).
- [x] Standardize comments.
- [x] **Phase 4: Clarification & Documentation**
- [x] Add standard tags (`[INTENTIONAL]`, `[LEGACY]`).
- [x] Document implicit dependencies (Gemfile or docs).
- [x] **Phase 5: Reporting**
- [x] Generate `organization_report.md`.