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 (
+
+
+
+
+
+
+
+ Read
+ {statusText}
+
+
+ );
+}
+
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;