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: 3 additions & 3 deletions apps/mobile/modules/t3-markdown-text/src/markdownLinks.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { fileBasename } from "@t3tools/shared/path";
import { formatFilePathPosition } from "@t3tools/shared/fileLinks";
import {
fileBasename,
formatFilePathPosition,
inlineCodeFilePathCandidate,
normalizeMarkdownLinkDestination,
parseMarkdownFileLink,
} from "@t3tools/client-runtime/markdown-links";
} from "@t3tools/shared/markdownLinks";
import { parseThreadLinkHref } from "@t3tools/shared/threadLinks";
import { videoMimeType } from "@t3tools/shared/video";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MarkdownNode } from "react-native-nitro-markdown/headless";
import { isMarkdownFileLinkLabel } from "@t3tools/client-runtime/markdown-links";
import { isMarkdownFileLinkLabel } from "@t3tools/shared/markdownLinks";
import { collectComposerInlineTokens } from "@t3tools/shared/composerInlineTokens";
import { imageMimeType } from "@t3tools/shared/image";
import { isWindowsAbsolutePath } from "@t3tools/shared/path";
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type {
import {
appendCodexArtifactTemplateUsePrompt,
type CodexArtifactTemplate,
} from "@t3tools/client-runtime/codex-artifact-templates";
} from "@t3tools/shared/codexArtifactTemplates";
import type { ThreadUserInputQuestion } from "@t3tools/client-runtime/state/thread-requests";
import {
presentPendingBackgroundWork,
Expand Down
6 changes: 3 additions & 3 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ import { writeComposerContextClipboard } from "../../lib/composerContextClipboar
import {
codexArtifactTemplatePresentationLabel,
type CodexArtifactTemplate,
} from "@t3tools/client-runtime/codex-artifact-templates";
} from "@t3tools/shared/codexArtifactTemplates";
import { resolveAssetUrl } from "@t3tools/client-runtime/state/assets";
import { isMarkdownFileLinkLabel } from "@t3tools/client-runtime/markdown-links";
import { isMarkdownFileLinkLabel } from "@t3tools/shared/markdownLinks";
import { getTextContent, type MarkdownNode } from "react-native-nitro-markdown/headless";
import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments";
import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
Expand All @@ -50,7 +50,7 @@ import { resolveViewedImageAsset } from "@t3tools/client-runtime/work-log/presen
import {
renderCodexFileCitationsAsMarkdown,
splitCodexArtifactTemplateMarkdown,
} from "@t3tools/client-runtime/codex-markdown-directives";
} from "@t3tools/shared/codexMarkdownDirectives";
import { CHAT_LIST_ANCHOR_OFFSET, resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList";
import { imageMimeType } from "@t3tools/shared/image";
import { videoMimeType } from "@t3tools/shared/video";
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/threads/fileChipMenu.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fileBasename } from "@t3tools/client-runtime/markdown-links";
import { fileBasename } from "@t3tools/shared/path";
import type { ThreadId } from "@t3tools/contracts";
import { resolveMarkdownLinkPresentation } from "@t3tools/mobile-markdown-text/links";
import type { MarkdownFileContextMenu } from "@t3tools/mobile-markdown-text/types";
Expand Down
43 changes: 2 additions & 41 deletions apps/mobile/src/features/threads/thread-search-match.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,9 @@
import { splitThreadSearchText } from "@t3tools/shared/threadSearch";
import type { EnvironmentThreadSearchMatch } from "@t3tools/client-runtime/state/thread-search";

import { AppText as Text } from "../../components/AppText";
import { cn } from "../../lib/cn";

function foldAsciiCase(value: string): string {
return value.replace(/[A-Z]/g, (character) => character.toLowerCase());
}

function splitHighlightParts(text: string, query: string) {
const normalizedText = foldAsciiCase(text);
const normalizedQuery = foldAsciiCase(query.trim());
if (normalizedQuery.length === 0) {
return [{ text, highlighted: false, start: 0 }];
}

const parts: Array<{
readonly text: string;
readonly highlighted: boolean;
readonly start: number;
}> = [];
let cursor = 0;
while (cursor < text.length) {
const matchIndex = normalizedText.indexOf(normalizedQuery, cursor);
if (matchIndex === -1) {
parts.push({ text: text.slice(cursor), highlighted: false, start: cursor });
break;
}
if (matchIndex > cursor) {
parts.push({
text: text.slice(cursor, matchIndex),
highlighted: false,
start: cursor,
});
}
parts.push({
text: text.slice(matchIndex, matchIndex + normalizedQuery.length),
highlighted: true,
start: matchIndex,
});
cursor = matchIndex + normalizedQuery.length;
}
return parts;
}

