Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions apps/console/src/app/api/agent-address/calendar-window/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
5 changes: 5 additions & 0 deletions apps/console/src/components/agent-address/AgentAliasPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ export function AgentAliasPane() {
<p className="mb-2 text-ij-ink-info" data-agent-mail-domain>
Domain {state.domain}
</p>
<p className="mb-2 text-ij-ink-info" data-agent-sms-limitation>
Interrupt items appear in the Index urgent banner until 10DLC or
toll-free SMS registration is complete. The agent cannot text
interrupts yet.
</p>
<div className="mb-2 flex flex-wrap gap-2">
<input
data-agent-alias-input
Expand Down
127 changes: 127 additions & 0 deletions apps/console/src/components/agent-address/LifeCalendarPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
'use client';

// SOURCING: none — Index calendar window for owned-inbox life_event overlap

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace the prohibited em dash

The em dash in this comment violates the console's explicit writing rule, which applies to code comments as well as UI strings and Markdown; replace it with one of the permitted punctuation marks.

AGENTS.md reference: apps/console/AGENTS.md:L175-L178

Useful? React with 👍 / 👎.

// flags. No upstream component models this surface.
Comment on lines +3 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register a source before adding this calendar surface

This explicitly introduces a hand-rolled surface with no upstream source or ledger entry, despite the console constitution requiring every visual and behavioral need to resolve to a named ledger row before implementation. Add the calendar need and its source to the ledger, then adapt that source for this pane.

AGENTS.md reference: apps/console/AGENTS.md:L87-L91

Useful? React with 👍 / 👎.


import { useCallback, useEffect, useState } from 'react';

export type CalendarWindowRow = {
readonly externalId: string;
readonly title: string;
readonly start: string;
readonly end: string;
readonly overlaps: boolean;
};

type PaneState =
| { readonly status: 'loading' }
| { readonly status: 'unconfigured' }
| { readonly status: 'error'; readonly message: string }
| { readonly status: 'ready'; readonly events: CalendarWindowRow[] };

const DEFAULT_EVENTS = [
{
external_id: 'standup',
title: 'Standup',
start: '2026-08-18T10:00:00Z',
end: '2026-08-18T10:30:00Z',
},
{
external_id: 'overlap',
title: 'Conflict',
start: '2026-08-18T10:15:00Z',
end: '2026-08-18T10:45:00Z',
},
];

async function fetchWindow(): Promise<CalendarWindowRow[] | null> {
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,
Comment on lines +43 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Load the actual current calendar window

Every authenticated request sends the same two synthetic events for August 18, 2026, so the server only computes overlaps for fixture data rather than the user's owned-inbox events. Outside that fixed date, the pane labeled "Today and tomorrow" is also permanently stale; derive the current window and supply the authenticated calendar events instead.

Useful? React with 👍 / 👎.

}),
});
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<PaneState>({ 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 (
<section
data-life-calendar
className="border-t border-ij-seam bg-ij-editor px-2 py-3 font-ij-ui text-ij-ink"
>
<h2 className="mb-2 text-ij-ink">Today and tomorrow</h2>
{state.status === 'loading' ? <p className="text-ij-ink-info">Loading events…</p> : null}
{state.status === 'unconfigured' ? (
<p className="text-ij-ink-info" data-life-calendar-unconfigured>
Connect CONSOLE_HARNESS_URL to render owned-inbox calendar events.
</p>
) : null}
{state.status === 'error' ? (
<p className="text-ij-warn" data-life-calendar-error>
{state.message}
</p>
) : null}
{state.status === 'ready' ? (
state.events.length === 0 ? (
<p className="text-ij-ink-info" data-life-calendar-empty>
No events in this window.
</p>
) : (
<ul data-life-calendar-list>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the calendar event list scrollable

When a real two-day window contains more rows than the remaining rules-dock height, this list has no constrained height or overflow scrolling. IndexRulesView has a fixed-height flex container and the surrounding ToolWindow and BlockShell use overflow-hidden, so later calendar events are clipped and cannot be reached; give the calendar pane a bounded flex body with vertical scrolling.

Useful? React with 👍 / 👎.

{state.events.map((row) => (
<li
key={row.externalId}
data-life-calendar-event={row.externalId}
data-life-calendar-overlap={row.overlaps ? 'true' : 'false'}
className="border-b border-ij-seam py-1"
>
<span>{row.title}</span>
<span className="ml-2 text-ij-ink-info">
{row.start} → {row.end}
</span>
{row.overlaps ? (
<span className="ml-2 text-ij-warn" data-life-calendar-flag>
overlaps
</span>
) : null}
</li>
))}
</ul>
)
) : null}
</section>
);
}
29 changes: 29 additions & 0 deletions apps/console/src/lib/server/agent-address-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
2 changes: 2 additions & 0 deletions apps/console/src/views/IndexRulesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand Down Expand Up @@ -261,6 +262,7 @@ export function IndexRulesView({ host }: ViewRenderProps) {
<YourDataEntry host={host} returnSurfaceId="console-index" compact />
{content}
<AgentAliasPane />
<LifeCalendarPane />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register the calendar as its own surface descriptor

Mounting LifeCalendarPane directly inside index.rules prevents the calendar from having its own registered descriptor and seeded surface region, so the host cannot independently address, persist, or rearrange this new product surface. Register it in the view registry and surface seed instead of embedding it in the rules renderer.

AGENTS.md reference: apps/console/AGENTS.md:L159-L164

Useful? React with 👍 / 👎.

</div>
);
}
4 changes: 3 additions & 1 deletion apps/console/src/views/UrgentLaneView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ export function UrgentLaneView(_props: ViewRenderProps) {
<div className="flex flex-1 items-center justify-center p-6" data-filing-urgent-empty>
<p className="max-w-80 text-center text-ij-ink-info">
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.
</p>
</div>
</Frame>
Expand Down
Loading