diff --git a/vessel/src/app/api/raven-chat/__tests__/forecastIntent.test.ts b/vessel/src/app/api/raven-chat/__tests__/forecastIntent.test.ts index aff41bdff..e0c367079 100644 --- a/vessel/src/app/api/raven-chat/__tests__/forecastIntent.test.ts +++ b/vessel/src/app/api/raven-chat/__tests__/forecastIntent.test.ts @@ -341,4 +341,15 @@ test('cross-year symbolic weather declarations include explicit years', () => { }); assert.match(context.declaration, /2025/); assert.match(context.declaration, /2026/); +}); + +test('parses tonight, this evening, this morning, and this afternoon as today', () => { + const now = new Date('2026-06-04T12:00:00Z'); + const expected = { startDate: '2026-06-04', endDate: '2026-06-04' }; + + assert.deepEqual(parseSymbolicMomentDateRange('tell me about tonight', now), expected); + assert.deepEqual(parseSymbolicMomentDateRange('tell me about...tonight.', now), expected); + assert.deepEqual(parseSymbolicMomentDateRange('how is this evening looking?', now), expected); + assert.deepEqual(parseSymbolicMomentDateRange('what is active this morning?', now), expected); + assert.deepEqual(parseSymbolicMomentDateRange('read this afternoon', now), expected); }); \ No newline at end of file diff --git a/vessel/src/app/api/raven-chat/dateUtils.ts b/vessel/src/app/api/raven-chat/dateUtils.ts index ca7ee7f72..c74f97d6a 100644 --- a/vessel/src/app/api/raven-chat/dateUtils.ts +++ b/vessel/src/app/api/raven-chat/dateUtils.ts @@ -36,6 +36,9 @@ export function parseIsoDate(value: string): string | null { probe.getUTCDate() === day; return valid ? trimmed : null; } + +const SAME_DAY_PART_PATTERN = /\b(?:this\s+)?(?:tonight|evening|morning|afternoon)\b/i; + export function parseLooseDateToken(token: string, now: Date = new Date()): string | null { const trimmed = token .trim() @@ -58,7 +61,11 @@ export function parseLooseDateToken(token: string, now: Date = new Date()): stri } const normalized = trimmed.toLowerCase().replace(/,/g, ' ').replace(/\s+/g, ' ').trim(); const today = utcStartOfDay(now); - if (normalized === 'now' || normalized === 'today') { + if ( + normalized === 'now' + || normalized === 'today' + || SAME_DAY_PART_PATTERN.test(normalized) + ) { return toIsoDate(today); } if (normalized === 'tomorrow') { @@ -84,7 +91,7 @@ function extractLooseDateFromPhrase(phrase: string, now: Date = new Date()): str ...Array.from(phrase.matchAll(/\b\d{4}-\d{2}-\d{2}\b/g)).map((entry) => entry[0]), ...Array.from(phrase.matchAll(/\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sep(?:t|tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s*\d{4}|\s+\d{4})?\b/ig)).map((entry) => entry[0]), ...Array.from(phrase.matchAll(/\b\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?\b/g)).map((entry) => entry[0]), - ...Array.from(phrase.matchAll(/\b(?:now|today|tomorrow)\b/ig)).map((entry) => entry[0]), + ...Array.from(phrase.matchAll(/\b(?:now|today|tonight|tomorrow|(?:this\s+)?(?:evening|morning|afternoon))\b/ig)).map((entry) => entry[0]), ]; for (const candidate of candidates) { @@ -113,7 +120,7 @@ function buildForwardRange(startDate: string, spanDays: number = 14): SymbolicMo } const DATE_PHRASE_PREFIX = - String.raw`(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sep(?:t|tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s*\d{4}|\s+\d{4})?|\d{4}-\d{2}-\d{2}|\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?|today|tomorrow`; + String.raw`(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sep(?:t|tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s*\d{4}|\s+\d{4})?|\d{4}-\d{2}-\d{2}|\d{1,2}[/-]\d{1,2}(?:[/-]\d{2,4})?|today|tonight|tomorrow|(?:this\s+)?(?:evening|morning|afternoon)`; function daysBetweenIsoDates(startDate: string, endDate: string): number | null { const start = parseIsoDate(startDate); @@ -242,7 +249,7 @@ function parseSymbolicMomentDateRangeRaw(userMessage: string, now: Date = new Da endDate: toIsoDate(utcAddDays(today, span - 1)), }; } - if (/\btoday\b/.test(normalized)) { + if (/\btoday\b/.test(normalized) || SAME_DAY_PART_PATTERN.test(normalized)) { const iso = toIsoDate(today); return { startDate: iso, endDate: iso }; } diff --git a/vessel/src/app/page.tsx b/vessel/src/app/page.tsx index 8b323e885..6ef6fe61d 100644 --- a/vessel/src/app/page.tsx +++ b/vessel/src/app/page.tsx @@ -563,6 +563,30 @@ function buildRavenThinkingStatusText(input: { return 'Reading in flight…'; } +function WheelReadingResponseGlyph({ + isVisible, + statusText, +}: { + isVisible: boolean; + statusText: string; +}) { + if (!isVisible) return null; + + return ( +
+
+ ); +} + function BlindMirrorPanel({ blindMirror, disabled = false, @@ -8618,6 +8642,9 @@ export default function App() { const showGuidedMobileWheel = isCompactViewport && !sessionActive && !selectedChamber; const showGuidedMobileCard = isCompactViewport && !sessionActive && selectedChamber; const showGuidedMobileChat = isCompactViewport && sessionActive; + const guidedReadTabResponding = sessionActive && isLoading; + const guidedReadTabActive = guidedChatFocused || guidedReadTabResponding; + const wheelReadingStatusText = ravenThinkingStatusText || 'Raven is responding.'; const activeTimingConnections = (() => { const drivers = latestAssistantCorridorSnapshotForDisplay?.transitDrivers?.subjectA?.drivers || []; @@ -8756,7 +8783,11 @@ export default function App() { ✕ Close -
+
+
-
+
+ focusGuidedComposer()); }} className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[10px] font-mono uppercase tracking-wider transition-all duration-300 ${ - guidedChatFocused - ? 'bg-white/[0.06] border border-white/[0.12] text-slate-200' + guidedReadTabActive + ? cn( + 'bg-white/[0.06] border border-white/[0.12] text-slate-200', + guidedReadTabResponding && 'border-emerald-300/45 bg-emerald-500/10 text-emerald-100 shadow-[0_0_22px_rgba(16,185,129,0.22)] animate-pulse', + ) : 'border border-transparent text-slate-500 hover:text-slate-300 hover:bg-white/[0.03]' }`} > @@ -9718,6 +9756,10 @@ export default function App() { )}
+ {shouldShowSunStartPanel && sunWheelPlacement && (
+ {soloScopedProfile && soloScopedProfile.id !== activeProfile?.id && (
{soloScopedProfile.name} · Solo Mirror diff --git a/vessel/src/lib/raven/__tests__/symbolicMomentIntent.test.ts b/vessel/src/lib/raven/__tests__/symbolicMomentIntent.test.ts index b27d67d2f..c5cd6c129 100644 --- a/vessel/src/lib/raven/__tests__/symbolicMomentIntent.test.ts +++ b/vessel/src/lib/raven/__tests__/symbolicMomentIntent.test.ts @@ -418,6 +418,10 @@ test('detects timing-window planner scan phrases', () => { 'What is the high-valence date coming up?', 'When is the next positive bias window?', 'When does the next high valence window open?', + 'tell me about...tonight.', + 'how is this evening looking?', + 'what is active this morning?', + 'read this afternoon', ]; for (const msg of timingWindowQuestions) { assert.equal( @@ -444,6 +448,8 @@ test('does not flag ordinary timing follow-ups as timing-window planner scans', 'Will it get lighter tomorrow?', 'When will this pressure ease?', 'How soon does this settle?', + 'I am exhausted tonight.', + 'Maybe I should stop pushing tonight?', ]; for (const msg of nonTimingWindow) { assert.equal( diff --git a/vessel/src/lib/raven/symbolicMomentIntent.ts b/vessel/src/lib/raven/symbolicMomentIntent.ts index 3dc9aa5ec..1f497d9a1 100644 --- a/vessel/src/lib/raven/symbolicMomentIntent.ts +++ b/vessel/src/lib/raven/symbolicMomentIntent.ts @@ -98,6 +98,9 @@ const TIMING_WINDOW_INTENT_PATTERNS = [ /\bhigh[\s-]?valence\s+date\b/i, /\bwhen\s+is\s+my\s+next\s+(?:high[\s-]?valence|positive\s+bias)\b/i, /\bwhen\s+(?:is|does)\s+(?:the\s+)?(?:next|best)\s+(?:high[\s-]?valence|positive\s+bias|expansion)\b/i, + /\b(?:tell\s+me\s+about|read|show|check|pull|fetch|run)\b[\s.?!,;:…-]{0,20}\b(?:tonight|(?:this\s+)?(?:evening|morning|afternoon))\b/i, + /\b(?:what(?:s| is)|how(?:s| is))\b[\s.?!,;:…-]{0,80}\b(?:active|looking|running|happening|pressing|live|the\s+(?:field|sky|pressure|moment))\b[\s.?!,;:…-]{0,40}\b(?:tonight|(?:this\s+)?(?:evening|morning|afternoon))\b/i, + /\bhow(?:s| is)\b[\s.?!,;:…-]{0,20}\b(?:tonight|(?:this\s+)?(?:evening|morning|afternoon))\b[\s.?!,;:…-]{0,40}\b(?:looking|running|happening|landing|feeling)\b/i, ] as const; const NEW_SYMBOLIC_READ_PATTERNS = [ @@ -445,7 +448,7 @@ export function isSymbolicMomentsIntent( if (/\b(?:how\s+is)\s+(?:the\s+)?(?:sky|field|symbolic\s+moment|pressure)\b/i.test(normalized)) return true; if (/\b(?:what(?:s|'s| is))\s+(?:going\s+on|doing\s+on|happening)\s+(?:in\s+(?:the\s+)?(?:sky|field|chart|transits)|right\s+now|today|this\s+week|astrologically)\b/i.test(normalized)) return true; if (/\b(?:what(?:s|'s| is))\s+(?:doing|going)\s+on\s+right\s+now\b/i.test(normalized)) return true; - if (/\b(?:ask\s+about|tell\s+me\s+about)\s+(?:today|this\s+week|right\s+now|the\s+current\s+(?:sky|moment|field|pressure|symbolic\s+moment))\b/i.test(normalized)) return true; + if (/\b(?:ask\s+about|tell\s+me\s+about)\s+(?:today|tonight|this\s+(?:morning|afternoon|evening|week)|right\s+now|the\s+current\s+(?:sky|moment|field|pressure|symbolic\s+moment))\b/i.test(normalized)) return true; if (/\b(?:what(?:'s|s|\s+is)\s+(?:just\s+)?loud|just\s+what\s+(?:is|it)\s+loud|read\s+(?:what(?:'s|s|\s+is)\s+)?loud)\b/i.test(normalized)) return true;