export function ThreadSearchMatchExcerpt(props: {
readonly match: EnvironmentThreadSearchMatch;
readonly query: string;
Expand All @@ -51,7 +12,7 @@ export function ThreadSearchMatchExcerpt(props: {
readonly sidebar?: boolean;
}) {
const isUser = props.match.source === "user";
const parts = splitHighlightParts(props.match.snippet, props.query);
const parts = splitThreadSearchText(props.match.snippet, props.query);
return (
<Text
className={cn(
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/src/native/createV5StackNavigator.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,14 @@ export function V5CardStackView(props: V5StackViewProps) {
>
<NavigationContext value={descriptor.navigation}>
<NavigationRouteContext value={descriptor.route}>
<V5StackHeader options={descriptor.options} canGoBack={index > 0} />
<V5StackHeader
options={descriptor.options}
canGoBack={
attached
? props.state.routes.findIndex((current) => current.key === route.key) > 0
: index > 0
}
/>
<NativeColumnContent>{descriptor.render()}</NativeColumnContent>
</NavigationRouteContext>
</NavigationContext>
Expand Down
26 changes: 23 additions & 3 deletions apps/mobile/src/native/workspace-stack-projection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,21 +143,41 @@ describe("v5 stack handoff", () => {
});
it("still retains an unfinished JS pop beside a completed native pop", () => {
expect(reconcileStackScreens([home, thread, files], [home], new Set([files.key]))).toEqual([
thread,
home,
thread,
]);
});
it("retains removed native screens through a pop followed immediately by a push", () => {
const popped = reconcileStackScreens([home, thread, files], [home, thread]);
const next = { ...files, key: "new-files" };
const screens = reconcileStackScreens(popped, [home, thread, next]);
expect(screens.map((route) => route.key)).toEqual(["files", "home", "thread", "new-files"]);
expect(screens.map((route) => route.key)).toEqual(["home", "thread", "files", "new-files"]);
expect(screens.filter((route) => [home, thread, next].includes(route))).toEqual([
home,
thread,
next,
]);
expect(screens[0]).toBe(files);
expect(screens[2]).toBe(files);
});
it("keeps the root and outgoing settings page in place until the JS pop finishes", () => {
const environments = { ...files, key: "environments", name: "SettingsEnvironments" };
const popped = reconcileStackScreens([settings, environments], [settings]);
expect(popped).toEqual([settings, environments]);
expect(reconcileStackScreens(popped, [settings])).toEqual(popped);
const next = { ...environments, key: "next-environments" };
expect(reconcileStackScreens(popped, [settings, next])).toEqual([settings, environments, next]);
expect(reconcileStackScreens(popped, [settings], new Set([environments.key]))).toEqual([
settings,
]);
});
it("retains a multi-page pop in order without changing the active router order", () => {
const replacement = { ...thread, key: "replacement" };
const screens = reconcileStackScreens([home, thread, files], [home, replacement]);
expect(screens).toEqual([home, thread, files, replacement]);
expect(screens.filter((route) => route === home || route === replacement)).toEqual([
home,
replacement,
]);
});
it("updates params without retaining duplicate copies of the same screen", () => {
const updated = { ...thread, params: { threadId: "another-thread" } };
Expand Down
15 changes: 8 additions & 7 deletions apps/mobile/src/native/workspace-stack-projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,18 @@ export function partitionStackPresentations<T>(
return groups;
}

/** Preserve UIKit's outgoing screens, before newly pushed screens, until dismissal. */
/** Keep outgoing screens in place until dismissal; Fabric moves enqueue native pop/push operations. */
export function reconcileStackScreens<T extends { readonly key: string }>(
previous: readonly T[],
current: readonly T[],
completedNativeDismissals: ReadonlySet<string> = new Set(),
): T[] {
const active = new Set(current.map((route) => route.key));
return [
...previous.filter(
(route) => !active.has(route.key) && !completedNativeDismissals.has(route.key),
),
...current,
];
const screens = [...current];
previous.forEach((route, index) => {
if (!active.has(route.key) && !completedNativeDismissals.has(route.key)) {
screens.splice(Math.min(index, screens.length), 0, route);
}
});
return screens;
}
2 changes: 2 additions & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export const RPC_REQUIRED_SCOPES = {
[ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.searchThreads]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.searchThread]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.searchThreadStream]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.getTurnItem]: AuthOrchestrationReadScope,
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/observability/RpcInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const RPC_AGGREGATES = {
[ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: "orchestration",
[ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff]: "orchestration",
[ORCHESTRATION_V2_WS_METHODS.searchThreads]: "orchestration",
[ORCHESTRATION_V2_WS_METHODS.searchThread]: "orchestration",
[ORCHESTRATION_V2_WS_METHODS.searchThreadStream]: "orchestration",
[ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot]: "orchestration",
[ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: "orchestrationV2",
[ORCHESTRATION_V2_WS_METHODS.launchThread]: "orchestration",
Expand Down
42 changes: 42 additions & 0 deletions apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import type {
OrchestrationV2SearchThreadInput,
OrchestrationV2SearchThreadResult,
OrchestrationV2ThreadHistoryPage,
} from "@t3tools/contracts";
import {
latestExecutedRun,
latestRootProviderFailure,
Expand Down Expand Up @@ -281,6 +286,18 @@ export interface OrchestratorV2Shape {
readonly dispatch: (
command: OrchestrationV2ServerCommand,
) => Effect.Effect<OrchestratorV2DispatchResult, OrchestratorV2Error>;
readonly searchThreadStream: (
input: OrchestrationV2SearchThreadInput,
) => Stream.Stream<OrchestrationV2SearchThreadResult, OrchestratorV2Error>;
readonly searchThread: (
input: OrchestrationV2SearchThreadInput,
) => Effect.Effect<OrchestrationV2SearchThreadResult, OrchestratorV2Error>;
readonly getThreadHistoryPage: (
threadId: ThreadId,
cursor: string,
throughEntryId?: string,
conversationOnly?: boolean,
) => Effect.Effect<OrchestrationV2ThreadHistoryPage, OrchestratorV2Error>;
readonly getTimelinePage: (
threadId: ThreadId,
options: ProjectionTimelinePageOptions,
Expand Down Expand Up @@ -10817,6 +10834,26 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
recoverDelegatedTask,
delegatedTaskResultPending,
dispatch: dispatchWithReceipt,
searchThreadStream: (input) =>
projectionStore
.searchThreadStream(input)
.pipe(
Stream.mapError(
(cause) => new OrchestratorProjectionError({ threadId: input.threadId, cause }),
),
),
searchThread: (input) =>
projectionStore
.searchThread(input)
.pipe(
Effect.mapError(
(cause) => new OrchestratorProjectionError({ threadId: input.threadId, cause }),
),
),
getThreadHistoryPage: (threadId, cursor, throughEntryId, conversationOnly) =>
projectionStore
.getThreadHistoryPage(threadId, cursor, throughEntryId, conversationOnly)
.pipe(Effect.mapError((cause) => new OrchestratorProjectionError({ threadId, cause }))),
getTimelinePage: (threadId, options) =>
projectionStore
.getTimelinePage(threadId, options)
Expand Down Expand Up @@ -10951,6 +10988,11 @@ const layerUnavailable: Layer.Layer<OrchestratorV2> = Layer.succeed(
cause: "Orchestration V2 live runtime is not configured.",
}),
),
searchThreadStream: (input) =>
Stream.fail(new OrchestratorProjectionError({ threadId: input.threadId })),
searchThread: (input) =>
Effect.fail(new OrchestratorProjectionError({ threadId: input.threadId })),
getThreadHistoryPage: (threadId) => Effect.fail(new OrchestratorProjectionError({ threadId })),
getTimelinePage: (threadId) => Effect.fail(new OrchestratorProjectionError({ threadId })),
getMessageCount: (threadId) => Effect.fail(new OrchestratorProjectionError({ threadId })),
getTurnItem: ({ threadId }) => Effect.fail(new OrchestratorProjectionError({ threadId })),
Expand Down
Loading
Loading