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
8 changes: 0 additions & 8 deletions apps/web/src/components/chat/MessagesTimeline.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,6 @@ export function resolveTimelineMinimapHasPersistentGutter(

const TIMELINE_MINIMAP_HIT_STRIP_LEFT = 12;
const TIMELINE_MINIMAP_HIT_STRIP_MAX_WIDTH = 40;
const TIMELINE_MINIMAP_EXPANDED_HIT_STRIP_WIDTH = "22rem";

export function resolveTimelineMinimapHitStripWidth(
viewportWidth: number,
Expand All @@ -466,13 +465,6 @@ export function resolveTimelineMinimapNavigationInteractive(collapsedWidth: numb
return collapsedWidth >= TIMELINE_MINIMAP_NAVIGATION_REACH;
}

export function resolveTimelineMinimapInteractiveWidth(
collapsedWidth: number,
expanded: boolean,
): number | string {
return expanded ? TIMELINE_MINIMAP_EXPANDED_HIT_STRIP_WIDTH : collapsedWidth;
}

function computeElapsedMs(startIso: string, endIso: string): number | null {
const start = Date.parse(startIso);
const end = Date.parse(endIso);
Expand Down
23 changes: 5 additions & 18 deletions apps/web/src/components/chat/TimelineMinimap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,13 @@ import { Button } from "../ui/button";
import {
resolveTimelineMinimapHeightStyle,
resolveTimelineMinimapIndexFromPointer,
resolveTimelineMinimapInteractiveWidth,
resolveTimelineMinimapNavigationInteractive,
resolveTimelineMinimapTopPercent,
TIMELINE_MINIMAP_MIN_ITEMS,
} from "./MessagesTimeline.logic";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import { cn } from "~/lib/utils";

function timelineMinimapEventTargetsPreview(target: EventTarget): boolean {
return target instanceof Element && target.closest("[data-minimap-preview]") !== null;
}

// A release outside the window can skip `mouseup`, so the next button-free
// `mousemove` also counts.
function onMouseButtonsReleased(onRelease: () => void): () => void {
Expand Down Expand Up @@ -147,7 +142,7 @@ export function TimelineMinimap({
}}
style={{
height: resolveTimelineMinimapHeightStyle(items.length),
width: resolveTimelineMinimapInteractiveWidth(hitStripWidth, activeItem !== null),
width: hitStripWidth,
}}
>
<TimelineMinimapNavigationButton
Expand All @@ -163,9 +158,6 @@ export function TimelineMinimap({
className="absolute inset-y-0 left-0 w-full cursor-pointer bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70"
onBlur={() => setActiveIndex(null)}
onClick={(event) => {
if (timelineMinimapEventTargetsPreview(event.target)) {
return;
}
const nextIndex = resolveActiveIndexFromPointer(event);
const selectedItem = nextIndex === null ? null : (items[nextIndex] ?? null);
if (selectedItem) {
Expand Down Expand Up @@ -199,12 +191,7 @@ export function TimelineMinimap({
if (event.buttons !== 0 && !pressedOnStripRef.current) return;
updateActiveIndexFromPointer(event);
}}
onMouseDown={(event) => {
if (timelineMinimapEventTargetsPreview(event.target)) {
return;
}
event.preventDefault();
}}
onMouseDown={(event) => event.preventDefault()}
type="button"
>
<div className="absolute top-0 left-3 h-full w-px bg-border/15" />
Expand Down Expand Up @@ -247,10 +234,10 @@ export function TimelineMinimap({
);
})}
{activeItem ? (
// The preview is display-only: it overlaps the message column, so
// taking pointer events would block selecting the text beneath it.
<span
className="pointer-events-auto absolute left-8 w-80 cursor-text select-text"
data-minimap-preview
onMouseMove={(event) => event.stopPropagation()}
className="pointer-events-none absolute left-8 w-80"
style={{
top: `${activeTopPercent}%`,
transform: `translateY(${activeTooltipTranslate})`,
Expand Down
6 changes: 6 additions & 0 deletions scripts/fixtures/issue-16447.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"projectTitle": "Minimap synthetic evidence",
"user": "Inspect paragraph {n}: verify that text selection, the reference link, and wheel scrolling remain available beneath the timeline preview. This deliberately long user heading exercises compact preview truncation without including private conversation data.",
"assistant": "### Synthetic paragraph {n}\n\nSelectable text begins at the left content edge. Amber birds cross the quiet valley while readers compare navigation behavior. Drag across this sentence to select the underlying message, then move back into the narrow timeline rail. The preview should remain a display of context while the conversation stays usable.\n\n[Reference link](https://example.com/#synthetic-{n})\n\n**Result:** this is fixed synthetic markdown with a short list:\n\n- Navigation stays intentional.\n- Selection belongs to the conversation.\n- Scroll position can be reproduced.\n\n",
"longTail": "Additional synthetic context exercises the preview length cap. Cedar trees line a river beneath a clear sky. "
}
78 changes: 78 additions & 0 deletions scripts/fixtures/issue-16447.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Seed UI-only projections in this worktree's .t3, with its dev server STOPPED.

Run from the repository root: python3 scripts/fixtures/issue-16447.py
Requires an already migrated .t3/userdata/statev2.sqlite. Removes copied project
and conversation projections so no private sidebar/history appears in evidence.
Does not change events or claim backend behavior. Never targets live T3 home.

Deletes every projection table, so it refuses to touch the database until you
mark it disposable yourself, after copying in data you can lose:
echo issue-16447-disposable > .t3/userdata/statev2.sqlite.issue-16447-disposable
"""
import datetime
import json
from pathlib import Path
import sqlite3

root = Path.cwd().resolve()
db = root / '.t3/userdata/statev2.sqlite'
if not (root / '.git').exists() or not db.exists() or db.resolve() == Path.home() / '.t3/userdata/statev2.sqlite':
raise SystemExit('Run in the isolated, migrated worktree only')
marker = db.with_name(db.name + '.issue-16447-disposable')
if not marker.is_file() or marker.read_text().strip() != 'issue-16447-disposable':
raise SystemExit(f'Refusing to delete projections: create {marker} as described in this script\'s docstring')
fixture = json.loads(Path(__file__).with_suffix('.json').read_text())
c = sqlite3.connect(db)
c.execute('PRAGMA foreign_keys=OFF')

def insert(table, row):
keys = list(row)
c.execute(f'INSERT INTO {table} ({",".join(keys)}) VALUES ({",".join("?" for _ in keys)})', list(row.values()))

def stamp(n=0):
return (datetime.datetime(2026, 3, 17, 12, tzinfo=datetime.timezone.utc) + datetime.timedelta(seconds=n)).isoformat(timespec='milliseconds').replace('+00:00', 'Z')

def payload(table, obj, **columns):
insert('orchestration_v2_projection_' + table, dict(columns, payload_json=json.dumps(obj)))

with c:
tables = [r[0] for r in c.execute("SELECT name FROM sqlite_schema WHERE type='table'")]
for table in tables:
if (table.startswith('orchestration_v2_projection_') and table != 'orchestration_v2_projection_metadata') or table.startswith('projection_'):
c.execute(f'DELETE FROM {table}')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
insert('projection_projects', dict(project_id='synthetic-minimap', title=fixture['projectTitle'], workspace_root=str(root), scripts_json='[]', created_at=stamp(), updated_at=stamp()))
model = dict(instanceId='codex', model='gpt-5.4')
for label, count in [('long', 80), ('two', 2), ('one', 1), ('empty', 0)]:
tid = 'synthetic-minimap-' + label
title = f'Minimap {label} — synthetic fixture'
thread = dict(createdBy='user', creationSource='web', id=tid, projectId='synthetic-minimap', title=title, providerInstanceId='codex', modelSelection=model, runtimeMode='full-access', interactionMode='default', branch=None, worktreePath=None, linkedPullRequest=None, pullRequests=[], activeProviderThreadId=None, lineage=dict(parentThreadId=None, relationshipToParent=None, rootThreadId=tid), forkedFrom=None, createdAt=stamp(), updatedAt=stamp(), archivedAt=None, settledOverride='active', settledAt=None, lastVisitedAt=None, deletedAt=None)
payload('threads', thread, thread_id=tid, project_id='synthetic-minimap', title=title, default_provider='codex', provider_instance_id='codex', runtime_mode='full-access', interaction_mode='default', created_at=stamp(), updated_at=stamp())
for i in range(1, count+1):
rid = f'{tid}-run-{i:03}'
uid = f'{rid}-user'
ts = stamp(i*10)
run = dict(id=rid, threadId=tid, ordinal=i, providerInstanceId='codex', modelSelection=model, providerThreadId=None, userMessageId=uid, rootNodeId=None, activeAttemptId=None, status='completed', queuePosition=None, requestedAt=ts, startedAt=ts, completedAt=stamp(i*10+2), checkpointId=None, contextHandoffId=None, workspacePreparation=dict(type='root'))
payload('runs', run, run_id=rid, thread_id=tid, ordinal=i, provider='codex', provider_instance_id='codex', status='completed', requested_at=ts, completed_at=stamp(i*10+2))
for j, role in enumerate(['user', 'assistant']):
if label=='two' and i==1 and role=='assistant':
continue
text = fixture[role].replace('{n}', str(i))
if label=='two':
text = (' ' if i==1 else 'Short user message') if role=='user' else 'Short final assistant.'
elif role=='assistant':
text += fixture['longTail'] * 18
mid = uid if role=='user' else f'{rid}-assistant'
mt = stamp(i*10+j)
message = dict(createdBy='user' if role=='user' else 'agent', creationSource='web' if role=='user' else 'provider', id=mid, threadId=tid, runId=rid, nodeId=None, role=role, text=text, attachments=[], streaming=False, createdAt=mt, updatedAt=mt)
payload('messages', message, message_id=mid, thread_id=tid, run_id=rid, role=role, streaming=0, created_at=mt, updated_at=mt)
item = dict(id=mid+'-item', threadId=tid, runId=rid, nodeId=None, providerThreadId=None, providerTurnId=None, nativeItemRef=None, parentItemId=None, ordinal=i*1000000+j+1, status='completed', title=None, startedAt=mt, completedAt=mt, updatedAt=mt, type=role+'_message', messageId=mid, text=text, attachments=[])
if role=='user':
item.update(createdBy='user', creationSource='web', inputIntent='turn_start')
else:
item['streaming']=False
payload('turn_items', item, turn_item_id=item['id'], thread_id=tid, run_id=rid, ordinal=item['ordinal'], type=item['type'], status='completed', updated_at=mt)
if label=='long' and i%5==0:
item = dict(id=rid+'-reasoning', threadId=tid, runId=rid, nodeId=None, providerThreadId=None, providerTurnId=None, nativeItemRef=None, parentItemId=None, ordinal=i*1000000+3, status='completed', title=None, startedAt=ts, completedAt=stamp(i*10+2), updatedAt=stamp(i*10+2), type='reasoning', text='Checked synthetic layout and navigation constraints.', streaming=False)
payload('turn_items', item, turn_item_id=item['id'], thread_id=tid, run_id=rid, ordinal=item['ordinal'], type=item['type'], status='completed', updated_at=item['updatedAt'])
print('Seeded synthetic-only projections: long=80, two=2, one=1, empty=0. No provider calls.')
Loading