Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ T3 Code is selectively referenced for future ports from https://github.com/pingd

## Oh My Pi

Future adaptations of OMP source use the OMP repository under its repository license. OMP remains runtime authority; adapted files retain OMP attribution and the applicable source license. The vendored `@oh-my-pi/app-wire@0.5.9` package is packed from the public `lyc-aon/oh-my-pi` integration commit `5633bdd7e5f9062d1822eeeddb9311b2d942bf6f`, source tree `4d8794bad6fc57d86058a46dc4698fcca14263e5`; tarball SHA-256 `b3a891610e919833d16302b1893831f509d264322c3869d28f17adbbff6116f0`; golden corpus SHA-256 `50b087a3a22bb48908718b7786eff6ce618bbd6b6123c055e40c957ef47a805c`. Target integration commit is recorded in the Desktop commit history and compatibility matrix.
Future adaptations of OMP source use the OMP repository under its repository license. OMP remains runtime authority; adapted files retain OMP attribution and the applicable source license. The vendored `@oh-my-pi/app-wire@0.6.0` package is packed from the public `lyc-aon/oh-my-pi` integration commit `ae4b53b416f32b200865a32ed9baabd5a4666fa4`, source tree `2b8a5f697273f5044789b8ae638b6c264f9f8499`; tarball SHA-256 `92256497bb8086ab9cefa30e4890293060a52b9d0c5349743ee94ae3224ca32c`; golden corpus SHA-256 `7ebd5fa6cbc37ae0f28cf1d957d9cab841b875581cb42ffbe81cea66f1dc2ef1`. Target integration commit is recorded in the Desktop commit history and compatibility matrix.

## Oh My Pi icon

Expand Down
23 changes: 22 additions & 1 deletion apps/web/src/components/SessionScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
useReducedMotion,
} from "@t4-code/ui";
import { Popover } from "@base-ui/react/popover";
import { Link } from "@tanstack/react-router";
import { Check, PanelBottomClose, PanelBottomOpen, PanelRight, X } from "lucide-react";
import { useEffect, useState } from "react";

