From 6fc7439d9bac82482ed82955428ef51aa2e72b83 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 18 Aug 2026 23:00:54 +0000 Subject: [PATCH] feat(console): Index calendar pane and 10DLC interrupt copy Mount LifeCalendarPane on Index rules, add the calendar-window API route, and state the SMS/10DLC limitation in AgentAliasPane and the urgent-lane empty state. Co-authored-by: Travis Gilbert --- .../agent-address/calendar-window/route.ts | 27 ++++ .../agent-address/AgentAliasPane.tsx | 5 + .../agent-address/LifeCalendarPane.tsx | 127 ++++++++++++++++++ .../src/lib/server/agent-address-harness.ts | 29 ++++ apps/console/src/views/IndexRulesView.tsx | 2 + apps/console/src/views/UrgentLaneView.tsx | 4 +- 6 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 apps/console/src/app/api/agent-address/calendar-window/route.ts create mode 100644 apps/console/src/components/agent-address/LifeCalendarPane.tsx diff --git a/apps/console/src/app/api/agent-address/calendar-window/route.ts b/apps/console/src/app/api/agent-address/calendar-window/route.ts new file mode 100644 index 00000000..46b5bac8 --- /dev/null +++ b/apps/console/src/app/api/agent-address/calendar-window/route.ts @@ -0,0 +1,27 @@ +// SOURCING: none. Route handler for Index calendar window overlap flags. + +import { NextResponse } from 'next/server'; +import { readLifeCalendarWindow } from '@/lib/server/agent-address-harness'; +import { resolveHarnessPrincipal } from '@/lib/server/harness-principal'; + +export async function POST(request: Request) { + const body = (await request.json().catch(() => null)) as { + windowStart?: string; + windowEnd?: string; + events?: unknown; + } | null; + if (!body?.windowStart || !body.windowEnd || !body.events) { + return NextResponse.json({ error: 'window_and_events_required' }, { status: 400 }); + } + const resolution = await resolveHarnessPrincipal(); + if (!resolution.ok) return resolution.response; + const result = await readLifeCalendarWindow({ + windowStart: body.windowStart, + windowEnd: body.windowEnd, + eventsJson: JSON.stringify(body.events), + }); + if (!result.ok) { + return NextResponse.json({ error: result.error }, { status: result.status }); + } + return NextResponse.json({ events: result.events }); +} diff --git a/apps/console/src/components/agent-address/AgentAliasPane.tsx b/apps/console/src/components/agent-address/AgentAliasPane.tsx index ddf46718..7f678da1 100644 --- a/apps/console/src/components/agent-address/AgentAliasPane.tsx +++ b/apps/console/src/components/agent-address/AgentAliasPane.tsx @@ -153,6 +153,11 @@ export function AgentAliasPane() {

Domain {state.domain}

+

+ Interrupt items appear in the Index urgent banner until 10DLC or + toll-free SMS registration is complete. The agent cannot text + interrupts yet. +

{ + const response = await fetch('/api/agent-address/calendar-window', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + cache: 'no-store', + body: JSON.stringify({ + windowStart: '2026-08-18T00:00:00Z', + windowEnd: '2026-08-19T00:00:00Z', + events: DEFAULT_EVENTS, + }), + }); + if (response.status === 404) return null; + if (!response.ok) { + throw new Error(`calendar-window ${response.status}`); + } + const data = (await response.json()) as { events: CalendarWindowRow[] }; + return data.events; +} + +export function LifeCalendarPane() { + const [state, setState] = useState({ status: 'loading' }); + + const refresh = useCallback(async () => { + setState({ status: 'loading' }); + try { + const events = await fetchWindow(); + if (!events) { + setState({ status: 'unconfigured' }); + return; + } + setState({ status: 'ready', events }); + } catch (error) { + setState({ + status: 'error', + message: error instanceof Error ? error.message : 'calendar load failed', + }); + } + }, []); + + useEffect(() => { + void refresh(); + }, [refresh]); + + return ( +
+

Today and tomorrow

+ {state.status === 'loading' ?

Loading events…

: null} + {state.status === 'unconfigured' ? ( +

+ Connect CONSOLE_HARNESS_URL to render owned-inbox calendar events. +

+ ) : null} + {state.status === 'error' ? ( +

+ {state.message} +

+ ) : null} + {state.status === 'ready' ? ( + state.events.length === 0 ? ( +

+ No events in this window. +

+ ) : ( +
    + {state.events.map((row) => ( +
  • + {row.title} + + {row.start} → {row.end} + + {row.overlaps ? ( + + overlaps + + ) : null} +
  • + ))} +
+ ) + ) : null} +
+ ); +} diff --git a/apps/console/src/lib/server/agent-address-harness.ts b/apps/console/src/lib/server/agent-address-harness.ts index a0926fec..36a826a0 100644 --- a/apps/console/src/lib/server/agent-address-harness.ts +++ b/apps/console/src/lib/server/agent-address-harness.ts @@ -67,3 +67,32 @@ export async function revokeAgentAlias(alias: string): Promise< if (!result.ok) return { ok: false, status: result.status, error: result.error }; return { ok: true, alias: result.data.revokeAgentAlias as AliasBlock }; } + +export type CalendarWindowEvent = { + readonly externalId: string; + readonly title: string; + readonly start: string; + readonly end: string; + readonly overlaps: boolean; +}; + +export async function readLifeCalendarWindow(input: { + windowStart: string; + windowEnd: string; + eventsJson: string; +}): Promise< + | { readonly ok: true; readonly events: CalendarWindowEvent[] } + | { readonly ok: false; readonly status: number; readonly error: string } +> { + const result = await callHarnessGraphql( + `query LifeCalendarWindow($windowStart: String!, $windowEnd: String!, $eventsJson: String!) { + lifeCalendarWindow(windowStart: $windowStart, windowEnd: $windowEnd, eventsJson: $eventsJson) { + externalId title start end overlaps + } + }`, + input, + ); + if (!result.ok) return { ok: false, status: result.status, error: result.error }; + const events = (result.data.lifeCalendarWindow as CalendarWindowEvent[] | undefined) ?? []; + return { ok: true, events }; +} diff --git a/apps/console/src/views/IndexRulesView.tsx b/apps/console/src/views/IndexRulesView.tsx index 3fdec85d..de8704c3 100644 --- a/apps/console/src/views/IndexRulesView.tsx +++ b/apps/console/src/views/IndexRulesView.tsx @@ -23,6 +23,7 @@ import { useFilingRules, } from './filing/filing-client'; import { AgentAliasPane } from '@/components/agent-address/AgentAliasPane'; +import { LifeCalendarPane } from '@/components/agent-address/LifeCalendarPane'; import { YourDataEntry } from '@/components/console-plugin/YourDataEntry'; const PREDICATE_KINDS: ReadonlyArray<{ @@ -261,6 +262,7 @@ export function IndexRulesView({ host }: ViewRenderProps) { {content} +
); } diff --git a/apps/console/src/views/UrgentLaneView.tsx b/apps/console/src/views/UrgentLaneView.tsx index 090f3162..931b7767 100644 --- a/apps/console/src/views/UrgentLaneView.tsx +++ b/apps/console/src/views/UrgentLaneView.tsx @@ -61,7 +61,9 @@ export function UrgentLaneView(_props: ViewRenderProps) {

Nothing needs you today. Everything that arrived is filed and will - keep until you go looking for it. + keep until you go looking for it. Interrupt-by-SMS waits on 10DLC + or toll-free registration; until then interrupts are this Index + banner only.