pilot: move context pipette to left rail with chip replacement

This commit is contained in:
Ruslan Bakiev
2026-02-23 07:45:28 +07:00
parent 70c095bc67
commit 38fcb1bfcc

View File

@@ -9,6 +9,7 @@ import logPilotNoteMutation from "./graphql/operations/log-pilot-note.graphql?ra
import createCalendarEventMutation from "./graphql/operations/create-calendar-event.graphql?raw";
import archiveCalendarEventMutation from "./graphql/operations/archive-calendar-event.graphql?raw";
import createCommunicationMutation from "./graphql/operations/create-communication.graphql?raw";
import createWorkspaceDocumentMutation from "./graphql/operations/create-workspace-document.graphql?raw";
import updateCommunicationTranscriptMutation from "./graphql/operations/update-communication-transcript.graphql?raw";
import updateFeedDecisionMutation from "./graphql/operations/update-feed-decision.graphql?raw";
import chatConversationsQuery from "./graphql/operations/chat-conversations.graphql?raw";
@@ -1623,8 +1624,12 @@ function normalizedConversationId() {
}
function currentUiPath() {
if (selectedTab.value !== "communications") {
return withReviewQuery(`/chat/${encodeURIComponent(normalizedConversationId())}`);
if (selectedTab.value === "documents") {
const docId = selectedDocumentId.value.trim();
if (docId) {
return withReviewQuery(`/documents/${encodeURIComponent(docId)}`);
}
return withReviewQuery("/documents");
}
if (peopleLeftMode.value === "calendar") {
@@ -1756,6 +1761,15 @@ function applyPathToUi(pathname: string, search = "") {
return;
}
const documentsMatch = path.match(/^\/documents(?:\/([^/]+))?\/?$/i);
if (documentsMatch) {
const rawDocumentId = decodeURIComponent(documentsMatch[1] ?? "").trim();
selectedTab.value = "documents";
focusedCalendarEventId.value = "";
if (rawDocumentId) selectedDocumentId.value = rawDocumentId;
return;
}
const contactMatch = path.match(/^\/contact\/([^/]+)\/?$/i);
if (contactMatch) {
const rawContactId = decodeURIComponent(contactMatch[1] ?? "").trim();
@@ -2789,6 +2803,40 @@ const selectedContactRecentMessages = computed(() => {
.slice(0, 8);
});
const CONTACT_DOCUMENT_SCOPE_PREFIX = "contact:";
function buildContactDocumentScope(contactId: string, contactName: string) {
return `${CONTACT_DOCUMENT_SCOPE_PREFIX}${encodeURIComponent(contactId)}:${encodeURIComponent(contactName)}`;
}
function parseContactDocumentScope(scope: string) {
const raw = String(scope ?? "").trim();
if (!raw.startsWith(CONTACT_DOCUMENT_SCOPE_PREFIX)) return null;
const payload = raw.slice(CONTACT_DOCUMENT_SCOPE_PREFIX.length);
const [idRaw, ...nameParts] = payload.split(":");
const contactId = decodeURIComponent(idRaw ?? "").trim();
const contactName = decodeURIComponent(nameParts.join(":") ?? "").trim();
if (!contactId) return null;
return {
contactId,
contactName,
};
}
function formatDocumentScope(scope: string) {
const linked = parseContactDocumentScope(scope);
if (!linked) return scope;
return linked.contactName ? `Contact · ${linked.contactName}` : "Contact document";
}
function isDocumentLinkedToContact(doc: WorkspaceDocument, contact: Contact | null | undefined) {
if (!contact) return false;
const linked = parseContactDocumentScope(doc.scope);
if (!linked) return false;
if (linked.contactId) return linked.contactId === contact.id;
return Boolean(linked.contactName && linked.contactName === contact.name);
}
const documentSearch = ref("");
const selectedDocumentType = ref<"All" | WorkspaceDocument["type"]>("All");
@@ -2803,7 +2851,7 @@ const filteredDocuments = computed(() => {
.filter((item) => {
if (selectedDocumentType.value !== "All" && item.type !== selectedDocumentType.value) return false;
if (!query) return true;
const haystack = [item.title, item.summary, item.owner, item.scope, item.body].join(" ").toLowerCase();
const haystack = [item.title, item.summary, item.owner, formatDocumentScope(item.scope), item.body].join(" ").toLowerCase();
return haystack.includes(query);
})
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
@@ -2825,12 +2873,14 @@ watchEffect(() => {
const selectedDocument = computed(() => documents.value.find((item) => item.id === selectedDocumentId.value));
function openPilotInstructions() {
function openDocumentsTab(push = false) {
selectedTab.value = "documents";
focusedCalendarEventId.value = "";
if (!selectedDocumentId.value && filteredDocuments.value.length) {
const first = filteredDocuments.value[0];
if (first) selectedDocumentId.value = first.id;
}
syncPathFromUi(push);
}
const peopleListMode = ref<"contacts" | "deals">("contacts");
@@ -2936,6 +2986,7 @@ watch(
focusedCalendarEventId.value,
selectedContactId.value,
selectedDealId.value,
selectedDocumentId.value,
activeChangeSetId.value,
activeChangeStep.value,
],
@@ -2950,7 +3001,7 @@ const commPinnedOnly = ref(false);
const commDraft = ref("");
const commSending = ref(false);
const commRecording = ref(false);
const commComposerMode = ref<"message" | "planned" | "logged">("message");
const commComposerMode = ref<"message" | "planned" | "logged" | "document">("message");
const commQuickMenuOpen = ref(false);
const commEventSaving = ref(false);
const commEventError = ref("");
@@ -2960,6 +3011,13 @@ const commEventForm = ref({
startTime: "",
durationMinutes: 30,
});
const commDocumentForm = ref<{
type: WorkspaceDocument["type"];
title: string;
}>({
type: "Template",
title: "",
});
const eventCloseOpen = ref<Record<string, boolean>>({});
const eventCloseDraft = ref<Record<string, string>>({});
const eventCloseSaving = ref<Record<string, boolean>>({});
@@ -2985,6 +3043,7 @@ watch(selectedCommThreadId, () => {
commComposerMode.value = "message";
commQuickMenuOpen.value = false;
commEventError.value = "";
commDocumentForm.value = { type: "Template", title: "" };
eventCloseOpen.value = {};
eventCloseDraft.value = {};
eventCloseSaving.value = {};
@@ -3321,6 +3380,34 @@ const selectedWorkspaceContact = computed(() => {
return contacts.value[0] ?? null;
});
const contactRightPanelMode = ref<"summary" | "documents">("summary");
const contactDocumentsSearch = ref("");
const selectedWorkspaceContactDocuments = computed(() => {
const contact = selectedWorkspaceContact.value;
if (!contact) return [];
return documents.value
.filter((doc) => isDocumentLinkedToContact(doc, contact))
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
});
const filteredSelectedWorkspaceContactDocuments = computed(() => {
const query = contactDocumentsSearch.value.trim().toLowerCase();
if (!query) return selectedWorkspaceContactDocuments.value;
return selectedWorkspaceContactDocuments.value.filter((doc) => {
const haystack = [doc.title, doc.summary, doc.owner, formatDocumentScope(doc.scope), doc.body].join(" ").toLowerCase();
return haystack.includes(query);
});
});
watch(
() => selectedWorkspaceContact.value?.id ?? "",
() => {
contactRightPanelMode.value = "summary";
contactDocumentsSearch.value = "";
},
);
const selectedWorkspaceDeal = computed(() => {
const explicit = deals.value.find((deal) => deal.id === selectedDealId.value);
if (explicit) return explicit;
@@ -3723,6 +3810,13 @@ function setDefaultCommEventForm(mode: "planned" | "logged") {
};
}
function setDefaultCommDocumentForm() {
commDocumentForm.value = {
type: "Template",
title: "",
};
}
function openCommEventModal(mode: "planned" | "logged") {
if (!selectedCommThread.value) return;
commEventMode.value = mode;
@@ -3732,10 +3826,19 @@ function openCommEventModal(mode: "planned" | "logged") {
commQuickMenuOpen.value = false;
}
function openCommDocumentModal() {
if (!selectedCommThread.value) return;
setDefaultCommDocumentForm();
commEventError.value = "";
commComposerMode.value = "document";
commQuickMenuOpen.value = false;
}
function closeCommEventModal() {
if (commEventSaving.value) return;
commComposerMode.value = "message";
commEventError.value = "";
setDefaultCommDocumentForm();
commQuickMenuOpen.value = false;
}
@@ -3751,6 +3854,7 @@ function closeCommQuickMenu() {
function commComposerPlaceholder() {
if (commComposerMode.value === "planned") return "Опиши, что нужно запланировать...";
if (commComposerMode.value === "logged") return "Опиши итог/отчёт по прошедшему событию...";
if (commComposerMode.value === "document") return "Опиши документ или вложение для контакта...";
return "Type a message...";
}
@@ -3763,6 +3867,15 @@ function buildCommEventTitle(text: string, mode: "planned" | "logged", contact:
return mode === "logged" ? `Отчёт по контакту ${contact}` : `Событие с ${contact}`;
}
function buildCommDocumentTitle(text: string, contact: string) {
const cleaned = text.replace(/\s+/g, " ").trim();
if (cleaned) {
const sentence = cleaned.split(/[.!?\n]/)[0]?.trim() ?? "";
if (sentence) return sentence.slice(0, 120);
}
return `Документ для ${contact}`;
}
async function createCommEvent() {
if (!selectedCommThread.value || commEventSaving.value) return;
@@ -3816,6 +3929,47 @@ async function createCommEvent() {
}
}
async function createCommDocument() {
if (!selectedCommThread.value || commEventSaving.value) return;
const summary = commDraft.value.trim();
if (!summary) {
commEventError.value = "Текст документа обязателен";
return;
}
const title = commDocumentForm.value.title.trim() || buildCommDocumentTitle(summary, selectedCommThread.value.contact);
const scope = buildContactDocumentScope(selectedCommThread.value.id, selectedCommThread.value.contact);
const body = summary;
commEventSaving.value = true;
commEventError.value = "";
try {
const res = await gqlFetch<{ createWorkspaceDocument: WorkspaceDocument }>(createWorkspaceDocumentMutation, {
input: {
title,
type: commDocumentForm.value.type,
owner: authDisplayName.value,
scope,
summary,
body,
},
});
documents.value = [res.createWorkspaceDocument, ...documents.value.filter((doc) => doc.id !== res.createWorkspaceDocument.id)];
selectedDocumentId.value = res.createWorkspaceDocument.id;
contactRightPanelMode.value = "documents";
commDraft.value = "";
commComposerMode.value = "message";
commEventError.value = "";
setDefaultCommDocumentForm();
} catch (error: any) {
commEventError.value = String(error?.message ?? error ?? "Failed to create document");
} finally {
commEventSaving.value = false;
}
}
async function sendCommMessage() {
const text = commDraft.value.trim();
if (!text || commSending.value || !selectedCommThread.value) return;
@@ -3850,10 +4004,18 @@ function toggleCommRecording() {
function handleCommComposerEnter(event: KeyboardEvent) {
if (event.shiftKey) return;
event.preventDefault();
handleCommComposerSubmit();
}
function handleCommComposerSubmit() {
if (commComposerMode.value === "message") {
void sendCommMessage();
return;
}
if (commComposerMode.value === "document") {
void createCommDocument();
return;
}
void createCommEvent();
}
@@ -4011,15 +4173,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
<div>
<h2 class="text-sm font-semibold text-white/75">{{ pilotHeaderText }}</h2>
</div>
<button
class="btn btn-ghost btn-xs btn-square text-white/80 hover:bg-white/10"
title="Instructions"
@click="openPilotInstructions"
>
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current">
<path d="M19.14 12.94a7.43 7.43 0 0 0 .05-.94 7.43 7.43 0 0 0-.05-.94l2.03-1.58a.5.5 0 0 0 .12-.64l-1.92-3.32a.5.5 0 0 0-.6-.22l-2.39.96a7.28 7.28 0 0 0-1.62-.94l-.36-2.54A.5.5 0 0 0 14.9 2h-3.8a.5.5 0 0 0-.49.42l-.36 2.54c-.58.22-1.12.53-1.62.94l-2.39-.96a.5.5 0 0 0-.6.22L3.72 8.84a.5.5 0 0 0 .12.64l2.03 1.58c-.03.31-.05.63-.05.94s.02.63.05.94l-2.03 1.58a.5.5 0 0 0-.12.64l1.92 3.32c.13.23.4.32.64.22l2.39-.96c.5.41 1.04.72 1.62.94l.36 2.54c.04.24.25.42.49.42h3.8c.24 0 .45-.18.49-.42l.36-2.54c.58-.22 1.12-.53 1.62-.94l2.39.96c.24.1.51.01.64-.22l1.92-3.32a.5.5 0 0 0-.12-.64zM13 15.5A3.5 3.5 0 1 1 13 8.5a3.5 3.5 0 0 1 0 7z" />
</svg>
</button>
</div>
<div class="pilot-threads">
<div class="flex w-full items-center justify-between gap-2">
@@ -4190,35 +4343,53 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
</div>
</div>
<div class="pilot-input-wrap">
<div class="pilot-input-shell">
<textarea
v-model="pilotInput"
class="pilot-input-textarea"
<div class="pilot-input-wrap">
<div class="pilot-input-shell">
<textarea
v-model="pilotInput"
class="pilot-input-textarea"
:placeholder="pilotRecording ? 'Recording... speak, then press mic to fill or send to submit' : 'Type a message for Pilot...'"
@keydown.enter="handlePilotComposerEnter"
/>
<div v-if="pilotRecording" class="pilot-meter">
<div ref="pilotWaveContainer" class="pilot-wave-canvas" />
</div>
<div v-if="pilotRecording" class="pilot-meter">
<div ref="pilotWaveContainer" class="pilot-wave-canvas" />
</div>
<div class="pilot-input-actions">
<button
class="btn btn-xs btn-circle border border-white/20 bg-transparent text-white/90 hover:bg-white/10"
:class="contextPickerEnabled ? 'pilot-mic-active' : ''"
:disabled="pilotTranscribing || pilotSending"
:title="contextPickerEnabled ? 'Выключить пипетку' : 'Включить пипетку контекста'"
@click="toggleContextPicker"
>
<svg viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
<path d="M19.29 4.71a1 1 0 0 0-1.42 0l-2.58 2.58-1.17-1.17a1 1 0 0 0-1.41 0l-1.42 1.42a1 1 0 0 0 0 1.41l.59.59-5.3 5.3a2 2 0 0 0-.53.92l-.86 3.43a1 1 0 0 0 1.21 1.21l3.43-.86a2 2 0 0 0 .92-.53l5.3-5.3.59.59a1 1 0 0 0 1.41 0l1.42-1.42a1 1 0 0 0 0-1.41l-1.17-1.17 2.58-2.58a1 1 0 0 0 0-1.42z" />
</svg>
</button>
<button
class="btn btn-xs btn-circle border border-white/20 bg-transparent text-white/90 hover:bg-white/10"
:class="pilotRecording ? 'pilot-mic-active' : ''"
:disabled="!pilotMicSupported || pilotTranscribing || pilotSending"
<div v-if="!pilotRecording" class="pilot-input-context">
<button
v-if="contextScopeChips.length === 0"
class="context-pipette-trigger"
:class="contextPickerEnabled ? 'context-pipette-active' : ''"
:disabled="pilotTranscribing || pilotSending"
:title="contextPickerEnabled ? 'Выключить пипетку' : 'Включить пипетку контекста'"
@click="toggleContextPicker"
>
<svg viewBox="0 0 24 24" class="h-3.5 w-3.5 fill-current">
<path d="M19.29 4.71a1 1 0 0 0-1.42 0l-2.58 2.58-1.17-1.17a1 1 0 0 0-1.41 0l-1.42 1.42a1 1 0 0 0 0 1.41l.59.59-5.3 5.3a2 2 0 0 0-.53.92l-.86 3.43a1 1 0 0 0 1.21 1.21l3.43-.86a2 2 0 0 0 .92-.53l5.3-5.3.59.59a1 1 0 0 0 1.41 0l1.42-1.42a1 1 0 0 0 0-1.41l-1.17-1.17 2.58-2.58a1 1 0 0 0 0-1.42z" />
</svg>
<span>Контекст</span>
</button>
<div v-else class="pilot-context-chips">
<button
v-for="chip in contextScopeChips"
:key="`context-chip-${chip.scope}`"
type="button"
class="context-pipette-chip"
@click="removeContextScope(chip.scope)"
>
{{ chip.label }}
<span class="opacity-70">×</span>
</button>
</div>
</div>
<div class="pilot-input-actions">
<button
class="btn btn-xs btn-circle border border-white/20 bg-transparent text-white/90 hover:bg-white/10"
:class="pilotRecording ? 'pilot-mic-active' : ''"
:disabled="!pilotMicSupported || pilotTranscribing || pilotSending"
:title="pilotRecording ? 'Stop and insert transcript' : 'Voice input'"
@click="togglePilotRecording"
>
@@ -4237,23 +4408,11 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current" :class="pilotSending ? 'opacity-50' : ''">
<path d="M4.5 19.5 21 12 4.5 4.5l.02 5.84L15 12l-10.48 1.66z" />
</svg>
</button>
</div>
</div>
<div v-if="contextScopeChips.length" class="mt-1 flex flex-wrap items-center gap-1.5 px-1">
<button
v-for="chip in contextScopeChips"
:key="`context-chip-${chip.scope}`"
type="button"
class="inline-flex items-center gap-1 rounded border border-white/25 bg-white/10 px-2 py-0.5 text-[10px] text-white/90 hover:bg-white/15"
@click="removeContextScope(chip.scope)"
>
{{ chip.label }}
<span class="opacity-70">×</span>
</button>
</div>
<p v-if="pilotMicError" class="pilot-mic-error">{{ pilotMicError }}</p>
</div>
</button>
</div>
</div>
<p v-if="pilotMicError" class="pilot-mic-error">{{ pilotMicError }}</p>
</div>
</div>
</aside>
@@ -4261,11 +4420,11 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
<div class="flex h-full min-h-0 flex-col">
<div class="workspace-topbar border-b border-base-300 px-3 py-2 md:px-4">
<div class="flex items-center justify-between gap-3">
<div v-if="selectedTab === 'communications'" class="join">
<div class="join">
<button
class="btn btn-sm join-item"
:class="
peopleLeftMode === 'contacts'
selectedTab === 'communications' && peopleLeftMode === 'contacts'
? 'btn-ghost border border-base-300 bg-base-200/70 text-base-content'
: 'btn-ghost border border-transparent text-base-content/65 hover:border-base-300/70 hover:text-base-content'
"
@@ -4276,7 +4435,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
<button
class="btn btn-sm join-item"
:class="
peopleLeftMode === 'calendar'
selectedTab === 'communications' && peopleLeftMode === 'calendar'
? 'btn-ghost border border-base-300 bg-base-200/70 text-base-content'
: 'btn-ghost border border-transparent text-base-content/65 hover:border-base-300/70 hover:text-base-content'
"
@@ -4284,8 +4443,18 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
>
Calendar
</button>
<button
class="btn btn-sm join-item"
:class="
selectedTab === 'documents'
? 'btn-ghost border border-base-300 bg-base-200/70 text-base-content'
: 'btn-ghost border border-transparent text-base-content/65 hover:border-base-300/70 hover:text-base-content'
"
@click="openDocumentsTab(true)"
>
Documents
</button>
</div>
<div v-else />
<div class="dropdown dropdown-end">
<button tabindex="0" class="btn btn-sm btn-ghost gap-2">
@@ -5233,7 +5402,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
tabindex="0"
type="button"
class="btn btn-sm btn-circle border border-base-300 bg-base-100 text-base-content/85 hover:bg-base-200"
title="Add event"
title="Add item"
@click.stop="toggleCommQuickMenu"
>
+
@@ -5249,6 +5418,11 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
Log past event
</button>
</li>
<li>
<button @click="openCommDocumentModal">
Attach document
</button>
</li>
</ul>
</div>
</div>
@@ -5270,10 +5444,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
@keydown.enter="handleCommComposerEnter"
/>
<div
v-if="commComposerMode !== 'message'"
class="comm-event-controls"
>
<div v-if="commComposerMode === 'planned' || commComposerMode === 'logged'" class="comm-event-controls">
<input
v-model="commEventForm.startDate"
type="date"
@@ -5298,6 +5469,25 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
<option :value="90">90m</option>
</select>
</div>
<div v-else-if="commComposerMode === 'document'" class="comm-event-controls">
<input
v-model="commDocumentForm.title"
type="text"
class="input input-bordered input-xs h-7 min-h-7 flex-1"
:disabled="commEventSaving"
placeholder="Document title (optional)"
>
<select
v-model="commDocumentForm.type"
class="select select-bordered select-xs h-7 min-h-7"
:disabled="commEventSaving"
>
<option value="Regulation">Regulation</option>
<option value="Playbook">Playbook</option>
<option value="Policy">Policy</option>
<option value="Template">Template</option>
</select>
</div>
<p v-if="commEventError && commComposerMode !== 'message'" class="comm-event-error text-xs text-error">
{{ commEventError }}
@@ -5352,8 +5542,16 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
<button
class="btn btn-sm btn-circle border-0 bg-[#5865f2] text-white hover:bg-[#4752c4]"
:disabled="commSending || commEventSaving || !commDraft.trim() || (commComposerMode === 'message' && !commSendChannel)"
:title="commComposerMode === 'message' ? `Send via ${commSendChannel}` : (commComposerMode === 'logged' ? 'Save log event' : 'Create event')"
@click="commComposerMode === 'message' ? sendCommMessage() : createCommEvent()"
:title="
commComposerMode === 'message'
? `Send via ${commSendChannel}`
: commComposerMode === 'logged'
? 'Save log event'
: commComposerMode === 'document'
? 'Save document'
: 'Create event'
"
@click="handleCommComposerSubmit"
>
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current" :class="commSending ? 'opacity-50' : ''">
<path d="M4.5 19.5 21 12 4.5 4.5l.02 5.84L15 12l-10.48 1.66z" />
@@ -5977,6 +6175,83 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
gap: 6px;
}
.pilot-input-context {
position: absolute;
left: 10px;
bottom: 8px;
right: 96px;
display: flex;
align-items: center;
min-height: 24px;
overflow: hidden;
}
.context-pipette-trigger {
display: inline-flex;
align-items: center;
gap: 6px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.28);
background: rgba(255, 255, 255, 0.06);
color: rgba(245, 247, 255, 0.94);
padding: 4px 10px;
font-size: 11px;
line-height: 1;
transition: transform 220ms ease, border-color 220ms ease, background-color 220ms ease;
}
.context-pipette-trigger:hover {
transform: scale(1.03);
border-color: color-mix(in oklab, var(--color-primary) 62%, transparent);
background: color-mix(in oklab, var(--color-primary) 18%, transparent);
}
.context-pipette-active {
border-color: color-mix(in oklab, var(--color-primary) 72%, transparent);
background: color-mix(in oklab, var(--color-primary) 24%, transparent);
box-shadow: 0 0 0 1px color-mix(in oklab, var(--color-primary) 38%, transparent) inset;
}
.pilot-context-chips {
display: flex;
align-items: center;
gap: 6px;
max-width: 100%;
overflow-x: auto;
padding-bottom: 1px;
}
.pilot-context-chips::-webkit-scrollbar {
height: 3px;
}
.pilot-context-chips::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.24);
border-radius: 999px;
}
.context-pipette-chip {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 6px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.28);
background: rgba(255, 255, 255, 0.08);
color: rgba(245, 247, 255, 0.95);
padding: 4px 9px;
font-size: 10px;
line-height: 1;
white-space: nowrap;
transition: transform 220ms ease, border-color 220ms ease, background-color 220ms ease;
}
.context-pipette-chip:hover {
transform: scale(1.03);
border-color: color-mix(in oklab, var(--color-primary) 62%, transparent);
background: color-mix(in oklab, var(--color-primary) 20%, transparent);
}
.pilot-meter {
position: absolute;
left: 12px;
@@ -6308,16 +6583,19 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
.context-scope-block {
position: relative;
cursor: crosshair;
transition: box-shadow 220ms ease, outline-color 220ms ease, transform 220ms ease;
}
.context-scope-block-active {
outline: 2px dashed color-mix(in oklab, var(--color-primary) 55%, transparent);
outline: 2px solid color-mix(in oklab, var(--color-primary) 58%, transparent);
outline-offset: 2px;
box-shadow: 0 0 0 1px color-mix(in oklab, var(--color-primary) 30%, transparent) inset;
}
.context-scope-block-selected {
outline: 2px solid color-mix(in oklab, var(--color-primary) 75%, transparent);
outline: 2px solid color-mix(in oklab, var(--color-primary) 72%, transparent);
outline-offset: 2px;
box-shadow: 0 0 0 2px color-mix(in oklab, var(--color-primary) 22%, transparent) inset;
}
.context-scope-label {