Skip to content
Merged
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
6 changes: 6 additions & 0 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { GitConfirmSheet } from "./features/threads/git/GitConfirmSheet";
import { GitOverviewSheet } from "./features/threads/git/GitOverviewSheet";
import { ThreadPullRequestsSheet } from "./features/threads/ThreadPullRequestsSheet";
import { ConversationSummariesRouteScreen } from "./features/conversation-summaries/ConversationSummariesRouteScreen";
import { ExternalHistoryRouteScreen } from "./features/external-history/ExternalHistoryRouteScreen";
import { ThreadAgentsSheet } from "./features/threads/ThreadAgentsSheet";
import { ThreadBackgroundWorkSheet } from "./features/threads/ThreadBackgroundWorkSheet";
import { ThreadQueueSheet } from "./features/threads/ThreadQueueControl";
Expand Down Expand Up @@ -304,6 +305,11 @@ const SETTINGS_SCREENS = {
linking: "settings/thread-preferences",
options: { ...SETTINGS_PAGE_OPTIONS, title: "Threads" },
}),
SettingsExternalHistory: createNativeStackScreen({
screen: ExternalHistoryRouteScreen,
linking: "settings/environments/:environmentId/external-history",
options: { ...SETTINGS_PAGE_OPTIONS, title: "External history" },
}),
SettingsAbout: createNativeStackScreen({
screen: SettingsAboutRouteScreen,
linking: "settings/about",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@ import {
ProviderInstanceId,
RunId,
ThreadId,
TurnItemId,
conversationSummarySourceKey,
type ConversationSummary,
type ConversationSummaryExpandInput,
type ConversationSummaryList,
type ConversationSummaryListInput,
type ConversationSummarySourceRef,
} from "@t3tools/contracts";

const state = vi.hoisted(() => ({
supported: null as unknown,
searchSupported: null as unknown,
focused: true,
list: null as ConversationSummaryList | null,
lists: [] as { environmentId: EnvironmentId; input: ConversationSummaryListInput }[],
expansions: [] as { environmentId: EnvironmentId; input: ConversationSummaryExpandInput }[],
longSource: "",
}));
vi.mock("react-native", () => ({
View: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
Expand All @@ -36,6 +41,21 @@ vi.mock("../../components/ui/AppBar", () => ({
}));
vi.mock("../../components/ui/AppText", () => ({
AppText: ({ children }: { children?: ReactNode }) => <span>{children}</span>,
AppTextInput: ({
accessibilityLabel,
value,
onChangeText,
}: {
accessibilityLabel: string;
value: string;
onChangeText: (value: string) => void;
}) => (
<input
aria-label={accessibilityLabel}
value={value}
onChange={(event) => onChangeText(event.target.value)}
/>
),
}));
vi.mock("../../components/ui/ScreenContent", () => ({
ScreenContent: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
Expand All @@ -57,6 +77,7 @@ vi.mock("../../components/ui/Button", () => ({
}));
vi.mock("../../state/conversationSummaries", () => ({
conversationSummariesSupportedAtom: () => state.supported,
conversationSummarySearchSupportedAtom: () => state.searchSupported,
conversationSummaries: {
list: (request: { environmentId: EnvironmentId; input: ConversationSummaryListInput }) => {
state.lists.push(request);
Expand All @@ -65,40 +86,60 @@ vi.mock("../../state/conversationSummaries", () => ({
expand: (request: { environmentId: EnvironmentId; input: ConversationSummaryExpandInput }) => {
state.expansions.push(request);
const offset = request.input.offset ?? 0;
const textOffset = request.input.textOffset ?? 0;
const maxChars = request.input.maxChars ?? 12_000;
const summary = state.list?.summaries[0];
if (summary?.state !== "available") throw new Error("Expected summary");
const firstPart = offset === 0 && textOffset === 0;
return {
summary,
sources: summary.summary.sources.slice(offset, offset + 6).map((ref, index) => ({
ref,
result:
offset + index === 0
? { state: "unavailable", reason: "changed" }
: {
state: "available",
role: "assistant",
text: "Exact source " + (offset + index + 1) + ": 日本語 🧪\n unchanged.",
},
})),
nextOffset: offset === 0 ? 6 : null,
sources: summary.summary.sources
.slice(offset, offset + (firstPart ? 1 : 6))
.map((ref, index) => ({
ref,
result:
offset + index === 0
? {
state: "available",
role: "assistant",
text: state.longSource.slice(textOffset, textOffset + maxChars),
textOffset,
totalChars: state.longSource.length,
}
: offset + index === 1
? { state: "unavailable", reason: "changed" }
: {
state: "available",
role: ref.kind === "turn_item" ? "tool" : "assistant",
text: "Exact source " + (offset + index + 1) + ": 日本語 🧪\n unchanged.",
},
})),
nextOffset: firstPart ? 0 : offset === 0 ? 6 : null,
nextTextOffset: firstPart ? maxChars : 0,
};
},
},
}));
vi.mock("../../state/query", () => ({
useEnvironmentQuery: (data: unknown) => ({ data, error: null, isPending: false }),
}));
vi.mock("../external-history/ExternalHistoryRouteScreen", () => ({
ExternalHistoryThreadButton: () => null,
}));

import { ConversationSummariesRouteScreen } from "./ConversationSummariesRouteScreen";

function setup() {
const supported = Atom.make(true);
const registry = AtomRegistry.make();
state.supported = supported;
state.searchSupported = Atom.make(true);
state.focused = false;
state.lists = [];
state.expansions = [];
state.list = null;
state.longSource =
"Exact source 1: 日本語 🧪\n unchanged.\n" + "x".repeat(12_000) + "\nFinal evidence.";
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
const container = document.createElement("div");
const root = createRoot(container);
Expand Down Expand Up @@ -141,20 +182,39 @@ it("reads only while open and supported, shows pass/citations/stale reasons and
screen.setSupported(false);
expect(state.lists).toEqual([]);
expect(screen.container.textContent).toContain("Update the server");
const sources = Array.from({ length: 10 }, (_, index): ConversationSummarySourceRef =>
index === 9
? {
kind: "turn_item",
threadId: ThreadId.make("thread"),
itemId: TurnItemId.make("tool-output"),
digest: "a".repeat(64),
}
: {
kind: "message",
threadId: ThreadId.make("thread"),
messageId: MessageId.make("message-" + (index < 2 ? 0 : index)),
digest: "a".repeat(64),
...(index < 2
? {
range: {
start: index * state.longSource.length,
end: (index + 1) * state.longSource.length,
total: state.longSource.length * 2,
},
}
: {}),
},
);
const summary: ConversationSummary = {
id: "summary:" + "a".repeat(64),
projectId: ProjectId.make("project"),
threadId: ThreadId.make("thread"),
sources: Array.from({ length: 10 }, (_, index) => ({
kind: "message",
threadId: ThreadId.make("thread"),
messageId: MessageId.make("message-" + index),
digest: "a".repeat(64),
})),
sources,
statements: [
{
text: "Keep the canonical sources.",
citations: [MessageId.make("message-0"), MessageId.make("message-9")],
citations: [sources[0]!, sources[1]!, sources[9]!].map(conversationSummarySourceKey),
},
],
modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5" },
Expand Down Expand Up @@ -190,37 +250,55 @@ it("reads only while open and supported, shows pass/citations/stale reasons and
});
expect(state.expansions).toEqual([]);
expect(screen.container.textContent).toContain(
"Last pass failed. No model 10 older messages wait",
"Last pass failed. No model 10 older sources wait",
);
expect(screen.container.textContent).toContain("Keep the canonical sources. [#1, #10]");
expect(screen.container.textContent).toContain("Keep the canonical sources. [#1, #2, #10]");
expect(screen.container.textContent).toContain(
"Outdated: source changed. The summary text is hidden",
);
await screen.click("Later summaries");
expect(state.lists.at(-1)?.input.cursor).toBe(staleId);
await screen.click("Earlier summaries");
expect(state.lists.at(-1)?.input.cursor).toBeUndefined();
await screen.click("Show source messages");
await screen.click("Show sources");
expect(state.expansions.at(-1)).toEqual({
environmentId: "remote",
input: { projectId: "project", threadId: "thread", summaryId: summary.id, offset: 0, limit: 6 },
input: {
projectId: "project",
threadId: "thread",
summaryId: summary.id,
offset: 0,
textOffset: 0,
limit: 6,
maxChars: 12_000,
},
});
expect(screen.container.textContent).toContain("Exact source 1: 日本語 🧪\n unchanged.");
expect(screen.container.textContent).not.toContain("Final evidence.");
await screen.click("Next sources");
expect(state.expansions.at(-1)?.input).toMatchObject({ offset: 0, textOffset: 12_000 });
expect(screen.container.textContent).toContain("Final evidence.");
expect(screen.container.textContent).not.toContain("Exact source 1:");
expect(screen.container.textContent).toContain("Source changed.");
expect(screen.container.textContent).toContain("Exact source 2: 日本語 🧪\n unchanged.");
await screen.click("Next messages");
await screen.click("Previous sources");
expect(state.expansions.at(-1)?.input).toMatchObject({ offset: 0, textOffset: 0 });
expect(screen.container.textContent).toContain("Exact source 1:");
await screen.click("Next sources");
await screen.click("Next sources");
expect(state.expansions.at(-1)?.input.offset).toBe(6);
expect(screen.container.textContent).toContain("#7 · Assistant");
expect(screen.container.textContent).not.toContain("Exact source 2:");
expect(screen.container.textContent).toContain("#10 · Tool");
expect(screen.container.textContent).not.toContain("Exact source 1:");
expect(
[...screen.container.querySelectorAll("button")].find(
(button) => button.textContent === "Next messages",
(button) => button.textContent === "Next sources",
)?.disabled,
).toBe(true);
await screen.click("Previous messages");
expect(state.expansions.at(-1)?.input.offset).toBe(0);
await screen.click("Previous sources");
expect(state.expansions.at(-1)?.input).toMatchObject({ offset: 0, textOffset: 12_000 });
await screen.click("Back to summaries");
expect(screen.container.textContent).not.toContain("Exact source");
await screen.click("Show source messages");
await screen.click("Show sources");
const listsBeforeCapabilityLoss = state.lists.length;
const expansionsBeforeCapabilityLoss = state.expansions.length;
screen.setSupported(false);
Expand Down
Loading
Loading