Files
multica/packages/views/issues/components/issue-chip.tsx
Naiyuan Qing 2d0916ee38 feat(chat): focus mode — share current page as context (#1502)
* refactor(views): extract IssueChip shared primitive from mention card

IssueMention (in editor NodeView) and IssueMentionCard shared 95% of their
markup — StatusIcon + identifier + title inside a bordered chip. They drifted
into two parallel implementations so changes had to be made in two places.

Extract the presentational chip into IssueChip. The navigable variants
(IssueMentionCard, the editor NodeView) become thin shells that layer
routing + cmd/shift behaviour onto the shared chip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(chat): add focus mode to share current page as context

Adds a Focus button next to the chat submit. When on, the chat auto-attaches
whatever the user is viewing (issue, project, or inbox-selected issue) as a
context prefix on outgoing messages, so the agent knows what "this" refers
to without the user pasting ids.

The attached object is derived from the route + react-query cache on every
render — no separate copy in state. Only the boolean focusMode is persisted
(global to the user, not per-workspace), matching the "my preference"
mental model.

The button has three visual states driven by two dimensions (focusMode +
whether the current route resolves to an anchorable object):
  - off:         ghost + muted, click turns on
  - on  + anchor: secondary (bright), click turns off
  - on  + none:  disabled (nothing to attach here)

The derived anchor renders above the input as a chip — IssueChip for issues,
a new ProjectChip for projects — wrapped in AppLink so the visual target
matches the clickable target (mirrors IssueMentionCard's hover + navigation).

Prefix format reuses the editor's mention markdown:
  Context: [MUL-1](mention://issue/<uuid>) — "Fix login bug"
  Context: Project "Authentication"

so the agent sees an identical token whether the user @-mentioned inline or
focus-mode attached. Backend is untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:25:58 +08:00

67 lines
2.4 KiB
TypeScript

"use client";
import { useQuery } from "@tanstack/react-query";
import { issueListOptions, issueDetailOptions } from "@multica/core/issues/queries";
import { useWorkspaceId } from "@multica/core/hooks";
import { StatusIcon } from "./status-icon";
/**
* Compact, presentation-only representation of an issue —
* `<StatusIcon> <identifier> <title>`, bordered, truncating to max-w-72.
*
* This is the single source of truth for the "issue-mention card" look.
* It is intentionally **not** a link or button: callers wrap it in whatever
* interactive shell they need (AppLink for markdown mentions, an <a> with
* cmd-click support inside the editor's NodeView, a plain span next to a
* dismiss button in chat's context anchor card, …).
*
* Size budget: must fit within a 14px line-box when used inline — hence
* `py-0.5` + text-xs (see MentionView docstring for the math).
*/
export interface IssueChipProps {
issueId: string;
/** Shown when the issue can't be resolved (deleted, other workspace, …). */
fallbackLabel?: string;
/** Extra classes — callers layer interaction hints here
* (e.g. `hover:bg-accent cursor-pointer` for navigable variants). */
className?: string;
}
const BASE_CLASS =
"issue-mention inline-flex items-center gap-1.5 rounded-md border mx-0.5 px-2 py-0.5 text-xs max-w-72";
export function IssueChip({ issueId, fallbackLabel, className }: IssueChipProps) {
const wsId = useWorkspaceId();
const { data: issues = [] } = useQuery(issueListOptions(wsId));
const listIssue = issues.find((i) => i.id === issueId);
// Fallback fetch for issues outside the first page of the list (e.g. Done).
const { data: detailIssue } = useQuery({
...issueDetailOptions(wsId, issueId),
enabled: !listIssue,
});
const issue = listIssue ?? detailIssue;
const cls = className ? `${BASE_CLASS} ${className}` : BASE_CLASS;
if (!issue) {
return (
<span className={cls}>
<span className="font-medium text-muted-foreground">
{fallbackLabel ?? issueId.slice(0, 8)}
</span>
</span>
);
}
return (
<span className={cls}>
<StatusIcon status={issue.status} className="h-3.5 w-3.5 shrink-0" />
<span className="font-medium text-muted-foreground shrink-0">
{issue.identifier}
</span>
<span className="text-foreground truncate">{issue.title}</span>
</span>
);
}