Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .beads/issues.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{"id":"zuko-w0p","title":"Chat context for contacts, deals, and companies (agentic CRM from chat)","description":"Epic: Enable users to add contacts, deals, and companies as context to a chat so all conversations happen in that context. Support autocomplete when adding entities. Send context preferences to backend and use them for responses. Future: allow updating contact/deal/company (e.g. email, phone, status) from natural language in chat without leaving to Contacts/Deals UI. Source: Call transcript with Yuva Kumar.","status":"open","priority":2,"issue_type":"epic","created_at":"2026-03-03T11:05:05.271667+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:05:05.271667+05:30"}
{"id":"zuko-w0p.1","title":"Chat attachment dropdown: add Contact, Deal, Company options","description":"UI change: In the new-chat plus menu (attachment dropdown), make 'Add photos and files' the first option. Add a second row with three options in one line: Add contact, Add deal, Add company. These allow the user to attach CRM entities as context to the chat. Small PR: dropdown component + icons/labels.","acceptance_criteria":"Dropdown shows photos/files first; second row shows Add contact | Add deal | Add company. Clicking each opens the corresponding picker (picker implementation is a separate task).","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:19.94792+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:16:20.051803+05:30","dependencies":[{"issue_id":"zuko-w0p.1","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:19.954587+05:30","created_by":"babanitturu"}]}
{"id":"zuko-w0p.2","title":"Contact/Deal/Company picker with autocomplete","description":"When user clicks Add contact (or Add deal / Add company), show a search input. Typing (e.g. 'bill') shows autocomplete suggestions (e.g. Bill Gates). User selects one or more; selected entities are added to chat context. Support multiple entities and combination of types (one contact + one deal + one company). Reuse or integrate with existing contact/deal/company APIs for search. Small PR: picker component + search API usage.","acceptance_criteria":"Type partial name → list filters with autocomplete. Select item → added to context. Can add multiple contacts/deals/companies. Context displayed (e.g. chips) in chat input area.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:24.879993+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:05:24.879993+05:30","dependencies":[{"issue_id":"zuko-w0p.2","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:24.887111+05:30","created_by":"babanitturu"},{"issue_id":"zuko-w0p.2","depends_on_id":"zuko-w0p.1","type":"blocks","created_at":"2026-03-03T11:06:07.04574+05:30","created_by":"babanitturu"}]}
{"id":"zuko-w0p.3","title":"Context preferences: backend accept/store and frontend send multiple contextEntities; agent responds in context","description":"Single task covering both backend and frontend for chat context.\n\nBackend: Chat API accepts context preferences (e.g. contactIds[], dealIds[], companyIds[] or a unified contextEntities[]). Persist or pass context with the message/conversation so the agent can answer in the context of the selected contact/deal/company. Define DTOs and update chat controller and message storage.\n\nFrontend: Wire the contact/deal/company picker selections to the chat API. When the user sends a message, include the current context (selected entities) in the request. Multiple contextEntities can be sent from the UI (e.g. one contact + one deal + one company). Ensure context is sent with each message in the conversation.\n\nThe agent should respond in the context of the provided entities (e.g. in the context of this contact, deal, and company).","acceptance_criteria":"Backend: POST chat/messages accepts optional context payload (multiple contextEntities). Context is stored or associated with the conversation/message. Frontend: Sending a message includes selected contact/deal/company context. Agent responses are in the context of the provided entities.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:29.628034+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:16:28.404006+05:30","dependencies":[{"issue_id":"zuko-w0p.3","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:29.634675+05:30","created_by":"babanitturu"}]}
{"id":"zuko-w0p.3","title":"Context preferences: frontend send multiple contextEntities(contact, company, deals); agent should be able to respond with multile context combinations.\n","description":"Single task covering both backend and frontend for chat context.\n\nBackend: Chat API accepts context preferences (e.g. contactIds[], dealIds[], companyIds[] or a unified contextEntities[]). Persist or pass context with the message/conversation so the agent can answer in the context of the selected contact/deal/company. Define DTOs and update chat controller and message storage.\n\nFrontend: Wire the contact/deal/company picker selections to the chat API. When the user sends a message, include the current context (selected entities) in the request. Multiple contextEntities can be sent from the UI (e.g. one contact + one deal + one company). Ensure context is sent with each message in the conversation.\n\nThe agent should respond in the context of the provided entities (e.g. in the context of this contact, deal, and company).","acceptance_criteria":"Backend: POST chat/messages accepts optional context payload (multiple contextEntities). Context is stored or associated with the conversation/message. Frontend: Sending a message includes selected contact/deal/company context. Agent responses are in the context of the provided entities.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:29.628034+05:30","created_by":"babanitturu","updated_at":"2026-03-03T14:55:54.35405+05:30","dependencies":[{"issue_id":"zuko-w0p.3","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:29.634675+05:30","created_by":"babanitturu"}]}
{"id":"zuko-w0p.4","title":"Frontend: send context preferences with every chat message","description":"Wire the contact/deal/company picker selections to the chat API. When user sends a message, include the current context (selected contact IDs, deal IDs, company IDs) in the request. Ensure context is sent with each message in the conversation. Small PR: state in chat component + API client payload.","acceptance_criteria":"Sending a message includes context preferences when user has added contacts/deals/companies. Backend receives and can log/use them.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:33.442733+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:16:29.74942+05:30","dependencies":[{"issue_id":"zuko-w0p.4","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:33.44944+05:30","created_by":"babanitturu"}],"deleted_at":"2026-03-03T11:16:29.74942+05:30","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
{"id":"zuko-w0p.5","title":"Backend: use chat context in prompt/retrieval (contact, deal, company data)","description":"When a message has context (contactIds, dealIds, companyIds), fetch the relevant entity data (contact details, deal details, company details) and include them in the system prompt or RAG context so the model answers in the context of that person/deal/company. Small PR: service layer that loads entities and injects into prompt/context.","acceptance_criteria":"With context set, model responses are grounded in the selected contact/deal/company data. Without context, behavior unchanged.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:37.003106+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:16:15.535151+05:30","dependencies":[{"issue_id":"zuko-w0p.5","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:37.012167+05:30","created_by":"babanitturu"},{"issue_id":"zuko-w0p.5","depends_on_id":"zuko-w0p.3","type":"blocks","created_at":"2026-03-03T11:06:07.217888+05:30","created_by":"babanitturu"}],"deleted_at":"2026-03-03T11:16:15.535151+05:30","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
{"id":"zuko-w0p.6","title":"Agentic updates from chat (update contact/deal/company via natural language)","description":"Allow user to say in chat e.g. 'This person'\\''s email changed to X, phone to Y', 'Update deal status to closed'. Agent interprets intent and updates the contact/deal/company (or creates structured actions) so user does not need to go to Contacts/Deals UI to edit.\n","acceptance_criteria":"User can request contact/deal/company updates in natural language in a context-aware chat; backend applies updates (with confirmation or audit).","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:41.718235+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:09:36.961438+05:30","dependencies":[{"issue_id":"zuko-w0p.6","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:41.726704+05:30","created_by":"babanitturu"},{"issue_id":"zuko-w0p.6","depends_on_id":"zuko-w0p.3","type":"blocks","created_at":"2026-03-03T11:16:30.415474+05:30","created_by":"babanitturu"}]}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ test-output
/.codex/config.toml
/.codex/config.toml.bak
/.codex/skills
.cursor/commands/
/.opencode/skill
/.skillz
/.vibe/skills
Expand Down
92 changes: 92 additions & 0 deletions apps/web-e2e/src/chat.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,98 @@ test.describe("Chat", () => {
await expect(textarea).toHaveValue("");
});

test.describe("chat attachments", () => {
test("attachment menu shows Contact, Company, and Deal options", async ({
page,
}) => {
await page.goto(`/chat/${chatId}`);

const attachmentTrigger = page
.getByRole("main")
.locator('button[aria-haspopup="menu"]')
.first();
await expect(attachmentTrigger).toBeVisible({ timeout: 5000 });
await attachmentTrigger.click();

// Menu should list context options: Add contact, Add company, Add deal
await expect(
page.getByRole("menuitem", { name: /add contact/i }),
).toBeVisible({ timeout: 3000 });
await expect(
page.getByRole("menuitem", { name: /add company/i }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: /add deal/i }),
).toBeVisible();
});

test("can select and add one contact as context", async ({ page }) => {
await page.goto(`/chat/${chatId}`);

const attachmentTrigger = page
.getByRole("main")
.locator('button[aria-haspopup="menu"]')
.first();
await attachmentTrigger.click();
await page.getByRole("menuitem", { name: /add contact/i }).click();

const dialog = page.getByRole("dialog", { name: /add contacts/i });
await expect(dialog).toBeVisible({ timeout: 5000 });
await expect(dialog.getByText("TEST CONTACT")).toBeVisible({
timeout: 10000,
});
await dialog.getByRole("button", { name: /TEST CONTACT/ }).click();
await dialog.getByRole("button", { name: /^Add/ }).click();

await expect(dialog).not.toBeVisible();
await expect(page.getByText("TEST CONTACT").first()).toBeVisible();
});

test("can select and add one company as context", async ({ page }) => {
await page.goto(`/chat/${chatId}`);

const attachmentTrigger = page
.getByRole("main")
.locator('button[aria-haspopup="menu"]')
.first();
await attachmentTrigger.click();
await page.getByRole("menuitem", { name: /add company/i }).click();

const dialog = page.getByRole("dialog", { name: /add companies/i });
await expect(dialog).toBeVisible({ timeout: 5000 });
await expect(dialog.getByText("TEST COMPANY")).toBeVisible({
timeout: 10000,
});
await dialog.getByRole("button", { name: /TEST COMPANY/ }).click();
await dialog.getByRole("button", { name: /^Add/ }).click();

await expect(dialog).not.toBeVisible();
await expect(page.getByText("TEST COMPANY").first()).toBeVisible();
});

test("can select and add one deal as context", async ({ page }) => {
await page.goto(`/chat/${chatId}`);

const attachmentTrigger = page
.getByRole("main")
.locator('button[aria-haspopup="menu"]')
.first();
await attachmentTrigger.click();
await page.getByRole("menuitem", { name: /add deal/i }).click();

const dialog = page.getByRole("dialog", { name: /add deals/i });
await expect(dialog).toBeVisible({ timeout: 5000 });
await expect(dialog.getByText("TEST DEAL")).toBeVisible({
timeout: 10000,
});
await dialog.getByRole("button", { name: /TEST DEAL/ }).click();
await dialog.getByRole("button", { name: /^Add/ }).click();

await expect(dialog).not.toBeVisible();
await expect(page.getByText("TEST DEAL").first()).toBeVisible();
});
});

test.describe("with mentions", () => {
test("can add a contact mention to a message", async ({ page }) => {
await page.goto(`/chat/${chatId}`);
Expand Down
29 changes: 22 additions & 7 deletions apps/web/src/app/(app)/chat/[chatId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useParams } from 'next/navigation';
import { useInvalidateChats } from '@/hooks/use-chats';
import { contactsApi } from '@/lib/api/contacts';
import { companiesApi } from '@/lib/api/companies';
import { dealsApi } from '@/lib/api/deals';

export default function ChatPage() {
const params = useParams();
Expand Down Expand Up @@ -65,7 +66,7 @@ export default function ChatPage() {
type: string;
id: number;
}): Promise<ChatEntity> => {
const type = ref.type as 'contact' | 'company';
const type = ref.type as 'contact' | 'company' | 'deal';
try {
if (type === 'contact') {
const c = await contactsApi.getContact(ref.id);
Expand All @@ -76,18 +77,32 @@ export default function ChatPage() {
metadata: { type: 'contact', entityId: ref.id },
};
}
const c = await companiesApi.getCompany(ref.id);
if (type === 'company') {
const c = await companiesApi.getCompany(ref.id);
return {
type: 'company',
id: ref.id,
name: c.companyName,
metadata: { type: 'company', entityId: ref.id },
};
}
const d = await dealsApi.getDeal(ref.id);
return {
type: 'company',
type: 'deal',
id: ref.id,
name: c.companyName,
metadata: { type: 'company', entityId: ref.id },
name: d.title,
metadata: { type: 'deal', entityId: ref.id },
};
} catch {
return {
type,
id: ref.id,
name: type === 'contact' ? 'Contact' : 'Company',
name:
type === 'contact'
? 'Contact'
: type === 'company'
? 'Company'
: 'Deal',
metadata: { type, entityId: ref.id },
};
}
Expand Down Expand Up @@ -149,7 +164,7 @@ export default function ChatPage() {
// Hydrate context entities from backend response (includes names)
const hydratedEntities: ChatEntity[] = contextRefs.map(
(ref: { type: string; id: number; name: string }) => ({
type: ref.type as 'contact' | 'company',
type: ref.type as 'contact' | 'company' | 'deal',
id: ref.id,
name: ref.name, // Use actual name from backend
metadata: { type: ref.type, entityId: ref.id },
Expand Down
10 changes: 7 additions & 3 deletions apps/web/src/components/Chat/ChatContextDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ export const ChatContextDialog = () => {
title={
contextManager.dialogConfig.type === 'contact'
? 'Add Contacts'
: 'Add Companies'
: contextManager.dialogConfig.type === 'company'
? 'Add Companies'
: 'Add Deals'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of conditional statements, lets create a labelMap to render based on the type

}
description={
contextManager.dialogConfig.type === 'contact'
? 'Select contacts to add as context'
: 'Select companies to add as context'
: contextManager.dialogConfig.type === 'company'
? 'Select companies to add as context'
: 'Select deals to add as context'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use LableMap instead

}
searchPlaceholder={`Search ${contextManager.dialogConfig.type}s...`}
searchPlaceholder={`Search ${contextManager.dialogConfig.type ?? 'items'}...`}
items={contextManager.dialogConfig.items}
selectedIds={contextManager.dialogConfig.selectedIds}
onSelectionChange={contextManager.dialogConfig.onSelectionChange}
Expand Down
Loading