feat(search): ⌘K workspace-wide global search + inbox deep-link (#101)#81
Merged
Conversation
- new GET /api/search?q= — cross-project search over conversations
(visitor name/email + message body) and projects (name), behind
requireSession+requireWorkspace; returns { conversations, projects }
- tenant boundary expressed once via new lib/workspace-scope.ts
workspaceProjectIds() (reusable by the future #105 unified inbox);
every arm intersected with inArray(projectId, ids) in the base WHERE,
empty-set guard short-circuits before any IN(), and()-never-bare-eq()
- reuse lib/search.ts (likeContains/buildSnippet/MAX_MATCH_CONVERSATIONS);
per-entity caps (8 conversations / 5 projects); never returns project
secrets (publicKey/inboundEmailLocal/systemPrompt/knowledgeText)
- ⌘K/Ctrl+K command palette (vendored cmdk/CommandDialog) + TopBar search
affordance; 250ms debounce, min-2-chars, abort-in-flight; honesty rail
(live-gated rows, Searching…, "No matches" only after a settled query);
results route to /inbox?project=&c= or /settings/projects/<id>
- inbox deep-linkability: read ?project=&c= once on mount + mirror the open
thread to the URL via history.replaceState (shareable threads)
- CommandDialog now forwards cmdk Command props (shouldFilter) to the inner
Command + adds an sr-only DialogTitle; api() gains an AbortSignal passthrough
Tests (the deliverable): real-sqlite e2e proving the cross-workspace
boundary across all three arms (name/email, message-body two-hop, project),
the empty-set guard, secret-omission, and the 403 membership gate; palette
honesty-rail + routing; inbox deep-link open + URL sync.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
❌ Some deployments failed
Deployed with Ploy |
Omarear
added a commit
that referenced
this pull request
Jun 24, 2026
…(#112)
Two placement fixes on the inbox restyle; honesty rail + ROADMAP dimming
unchanged.
1. Search moved to the CENTER of the top bar as a wide box ("Search
conversations & projects… ⌘K"), matching the target — was top-right. Pure
reposition/restyle of the trigger; the ⌘K palette logic (#81) is untouched
(still opens + routes). Mobile keeps the icon trigger.
2. Restored the stats panel dropped by the restyle (Conversations / Escalated /
Resolved / Avg rating) — real server aggregates, honest 0s when empty. Placed
as a compact 2-col grid at the top of the list pane (fits the narrow column)
and replaces the redundant "N total" line. This was a regression, not a cut.
Tests: top-bar.test (centered trigger fires onOpenSearch + ⌘K hint/placeholder);
InboxStats restored with its test; ListFilters.test updated for the stats prop.
Gates: dashboard 415 tests, tsc + lint + prettier clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Omarear
added a commit
that referenced
this pull request
Jun 24, 2026
…stly dimmed (#112) (#82) * feat(inbox): restyle to match target design — LIVE real, ROADMAP honestly dimmed (#112) Layout/styling pass over the inbox to match the target mockup while holding the LIVE/ROADMAP honesty contract (real data or honest empty states — never fabricated; unbuilt features render dimmed/"soon", non-functional, never wired). Left pane (ListFilters, new): "Inbox" + real total → conversation search → FILTER-BY-TAG chip row (All + each real workspace tag, replaces the popover — drives the same tagIds engine) → STATUS pills (LIVE Open/Resolved/Escalated/All + dimmed ROADMAP Unassigned/AI-handled). Thread header (ThreadActions, new): LIVE Resolve/Reopen + Delete (confirm, non-optimistic) moved here from the contact panel; Assign rendered dimmed/inert. Contact panel (DetailPanel): Copy email (LIVE) + merged CONTACT fields (Email/IP/Device/Started/Messages) + CSAT (real) + Tags add/remove (moved here) + Visitor-context kept as the honest "Not captured yet" em-dash block. Composer (ReplyComposer): Reply (LIVE) vs Internal-note (dimmed); Suggest-with-AI and attach dimmed/inert; Send live. - removed the stat-card strip + horizontal toolbar to match the mockup; the real total is shown in the list header (per-conversation CSAT stays). The escalated/resolved/avg aggregates are intentionally dropped (mockup has none). - top-bar ⌘K search left untouched: it's LIVE (#81) and shell, not inbox. - inbox project switcher kept (our per-project filter); archive==resolve so a single Resolve, not a fake Archive. No LIVE/ROADMAP annotation pills ship. - deleted InboxToolbar / InboxStats / TagFilter (+ tests), superseded. Adversarial review: honesty + layout reviewers found 0 issues; correctness flagged the (intentional) stats drop + a missing ReplyComposer test — test added. Gates: dashboard 409 tests, tsc + lint + prettier clean. Three-pane bounded layout intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(inbox): center the top-bar search + restore the LIVE stats panel (#112) Two placement fixes on the inbox restyle; honesty rail + ROADMAP dimming unchanged. 1. Search moved to the CENTER of the top bar as a wide box ("Search conversations & projects… ⌘K"), matching the target — was top-right. Pure reposition/restyle of the trigger; the ⌘K palette logic (#81) is untouched (still opens + routes). Mobile keeps the icon trigger. 2. Restored the stats panel dropped by the restyle (Conversations / Escalated / Resolved / Avg rating) — real server aggregates, honest 0s when empty. Placed as a compact 2-col grid at the top of the list pane (fits the narrow column) and replaces the redundant "N total" line. This was a regression, not a cut. Tests: top-bar.test (centered trigger fires onOpenSearch + ⌘K hint/placeholder); InboxStats restored with its test; ListFilters.test updated for the stats prop. Gates: dashboard 415 tests, tsc + lint + prettier clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds the top-bar ⌘K command palette from the target design — workspace-wide search across conversations and projects — plus the inbox deep-linkability it requires. Scope is the ⌘K half of #101 (project-status is a separate slice); sources deferred; #105 not built (but its scope helper is). Branched from
main, independent of PR #80.Backend —
GET /api/search?q=requireSession+requireWorkspace, returns{ conversations, projects }.lib/workspace-scope.tsworkspaceProjectIds(env, workspaceId)— derived from the membership-validatedc.get("workspaceId"), reusable by the future #105 unified inbox.inArray(conversation.projectId, ids)inside the base WHERE (composes with caps, never bolted on afterLIMIT); empty-set guard short-circuits before anyIN ();and()never bareeq().message→conversation→projectpath, bounded byMAX_MATCH_CONVERSATIONS) + project name. Reuseslib/search.ts(likeContainsinjection-safe,buildSnippet). Per-entity caps (8 conv / 5 proj). Never returnspublicKey/inboundEmailLocal/systemPrompt/knowledgeText.Frontend — palette + deep-link
cmdk/CommandDialog(no new deps).api()). Honesty rail: live-gated rows, "Searching…" while pending, "No matches" only after a settled query — real rows only./inbox?project=&c=, project →/settings/projects/<id>.?project=&c=once on mount and mirrors the open thread to the URL viahistory.replaceState(shareable threads). The thread endpoint re-validates tenancy, so a forgedcyields an empty thread — never a leak.CommandDialognow forwards cmdkCommandprops (fixesshouldFiltersilently dropping) + adds an sr-onlyDialogTitle;api()gains an optionalAbortSignal.Tests — the deliverable
Real-sqlite e2e proving the cross-workspace boundary across all three arms (name/email, message-body two-hop, project), the empty-set guard (empty, not all-rows, not 500), secret-omission, and the 403 membership gate (a forged
x-workspace-idcan't pivot). Plus palette honesty-rail + routing, and inbox deep-link open + URL-sync.Adversarial review
3 parallel reviewers (security/IDOR · correctness · frontend) → per-finding verification. Tenant boundary: no constructible leak across all 12 traced query paths. 3 confirmed low/nit findings, all applied: project-name lookup now self-asserts
workspaceId(and()-never-bare-eq()); palette no longer flashes stale rows on quick reopen / workspace switch; ⌘K respectsdefaultPrevented.Gates
API 376 + dashboard 401 tests pass; both typecheck clean; lint 7/7; prettier clean.
Deploy note: touches the dashboard → preview runs the
kind: nextjsbuilder; if it fails withNo executable pnpm found, that's the known transient Ploy flake — re-trigger once.🤖 Generated with Claude Code