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
23 changes: 23 additions & 0 deletions apps/mobile/src/features/threads/PersistentThreadIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Svg, { Path, Rect } from "react-native-svg";

export function PersistentThreadIcon(props: { readonly color: string; readonly size?: number }) {
const size = props.size ?? 14;
return (
<Svg
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={props.color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<Path d="M22 17a2 2 0 0 1-2 2H6.83a2 2 0 0 0-1.42.59l-2.2 2.2A.71.71 0 0 1 2 21.29V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" />
<Rect width={8} height={5} x={8} y={10} rx={1} />
<Path d="M10 10V8a2 2 0 0 1 4 0v2" />
</Svg>
);
}
44 changes: 32 additions & 12 deletions apps/mobile/src/features/threads/thread-list-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { buildThreadTitleRegenerationMenuItems } from "./thread-title-regenerati
import { resolveThreadStatus, shouldShowActionWaitingIndicator } from "./threadPresentation";
import { actionRunningPresentation } from "@t3tools/shared/actionResume";
import { ThreadSearchMatchExcerpt } from "./thread-search-match";
import { PersistentThreadIcon } from "./PersistentThreadIcon";

/**
* Shared presentation for the thread lists: the compact (phone) Home list and
Expand Down Expand Up @@ -709,9 +710,20 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
}}
>
<View className="flex-row items-center justify-between gap-2">
<Text className="flex-1 text-lg font-t3-bold text-foreground" numberOfLines={1}>
{thread.title}
</Text>
<View className="flex-1 flex-row items-center gap-1.5">
{thread.persistent === true ? (
<PersistentThreadIcon color={String(theme["--color-foreground"])} size={15} />
) : null}
Comment thread
lastobelus marked this conversation as resolved.
<Text
className={cn(
"flex-1 text-lg font-t3-bold text-foreground",
thread.persistent === true && "italic",
)}
numberOfLines={1}
>
{thread.title}
</Text>
</View>
<View className="flex-row items-center gap-2">
{actionStatusIndicator}
{statusPill}
Expand Down Expand Up @@ -769,15 +781,23 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
>
<View className="gap-[3px]">
<View className="flex-row items-center justify-between gap-2">
<Text
className={cn(
"flex-1 text-base font-t3-medium",
selected ? "text-user-bubble-foreground" : "text-foreground",
)}
numberOfLines={1}
>
{thread.title}
</Text>
<View className="flex-1 flex-row items-center gap-1.5">
{thread.persistent === true ? (
<PersistentThreadIcon
color={String(selected ? selectedForegroundColor : theme["--color-foreground"])}
/>
) : null}
<Text
className={cn(
"flex-1 text-base font-t3-medium",
selected ? "text-user-bubble-foreground" : "text-foreground",
thread.persistent === true && "italic",
)}
numberOfLines={1}
>
{thread.title}
</Text>
</View>
<View className="flex-row items-center gap-2">
{actionStatusIndicator}
{statusPill}
Expand Down
57 changes: 39 additions & 18 deletions apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
type ThreadListV2Status,
} from "./threadListV2";
import { ThreadSearchMatchExcerpt } from "./thread-search-match";
import { PersistentThreadIcon } from "./PersistentThreadIcon";

/**
* Thread List v2 renders one flat native list: rich edge-to-edge rows for
Expand Down Expand Up @@ -855,15 +856,25 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
</Text>
</View>
</View>
<Text
className={cn(
"mt-1 text-base font-t3-medium",
selected ? "text-user-bubble-foreground" : "text-foreground",
)}
numberOfLines={2}
>
{thread.title}
</Text>
<View className="mt-1 flex-row items-start gap-1.5">
{thread.persistent === true ? (
<View className="pt-1">
<PersistentThreadIcon
color={String(selected ? selectedForegroundColor : theme["--color-foreground"])}
/>
</View>
) : null}
<Text
className={cn(
"flex-1 text-base font-t3-medium",
selected ? "text-user-bubble-foreground" : "text-foreground",
thread.persistent === true && "italic",
)}
numberOfLines={2}
>
{thread.title}
</Text>
</View>
{props.searchMatch ? (
<View className="mt-1">
<ThreadSearchMatchExcerpt
Expand Down Expand Up @@ -1025,15 +1036,25 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
</View>
) : null}
<View className="min-w-0 flex-1">
<Text
className={cn(
"text-base",
selected ? "text-user-bubble-foreground" : "text-foreground-muted",
)}
numberOfLines={1}
>
{thread.title}
</Text>
<View className="flex-row items-center gap-1.5">
{thread.persistent === true ? (
<PersistentThreadIcon
color={String(
selected ? selectedForegroundColor : theme["--color-foreground-muted"],
)}
/>
) : null}
<Text
className={cn(
"flex-1 text-base",
selected ? "text-user-bubble-foreground" : "text-foreground-muted",
thread.persistent === true && "italic",
)}
numberOfLines={1}
>
{thread.title}
</Text>
</View>
{props.searchMatch ? (
<ThreadSearchMatchExcerpt
match={props.searchMatch}
Expand Down
8 changes: 7 additions & 1 deletion apps/server/src/cli/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const ThreadListResult = Schema.Struct({
Schema.Struct({
...ThreadIdentity.fields,
title: Schema.String,
persistent: Schema.Boolean,
lifecycle: Schema.String,
project: ThreadProject,
workspace: ThreadWorkspace,
Expand All @@ -120,6 +121,7 @@ export const ThreadReadResult = Schema.Struct({
kind: Schema.Literal("read"),
...ThreadIdentity.fields,
title: Schema.String,
persistent: Schema.Boolean,
lifecycle: Schema.String,
project: ThreadProject,
workspace: ThreadWorkspace,
Expand Down Expand Up @@ -621,7 +623,9 @@ export const listThreadsOutput = Effect.fn("listThreadsOutput")(function* (
const now = DateTime.formatIso(yield* DateTime.now);
const sortedThreads = source.shell.threads.toSorted(
(left, right) =>
right.updatedAt.localeCompare(left.updatedAt) || left.id.localeCompare(right.id),
Number(right.persistent === true) - Number(left.persistent === true) ||
right.updatedAt.localeCompare(left.updatedAt) ||
left.id.localeCompare(right.id),
);
const threadsTruncated = sortedThreads.length > THREAD_LIST_MAX_RESULTS;
return yield* decodeThreadListResult({
Expand All @@ -636,6 +640,7 @@ export const listThreadsOutput = Effect.fn("listThreadsOutput")(function* (
environmentId: source.descriptor.environmentId,
threadId: thread.id,
title: thread.title,
persistent: thread.persistent === true,
lifecycle: threadLifecycle(thread, { now }),
project: projectOutput(project),
workspace: workspaceOutput(project, thread),
Expand Down Expand Up @@ -665,6 +670,7 @@ export const readThreadOutput = Effect.fn("readThreadOutput")(function* (
environmentId: source.descriptor.environmentId,
threadId: resolution.thread.id,
title: resolution.thread.title,
persistent: resolution.thread.persistent === true,
lifecycle: threadLifecycle(resolution.thread, { now }),
project: projectOutput(project),
workspace: workspaceOutput(project, resolution.thread),
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export const make = Effect.gen(function* () {
threadSnooze: true,
environmentThemes: true,
threadPinning: true,
threadPersistence: true,
threadPinReorder: true,
threadTitleRegeneration: true,
threadPullRequestLinking: true,
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
snoozedUntil: null,
snoozedAt: null,
pinnedAt: null,
persistent: 0,
pinOrderKey: null,
titleRegenerationRequestId: null,
titleRegenerationStartedAt: null,
Expand Down Expand Up @@ -702,6 +703,22 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
return;
}

case "thread.persistence-changed": {
const affectedIds = [event.payload.threadId, event.payload.replacedThreadId].filter(
(threadId): threadId is ThreadId => threadId !== null,
);
for (const threadId of new Set(affectedIds)) {
const existingRow = yield* projectionThreadRepository.getById({ threadId });
if (Option.isNone(existingRow)) continue;
yield* projectionThreadRepository.upsert({
...existingRow.value,
persistent: threadId === event.payload.persistentThreadId ? 1 : 0,
updatedAt: event.payload.updatedAt,
});
}
return;
}

case "thread.settled": {
const existingRow = yield* projectionThreadRepository.getById({
threadId: event.payload.threadId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
snoozedUntil: null,
snoozedAt: null,
pinnedAt: "2026-02-24T00:00:01.000Z",
persistent: false,
pinOrderKey: "gm",
titleRegeneration: null,
annotation: {
Expand Down Expand Up @@ -475,6 +476,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
snoozedUntil: null,
snoozedAt: null,
pinnedAt: "2026-02-24T00:00:01.000Z",
persistent: false,
pinOrderKey: "gm",
titleRegeneration: null,
annotation: {
Expand Down
10 changes: 10 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozed_until AS "snoozedUntil",
snoozed_at AS "snoozedAt",
pinned_at AS "pinnedAt",
persistent,
pin_order_key AS "pinOrderKey",
title_regeneration_request_id AS "titleRegenerationRequestId",
title_regeneration_started_at AS "titleRegenerationStartedAt",
Expand Down Expand Up @@ -488,6 +489,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozed_until AS "snoozedUntil",
snoozed_at AS "snoozedAt",
pinned_at AS "pinnedAt",
persistent,
pin_order_key AS "pinOrderKey",
title_regeneration_request_id AS "titleRegenerationRequestId",
title_regeneration_started_at AS "titleRegenerationStartedAt",
Expand Down Expand Up @@ -531,6 +533,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozed_until AS "snoozedUntil",
snoozed_at AS "snoozedAt",
pinned_at AS "pinnedAt",
persistent,
pin_order_key AS "pinOrderKey",
title_regeneration_request_id AS "titleRegenerationRequestId",
title_regeneration_started_at AS "titleRegenerationStartedAt",
Expand Down Expand Up @@ -979,6 +982,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozed_until AS "snoozedUntil",
snoozed_at AS "snoozedAt",
pinned_at AS "pinnedAt",
persistent,
pin_order_key AS "pinOrderKey",
title_regeneration_request_id AS "titleRegenerationRequestId",
title_regeneration_started_at AS "titleRegenerationStartedAt",
Expand Down Expand Up @@ -1749,6 +1753,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozedUntil: row.snoozedUntil,
snoozedAt: row.snoozedAt,
pinnedAt: row.pinnedAt,
persistent: (row.persistent ?? 0) > 0,
pinOrderKey: row.pinOrderKey ?? null,
titleRegeneration: mapTitleRegeneration(row),
...(row.annotation !== null ? { annotation: row.annotation } : {}),
Expand Down Expand Up @@ -1962,6 +1967,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozedUntil: row.snoozedUntil,
snoozedAt: row.snoozedAt,
pinnedAt: row.pinnedAt,
persistent: (row.persistent ?? 0) > 0,
pinOrderKey: row.pinOrderKey ?? null,
titleRegeneration: mapTitleRegeneration(row),
...(row.annotation !== null ? { annotation: row.annotation } : {}),
Expand Down Expand Up @@ -2105,6 +2111,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozedUntil: row.snoozedUntil,
snoozedAt: row.snoozedAt,
pinnedAt: row.pinnedAt,
persistent: (row.persistent ?? 0) > 0,
pinOrderKey: row.pinOrderKey ?? null,
titleRegeneration: mapTitleRegeneration(row),
...(row.annotation !== null ? { annotation: row.annotation } : {}),
Expand Down Expand Up @@ -2259,6 +2266,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozedUntil: row.snoozedUntil,
snoozedAt: row.snoozedAt,
pinnedAt: row.pinnedAt,
persistent: (row.persistent ?? 0) > 0,
pinOrderKey: row.pinOrderKey ?? null,
titleRegeneration: mapTitleRegeneration(row),
...(row.annotation !== null ? { annotation: row.annotation } : {}),
Expand Down Expand Up @@ -2545,6 +2553,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozedUntil: threadRow.value.snoozedUntil,
snoozedAt: threadRow.value.snoozedAt,
pinnedAt: threadRow.value.pinnedAt,
persistent: (threadRow.value.persistent ?? 0) > 0,
pinOrderKey: threadRow.value.pinOrderKey ?? null,
titleRegeneration: mapTitleRegeneration(threadRow.value),
...(threadRow.value.annotation !== null ? { annotation: threadRow.value.annotation } : {}),
Expand Down Expand Up @@ -2695,6 +2704,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozedUntil: threadRow.value.snoozedUntil,
snoozedAt: threadRow.value.snoozedAt,
pinnedAt: threadRow.value.pinnedAt,
persistent: (threadRow.value.persistent ?? 0) > 0,
pinOrderKey: threadRow.value.pinOrderKey ?? null,
titleRegeneration: mapTitleRegeneration(threadRow.value),
...(threadRow.value.annotation !== null ? { annotation: threadRow.value.annotation } : {}),
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/orchestration/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ThreadDeletedPayload as ContractsThreadDeletedPayloadSchema,
ThreadWorktreeCleanupUpdatedPayload as ContractsThreadWorktreeCleanupUpdatedPayloadSchema,
ThreadUnarchivedPayload as ContractsThreadUnarchivedPayloadSchema,
ThreadPersistenceChangedPayload as ContractsThreadPersistenceChangedPayloadSchema,
ThreadUnsettledPayload as ContractsThreadUnsettledPayloadSchema,
ThreadSnoozedPayload as ContractsThreadSnoozedPayloadSchema,
ThreadUnsnoozedPayload as ContractsThreadUnsnoozedPayloadSchema,
Expand Down Expand Up @@ -46,6 +47,7 @@ export const ThreadDeletedPayload = ContractsThreadDeletedPayloadSchema;
export const ThreadWorktreeCleanupUpdatedPayload =
ContractsThreadWorktreeCleanupUpdatedPayloadSchema;
export const ThreadUnarchivedPayload = ContractsThreadUnarchivedPayloadSchema;
export const ThreadPersistenceChangedPayload = ContractsThreadPersistenceChangedPayloadSchema;
export const ThreadUnsettledPayload = ContractsThreadUnsettledPayloadSchema;
export const ThreadSnoozedPayload = ContractsThreadSnoozedPayloadSchema;
export const ThreadUnsnoozedPayload = ContractsThreadUnsnoozedPayloadSchema;
Expand Down
Loading
Loading