Expand All @@ -25,6 +26,8 @@ import { TerminalDrawer } from "../features/terminal/TerminalDrawer.tsx";
import { FreshnessBadge, SessionMain, SessionOwnershipBadge } from "../features/transcript/SessionMain.tsx";
import { RIGHT_PANE_DOCK_QUERY, useMediaQuery } from "../hooks/useMediaQuery.ts";
import { useWorkspace, workspaceStore } from "../state/store-instance.ts";
import { useDesktopRuntimeSnapshot } from "../platform/desktop-runtime.ts";
import { resolveLiveSession } from "../platform/live-workspace.ts";
import {
type PaneFamily,
RIGHT_PANE_WIDTH,
Expand Down Expand Up @@ -157,6 +160,15 @@ export function SessionScreen({
(state) => selectSessionView(state, session.id).terminalDrawerOpen,
);
const paneDocks = useMediaQuery(RIGHT_PANE_DOCK_QUERY);
const runtimeSnapshot = useDesktopRuntimeSnapshot();
const previewAddress =
runtimeSnapshot === null ? null : resolveLiveSession(runtimeSnapshot, session.id);
const previewCount =
previewAddress === null
? 0
: (runtimeSnapshot?.projection.sessions
.get(`${previewAddress.hostId}\u0000${previewAddress.sessionId}`)
?.previews.size ?? 0);
const [panePreviewWidth, setPanePreviewWidth] = useState<number | null>(null);

// Transcript scroll ownership lives in TranscriptTimeline (virtualized
Expand Down Expand Up @@ -206,7 +218,16 @@ export function SessionScreen({
<FreshnessBadge session={session} />
<SessionOwnershipBadge session={session} />
</span>
<span className="min-w-0 flex-1" />
{previewCount > 0 && (
<Link
aria-label={`Open browser preview${previewCount === 1 ? "" : ` (${previewCount})`}`}
className="shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-ring"
params={{ sessionId: session.id }}
to="/sessions/$sessionId/preview"
>
<Badge variant="outline">Preview{previewCount === 1 ? "" : ` · ${previewCount}`}</Badge>
</Link>
)}
{!archived && (
<FamilyToggles paneFamily={viewPaneFamily} paneOpen={viewPaneOpen} sessionId={session.id} />
)}
Expand Down
66 changes: 66 additions & 0 deletions apps/web/src/features/panes/activity-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// filter/search them, redact secrets from the raw inspector, and export.
// Pure functions — the store applies them, tests drive them directly.
import { isSessionEvent, type SessionEvent } from "@t4-code/protocol";
import type { PreviewEventProjection, PreviewFreshness } from "@t4-code/client";

import { sessionEventSpec } from "../session-runtime/session-event-vocabulary.ts";
import type { ActivityEntry, ActivityFilter, ActivityKind } from "./model.ts";
Expand Down Expand Up @@ -83,6 +84,71 @@ export function classifySessionEvent(
};
}

function previewEventAt(event: PreviewEventProjection, fallbackAt: string): string {
if (
event.timestamp === undefined ||
!Number.isFinite(event.timestamp) ||
event.timestamp < 0 ||
event.timestamp > 8.64e15
) {
return fallbackAt;
}
return new Date(event.timestamp).toISOString();
}


/**
* Browser-preview activity is projection metadata, not a browser audit log.
* Keep only the already-sanitized origin/path, capture identity, timestamp,
* and a generic error outcome. Query/hash values, pixels, credentials, and
* backend error text never enter Activity or its export payload.
*/
export function classifyPreviewEvent(
event: PreviewEventProjection,
seq: number,
fallbackAt: string,
freshness?: PreviewFreshness,
): ActivityEntry {
const url = event.url === undefined ? null : `${event.url.origin}${event.url.pathname}`;
const context = freshness === "cached" || freshness === "stale" ? freshness : null;
const detailParts: string[] = [];
if (url !== null) detailParts.push(url);
if (event.type === "capture" && event.captureId !== undefined) {
detailParts.push(`Capture ${event.captureId}`);
}
if (context !== null) detailParts.push(context);
const title =
event.type === "launch"
? "Browser preview launched"
: event.type === "navigation"
? "Browser preview navigated"
: event.type === "capture"
? "Browser preview captured"
: "Browser preview failed";
return {
seq,
at: previewEventAt(event, fallbackAt),
kind: event.type === "error" ? "error" : "system",
title,
detail: event.type === "error" ? "The browser preview request did not complete." : (detailParts.join(" · ") || null),
agentId: null,
terminalId: null,
raw: {
type: `preview.${event.type}`,
previewId: event.previewId,
cursor: event.cursor,
...(url === null ? {} : { url }),
...(event.type === "capture" && event.captureId !== undefined
? { captureId: event.captureId }
: {}),
...(event.timestamp === undefined ? {} : { timestamp: event.timestamp }),
...(context === null ? {} : { freshness: context }),
},
unknown: false,
shellOutput: null,
};
}

/** Append with the retention cap; order is seq order, oldest dropped first. */
export function appendActivity(
entries: readonly ActivityEntry[],
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/features/panes/live-inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,8 @@ function emptyProjection(): SessionProjection {
audit: [],
confirmations: new Map(),
results: new Map(),
previews: new Map(),
previewEvents: [],
freshness: "cached",
transcriptEventArrivalOrdinal: 0,
contextMaintenanceEventArrivalOrdinal: 0,
Expand Down
22 changes: 20 additions & 2 deletions apps/web/src/features/panes/live-projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
// desktop runtime already validated and bounded) to the inspector pane
// view models. Every function here is derivation only: unknown fields stay
// null, unsafe paths disappear, and nothing is invented to fill a gap.
import type { AgentTranscriptProjection, ResultProjection, SessionProjection } from "@t4-code/client";
import type {
AgentTranscriptProjection,
PreviewFreshness,
ResultProjection,
SessionProjection,
} from "@t4-code/client";

import { classifySessionEvent } from "./activity-log.ts";
import { classifyPreviewEvent, classifySessionEvent } from "./activity-log.ts";
import { displayStateFromWire } from "./model.ts";
import type {
ActivityEntry,
Expand Down Expand Up @@ -205,6 +210,19 @@ export function collectActivity(session: SessionProjection): KeyedActivityEntry[
entry: classifySessionEvent(frame.event, 0, ""),
});
}
for (const event of session.previewEvents) {
let freshness: PreviewFreshness | undefined;
for (const preview of session.previews.values()) {
if (preview.previewId === event.previewId) {
freshness = preview.freshness;
break;
}
}
entries.push({
key: `preview:${event.cursor.epoch}:${event.cursor.seq}`,
entry: classifyPreviewEvent(event, 0, "", freshness),
});
}
for (const frame of session.audit) {
entries.push({
key: `audit:${frame.timestamp}\u0000${frame.action}\u0000${frame.actor}`,
Expand Down
Loading