From b6f74885914febdd77044b492370e470f498563f Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 21 Oct 2026 21:22:12 +0200 Subject: [PATCH] fix(assistant): green CI round, trip notes the assistant can keep, calmer change cards - the assistant keeps facts for a trip (remember_for_trip) as a change card, applied into the user's trip notes; existing expenses can be changed - change card fields as a few icon chips with the rest folded under Details, the day band marks only what changes, the week grid as compact tiles - no map button on receipts of things that have no place on the map - tests follow the composer's accessible name, the receipt's status icon and the GL map's container; the hanging stream helper honours an early abort - Postgres probe: qualified columns in the upsert updates; entities restored to the generator's output; strict ratchet reads the SDK client as untyped --- .../Admin/addons/useLlmParsingConfig.test.ts | 3 +- .../Assistant/AssistantChangeCard.test.tsx | 6 +- .../Assistant/AssistantChangeCard.tsx | 48 ++---- .../Assistant/AssistantChangeFields.tsx | 138 ++++++++++++++++++ .../Assistant/AssistantComposer.test.tsx | 2 +- .../components/Assistant/AssistantDayBand.tsx | 37 +++-- .../Assistant/AssistantSurface.test.tsx | 4 +- .../Assistant/AssistantWeekOverview.tsx | 36 +++-- .../Assistant/AssistantWindow.test.tsx | 12 +- .../components/Assistant/assistantModel.ts | 4 + .../Assistant/useAssistantPlaceDrop.test.tsx | 2 +- client/src/components/Map/MapViewGL.test.tsx | 2 + .../Map/useAssistantGhostMarkersGL.test.ts | 20 ++- .../Settings/IntegrationsTab.test.tsx | 3 +- .../Settings/ai/ClaudeConnectorCard.test.tsx | 3 +- client/src/store/assistantStore.test.ts | 16 +- client/src/store/assistantStore.ts | 2 + .../strict-typings/mcp-sdk-client.d.ts | 9 ++ .../db/entities/AssistantMessages.entity.ts | 4 +- .../db/entities/AssistantToolCalls.entity.ts | 3 +- .../repositories/AiConnections.repository.ts | 2 +- .../repositories/AssistantUsage.repository.ts | 6 +- server/src/nest-mcp/index.ts | 2 +- server/src/nest-mcp/registry.ts | 7 +- .../src/nest/assistant/assistant-context.ts | 2 +- server/src/nest/assistant/assistant-memory.ts | 60 ++++++++ .../assistant/assistant-tools.constants.ts | 18 ++- .../nest/assistant/assistant-tools.service.ts | 17 ++- server/src/nest/assistant/assistant.prompt.ts | 2 + .../src/nest/assistant/assistant.service.ts | 3 + .../assistant/tool-call-preview.helpers.ts | 31 +++- server/tests/integration/oauth.test.ts | 2 + .../model-catalog.service.test.ts | 11 +- server/tsconfig.strict.json | 3 +- shared/src/assistant/assistant.schema.ts | 4 +- shared/src/i18n/ar/assistant.ts | 4 + shared/src/i18n/az/assistant.ts | 4 + shared/src/i18n/br/assistant.ts | 4 + shared/src/i18n/ca/assistant.ts | 4 + shared/src/i18n/cs/assistant.ts | 4 + shared/src/i18n/de/assistant.ts | 4 + shared/src/i18n/en/assistant.ts | 4 + shared/src/i18n/es/assistant.ts | 4 + shared/src/i18n/et/assistant.ts | 4 + shared/src/i18n/fr/assistant.ts | 4 + shared/src/i18n/gr/assistant.ts | 4 + shared/src/i18n/hu/assistant.ts | 4 + shared/src/i18n/id/assistant.ts | 4 + shared/src/i18n/it/assistant.ts | 4 + shared/src/i18n/ja/assistant.ts | 4 + shared/src/i18n/ko/assistant.ts | 4 + shared/src/i18n/nl/assistant.ts | 4 + shared/src/i18n/pl/assistant.ts | 4 + shared/src/i18n/ru/assistant.ts | 4 + shared/src/i18n/sk/assistant.ts | 4 + shared/src/i18n/sv/assistant.ts | 4 + shared/src/i18n/th/assistant.ts | 4 + shared/src/i18n/tr/assistant.ts | 4 + shared/src/i18n/uk/assistant.ts | 4 + shared/src/i18n/vi/assistant.ts | 4 + shared/src/i18n/zh-TW/assistant.ts | 4 + shared/src/i18n/zh/assistant.ts | 4 + shared/src/place/place-kind.ts | 10 +- 63 files changed, 509 insertions(+), 133 deletions(-) create mode 100644 client/src/components/Assistant/AssistantChangeFields.tsx create mode 100644 server/scripts/strict-typings/mcp-sdk-client.d.ts create mode 100644 server/src/nest/assistant/assistant-memory.ts diff --git a/client/src/components/Admin/addons/useLlmParsingConfig.test.ts b/client/src/components/Admin/addons/useLlmParsingConfig.test.ts index 11d2029df..1b8dc8b2e 100644 --- a/client/src/components/Admin/addons/useLlmParsingConfig.test.ts +++ b/client/src/components/Admin/addons/useLlmParsingConfig.test.ts @@ -161,7 +161,8 @@ describe('useLlmParsingConfig', () => { await act(async () => result.current.setVision('off')); act(() => result.current.setProvider('anthropic')); await act(() => result.current.save()); - expect(vi.mocked(adminApi.updateAddon).mock.calls.at(-1)?.[1]).toEqual({ + const calls = vi.mocked(adminApi.updateAddon).mock.calls; + expect(calls[calls.length - 1]?.[1]).toEqual({ config: { provider: 'anthropic', model: '', baseUrl: '', apiKey: '', vision: 'off' }, }); }); diff --git a/client/src/components/Assistant/AssistantChangeCard.test.tsx b/client/src/components/Assistant/AssistantChangeCard.test.tsx index db833721c..b3dd0f9af 100644 --- a/client/src/components/Assistant/AssistantChangeCard.test.tsx +++ b/client/src/components/Assistant/AssistantChangeCard.test.tsx @@ -166,7 +166,7 @@ describe('AssistantChangeCard: the receipt', () => { const onShowOnMap = vi.fn(); renderCard([receipt('c1', '2026-10-21T08:15:00.000Z')], { onShowOnMap }); - expect(screen.getByText('Applied')).toBeInTheDocument(); + expect(screen.getByRole('img', { name: 'Applied' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Show on map' })); expect(onShowOnMap).toHaveBeenCalledWith(expect.objectContaining({ toolCallId: 'c1', dayId: 3 })); expect(screen.getByRole('button', { name: 'Undo' })).toBeInTheDocument(); @@ -188,7 +188,7 @@ describe('AssistantChangeCard: the receipt', () => { fireEvent.click(screen.getByRole('button', { name: 'Undo' })); }); expect(undo).toHaveBeenCalledWith(TRIP, 'c1'); - expect(screen.getByText('Undone')).toBeInTheDocument(); + expect(screen.getByRole('img', { name: 'Undone' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Undo' })).toBeNull(); first.unmount(); @@ -204,7 +204,7 @@ describe('AssistantChangeCard: the receipt', () => { renderCard([receipt('c1', null), { ...pending('c2', 'Ryoan-ji'), state: 'discarded' }]); expect(screen.queryByRole('button', { name: 'Show on map' })).toBeNull(); expect(screen.queryByRole('button', { name: 'Undo' })).toBeNull(); - expect(screen.getByText('Discarded')).toBeInTheDocument(); + expect(screen.getByRole('img', { name: 'Discarded' })).toBeInTheDocument(); }); }); diff --git a/client/src/components/Assistant/AssistantChangeCard.tsx b/client/src/components/Assistant/AssistantChangeCard.tsx index 007e16b88..20382c138 100644 --- a/client/src/components/Assistant/AssistantChangeCard.tsx +++ b/client/src/components/Assistant/AssistantChangeCard.tsx @@ -1,6 +1,5 @@ -import type { AssistantChangePreview, AssistantChangeRow, AssistantChangeVerbKey } from '@trek/shared'; +import type { AssistantApplied, AssistantChangePreview, AssistantChangeVerbKey } from '@trek/shared'; import { - ArrowRight, CalendarArrowUp, CalendarMinus, CalendarPlus, @@ -11,6 +10,7 @@ import { Luggage, MapPin, MapPinPlus, + NotebookPen, PencilLine, StickyNote, Ticket, @@ -27,10 +27,11 @@ import { useTripStore } from '../../store/tripStore'; import ToggleSwitch from '../Settings/ToggleSwitch'; import { useToast } from '../shared/Toast'; import { Tooltip } from '../shared/Tooltip'; +import AssistantChangeFields from './AssistantChangeFields'; import AssistantCompareSwitch from './AssistantCompareSwitch'; import AssistantDayBand from './AssistantDayBand'; import AssistantWeekOverview from './AssistantWeekOverview'; -import { CHANGE_LABELS, type ChangeItem, type ChangeState, undoLeftMs } from './assistantParts'; +import { type ChangeItem, type ChangeState, undoLeftMs } from './assistantParts'; import { useAssistantView } from './assistantViewContext'; import { changePoints, type DayChange } from './dayAfterModel'; import { useChangeHover } from './useChangeHover'; @@ -78,8 +79,10 @@ const VERB_LOOK: Record{t('assistant.change.empty')}; - return ( - - - {t(CHANGE_LABELS[row.labelKey].labelKey)} - - {row.op !== 'add' && ( - <> - {row.before || empty} - - ); -} - -function ChangeFields({ rows }: { rows: AssistantChangeRow[] }) { - if (rows.length === 0) return null; - return ( -
- {rows.map((row, i) => ( - - ))} -
- ); -} - /** The time now, ticking once more when the next undo window closes. */ function useUndoClock(items: ChangeItem[]): number { const [now, setNow] = useState(() => Date.now()); @@ -192,6 +164,9 @@ function dayChangesOf(items: readonly ChangeItem[], excluded: ReadonlySet = new Set(['place', 'assignment', 'day_note', 'reservation']); + function Receipt({ item, undone, @@ -209,6 +184,7 @@ function Receipt({ const receipt = undone ? RECEIPT.discarded : RECEIPT[item.state]; const { Icon } = receipt; const applied = item.state === 'applied' && !undone ? item.applied : null; + const mappable = applied !== null && ON_MAP.has(applied.entity); const settled = item.state === 'discarded' || item.state === 'expired' || undone; const statusLabel = t(undone ? 'assistant.change.undone' : receipt.labelKey); return ( @@ -217,7 +193,7 @@ function Receipt({
- {applied && onShowOnMap && ( + {applied && mappable && onShowOnMap && ( + {open && ( +
+ {rest.map((row, i) => ( + + ))} +
+ )} + + )} + + ); +} diff --git a/client/src/components/Assistant/AssistantComposer.test.tsx b/client/src/components/Assistant/AssistantComposer.test.tsx index e195b1efe..384a12c09 100644 --- a/client/src/components/Assistant/AssistantComposer.test.tsx +++ b/client/src/components/Assistant/AssistantComposer.test.tsx @@ -51,7 +51,7 @@ function seed(tripInPlanner = TRIP) { async function input(): Promise { render(); - return (await screen.findByPlaceholderText('Ask anything about Paris')) as HTMLTextAreaElement; + return (await screen.findByRole('textbox', { name: 'Ask anything about Paris' })) as HTMLTextAreaElement; } const mentions = () => chatOf(useAssistantStore.getState(), TRIP).mentions; diff --git a/client/src/components/Assistant/AssistantDayBand.tsx b/client/src/components/Assistant/AssistantDayBand.tsx index df1a5f791..3d94e6b9b 100644 --- a/client/src/components/Assistant/AssistantDayBand.tsx +++ b/client/src/components/Assistant/AssistantDayBand.tsx @@ -1,4 +1,4 @@ -import { ChevronDown, CircleCheck, TriangleAlert } from 'lucide-react'; +import { ChevronDown, CircleCheck, Minus, Plus, TriangleAlert } from 'lucide-react'; import { useMemo, useState } from 'react'; import { useShallow } from 'zustand/react/shallow'; @@ -20,8 +20,9 @@ import { useChangeHover } from './useChangeHover'; /** * The days a waiting card touches, as they would read once it ran: per day its - * date and its stops in order as small named chips, so a glance shows where the - * suggestion lands among the rest. The details (times, the way between stops, + * date and its stops in order. What stays reads as quiet text; only what the + * card adds is a filled pill and what it takes away is struck, so a glance + * finds the suggestion among the rest. The details (times, the way between stops, * free time, a clash) stay folded away until asked for. Two days at most; the * rest are counted. Shown only while the planner holds this trip, since the day * plans come from it. @@ -33,26 +34,33 @@ const MAX_DAYS = 2; const MAX_CHIPS = 6; const CHIP: Record = { - kept: 'border-transparent bg-surface-secondary text-content-muted', - new: 'border-dashed border-success bg-success-soft text-content', - removed: 'border-transparent text-content-faint line-through', + kept: 'px-0.5 text-content-muted', + new: 'rounded-full bg-accent px-2 py-0.5 font-medium text-accent-text shadow-sm', + removed: 'rounded-full px-1.5 py-0.5 text-content-faint line-through', }; /** One stop of the day, named; the change it belongs to lights up with it. */ function StopChip({ stop }: { stop: TimelineStop }) { const { tripId } = useAssistantView(); const { lit, handlers } = useChangeHover(tripId, stop.toolCallId); - const clash = stop.lead?.overlapsWith ? 'border-danger' : stopIssue(stop) ? 'border-warning' : ''; + const issue = stop.lead?.overlapsWith ? 'ring-2 ring-danger' : stopIssue(stop) ? 'ring-2 ring-warning' : ''; + const Mark = stop.status === 'new' ? Plus : stop.status === 'removed' ? Minus : null; return ( + {Mark && ); } +/** The thin rule between two stops, read as "then". */ +function Then() { + return