Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
a029d94
feat(context): persist typed inline context references
chrisdeeming Sep 5, 2026
ce13b7c
fix(context): address foundation review findings and unused exports
chrisdeeming Sep 6, 2026
048d4fa
fix(context): preserve reference targets and reject ambiguous identities
chrisdeeming Sep 6, 2026
28a7aff
fix(context): bound future context payloads
chrisdeeming Sep 6, 2026
dc29ed7
fix(context): load context in the optimized turn-start query
chrisdeeming Sep 6, 2026
7e0d7c3
fix(context): preserve malformed legacy context as text
chrisdeeming Sep 6, 2026
aa614eb
fix(context): distinguish preview boundaries from literal opening tags
chrisdeeming Sep 6, 2026
86ac63d
fix(context): preserve invalid legacy input and reject ambiguous uploads
chrisdeeming Sep 7, 2026
3a968ab
fix(contracts): retain structured preview style edits for context tra…
chrisdeeming Sep 7, 2026
ef1de64
fix(context): normalize labels on manually entered references
chrisdeeming Sep 7, 2026
df527a7
fix(context): bound aggregate payloads and preserve terminal token bo…
chrisdeeming Sep 7, 2026
59fb49c
fix(shared): upgrade terminal labels before punctuation
chrisdeeming Sep 7, 2026
e0c5c7d
fix(shared): preserve malformed legacy context and token boundaries
chrisdeeming Sep 7, 2026
29b90a2
fix(shared): preserve malformed legacy context atomically
chrisdeeming Sep 7, 2026
c317d6a
fix(shared): preserve review-shaped legacy context payloads
chrisdeeming Sep 7, 2026
633e4b9
fix(server): scope duplicate attachment IDs to turns
chrisdeeming Sep 8, 2026
ea1c13c
fix(server): project context before provider send
chrisdeeming Sep 8, 2026
5958ccb
test(server): use effect test runtime for context projection
chrisdeeming Sep 8, 2026
681e6f9
fix(shared): preserve markdown hard breaks during context upgrade
chrisdeeming Sep 8, 2026
8460b60
fix(server): tolerate renumbered context migration
chrisdeeming Sep 9, 2026
678e821
feat(web): move composer context into inline references
chrisdeeming Sep 5, 2026
fe6bebb
refactor(web): remove unused context helper exports
chrisdeeming Sep 6, 2026
f97c88f
refactor(web): defer paste helpers to the clipboard layer
chrisdeeming Sep 6, 2026
64fea72
fix(web): keep composer context references and draft records in sync
chrisdeeming Sep 7, 2026
af899f6
fix(web): serialize exact preview style targets and values
chrisdeeming Sep 7, 2026
9e1c176
test(web): cover structured annotation output in the composer layer
chrisdeeming Sep 7, 2026
873e4b0
fix(web): normalize legacy terminal identities and context-only titles
chrisdeeming Sep 7, 2026
8c9bc89
fix(web): preserve malformed history and focus tooltip-only context
chrisdeeming Sep 7, 2026
830ebf8
fix(web): namespace context identities and migrate persisted references
chrisdeeming Sep 7, 2026
dd70699
fix(web): resolve scoped references in the draft chip registry
chrisdeeming Sep 7, 2026
0223f08
fix(web): hide standalone images already referenced inline
chrisdeeming Sep 7, 2026
615f6ca
fix(web): report replacement attachments as accepted
chrisdeeming Sep 7, 2026
f87369c
fix(web): recall preview comments containing literal tag lines
chrisdeeming Sep 7, 2026
c838654
fix(web): preserve composer context identity and insertion behavior
chrisdeeming Sep 7, 2026
4a80b03
fix(web): prefer canonical context links during draft migration
chrisdeeming Sep 7, 2026
8b44469
fix(web): bind annotation screenshots only to images
chrisdeeming Sep 7, 2026
c4b192c
fix(web): restore file references with draft attachments
chrisdeeming Sep 7, 2026
1e6f649
fix(web): synchronize bulk preview annotation references
chrisdeeming Sep 7, 2026
c634ecf
fix(web): preserve composer context across servers, undo and large se…
chrisdeeming Sep 7, 2026
7416604
fix(web): preserve single-image draft contract
chrisdeeming Sep 8, 2026
5821e48
fix(web): retain file attachment list after rebase
chrisdeeming Sep 8, 2026
5509cda
fix(web): retain attachment context through undo
chrisdeeming Sep 8, 2026
4ca9c08
fix(web): keep inline files inside composer
chrisdeeming Sep 9, 2026
b471792
feat(context): preserve references across copy paste and stash
chrisdeeming Sep 5, 2026
7ee0462
fix(context): export record limit with its clipboard consumer
chrisdeeming Sep 6, 2026
4db32cf
fix(web): preserve context popovers in copied selections
chrisdeeming Sep 6, 2026
47b466d
refactor(context): introduce paste helpers with their consumers
chrisdeeming Sep 6, 2026
d84f6e8
fix(clipboard): align fragment budgets and refresh stash context call…
chrisdeeming Sep 7, 2026
6ab5989
fix(clipboard): restore preview styles from structured edit data
chrisdeeming Sep 7, 2026
21e17c4
fix(mobile): mask context labels in the compatibility review parser
chrisdeeming Sep 7, 2026
d1329a5
fix(clipboard): protect plain text and bound attachment downloads
chrisdeeming Sep 7, 2026
cd5da62
fix(clipboard): import draft context without an active thread
chrisdeeming Sep 7, 2026
f86cdbf
fix(clipboard): retain canonical positions for structured message copy
chrisdeeming Sep 7, 2026
20aad83
fix(web): preserve imported context ids and memoized chip rendering
chrisdeeming Sep 7, 2026
e0c699e
fix(web): keep context renderer dependencies precise
chrisdeeming Sep 7, 2026
ca8f89c
fix(web): preserve restorable attachment context in copy and stash
chrisdeeming Sep 7, 2026
8df5472
fix(web): keep pasted context distinct and annotation screenshots intact
chrisdeeming Sep 7, 2026
15e5fc3
fix(web): preserve dependent clipboard context
chrisdeeming Sep 8, 2026
8ed40e4
fix(clipboard): reuse attachments already in draft
chrisdeeming Sep 9, 2026
9d929c9
feat(web): unify context chip presentation and previews
chrisdeeming Sep 5, 2026
1bfbd28
refactor(web): share context chip slots and media dialog styling
chrisdeeming Sep 6, 2026
92f3eb6
refactor(web): reuse the shared terminal context popover
chrisdeeming Sep 6, 2026
3415d5f
refactor(web): reuse pull request state presentation
chrisdeeming Sep 6, 2026
014cd69
fix(web): vertically align inline context chips
chrisdeeming Sep 6, 2026
9a96728
refactor(web): share the inline context chip shell
chrisdeeming Sep 6, 2026
430bcdc
refactor(web): share the file context chip shell
chrisdeeming Sep 6, 2026
b3c4423
refactor(web): reuse the shared shell for terminal tooltips
chrisdeeming Sep 6, 2026
dd661fe
fix(web): expose unavailable file chip details to keyboard users
chrisdeeming Sep 7, 2026
ee287b2
test(web): retain review fence and source-comment regressions
chrisdeeming Sep 7, 2026
4980f6c
fix(web): render every known transcript context kind safely
chrisdeeming Sep 7, 2026
63a5503
fix(web): show unavailable state for empty annotation screenshots
chrisdeeming Sep 7, 2026
072411b
merge: sync inline context presentation with upstream main
chrisdeeming Sep 7, 2026
3e05400
fix(web): keep snapshot details components private
chrisdeeming Sep 8, 2026
e5de4f1
fix(web): compose context chips with button primitive
chrisdeeming Sep 8, 2026
b8e96e9
fix(web): preserve inline chip geometry
chrisdeeming Sep 9, 2026
d434be2
fix(web): reconcile the expanded image dialog with upstream
chrisdeeming Sep 10, 2026
b447428
fix(web): restore zoom, pan and snapshot details to the expanded dialog
chrisdeeming Sep 10, 2026
74c2422
fix(web): theme the terminal context popover
chrisdeeming Sep 12, 2026
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
Binary file added .pnpm-store/v11/index.db
Binary file not shown.
17 changes: 17 additions & 0 deletions apps/mobile/src/features/review/reviewCommentSelection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ function makeTarget(): ReviewCommentTarget {
}

describe("review comment serialization", () => {
it("keeps closing-tag text inside a chip label within a real review body", () => {
const body = "Before [</review_comment>](t3-context://v1/mention/context-1) after";
const serialized = `<review_comment sectionId="s" filePath="app.ts" startIndex="0" endIndex="0">${body}</review_comment>`;
const segments = parseReviewCommentMessageSegments(`${serialized} tail`);
expect(segments).toEqual([
{ kind: "review-comment", comment: expect.objectContaining({ text: body }) },
{ kind: "text", id: `review-comment-text:${serialized.length}`, text: " tail" },
]);
});

it("treats legacy markup inside a context label as opaque text", () => {
const text =
'[<review_comment sectionId="s" filePath="app.ts" startIndex="0" endIndex="0">Review this</review_comment>](t3-context://v1/mention/context-1)';
expect(parseReviewCommentMessageSegments(text)).toEqual([
{ kind: "text", id: "review-comment-text:0", text },
]);
});
it("preserves enough metadata for inline diff rendering", () => {
const serialized = formatReviewCommentContext(makeTarget(), "Please keep this configurable.");

Expand Down
23 changes: 18 additions & 5 deletions apps/mobile/src/features/review/reviewCommentSelection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useSyncExternalStore } from "react";
import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences";

import type { ReviewRenderableLineRow } from "./reviewModel";

Expand Down Expand Up @@ -286,9 +287,12 @@ export function parseReviewCommentMessageSegments(
const segments: ReviewCommentMessageSegment[] = [];
let cursor = 0;
let parsedCommentIndex = 0;

for (const match of value.matchAll(REVIEW_COMMENT_BLOCK_PATTERN)) {
const matchIndex = match.index ?? 0;
// Labels are opaque text, even when they contain legacy review markup. Keep offsets intact.
const masked = replaceComposerContextReferences(value, (reference) =>
" ".repeat(reference.source.length),
);
for (const match of masked.matchAll(REVIEW_COMMENT_BLOCK_PATTERN)) {
const matchIndex = match.index;
const beforeText = value.slice(cursor, matchIndex);
if (beforeText.length > 0) {
segments.push({
Expand All @@ -298,15 +302,24 @@ export function parseReviewCommentMessageSegments(
});
}

const comment = parseReviewInlineComment(match[1] ?? "", match[2] ?? "", parsedCommentIndex);
// Use the masked delimiters but read the original payload. Re-parsing raw text could
// mistake a closing tag inside a chip label for the end of the review.
const raw = value.slice(matchIndex, matchIndex + match[0].length);
const attributeStart = "<review_comment".length;
const attributeEnd = attributeStart + (match[1]?.length ?? 0);
const comment = parseReviewInlineComment(
raw.slice(attributeStart, attributeEnd),
raw.slice(attributeEnd + 1, -"</review_comment>".length),
parsedCommentIndex,
);
if (comment) {
segments.push({ kind: "review-comment", comment });
parsedCommentIndex += 1;
} else {
segments.push({
kind: "text",
id: `review-comment-invalid:${matchIndex}`,
text: match[0],
text: value.slice(matchIndex, matchIndex + match[0].length),
});
}

Expand Down
16 changes: 13 additions & 3 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
TurnId,
} from "@t3tools/contracts";
import { renderAssistantCitationsAsText } from "@t3tools/shared/assistantCitations";
import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences";
import {
codexArtifactTemplatePresentationLabel,
type CodexArtifactTemplate,
Expand Down Expand Up @@ -1677,13 +1678,22 @@ function UserMessageContent(props: {
readonly linkHandlers: MarkdownLinkHandlers;
readonly renderImage: MarkdownImageRenderer;
}) {
const segments = parseReviewCommentMessageSegments(props.text);
// Inline context references render as their labels until mobile grows chips for them.
const text = replaceComposerContextReferences(props.text, (occurrence) => occurrence.label);
const segments = parseReviewCommentMessageSegments(props.text).map((segment) =>
segment.kind === "text"
? {
...segment,
text: replaceComposerContextReferences(segment.text, (occurrence) => occurrence.label),
}
: segment,
);
const hasReviewComment = segments.some((segment) => segment.kind === "review-comment");
if (!hasReviewComment) {
if (hasNativeSelectableMarkdownText()) {
return (
<SelectableMarkdownText
markdown={props.text}
markdown={text}
skills={props.skills}
textStyle={props.markdownStyles.nativeTextStyle}
preserveSoftBreaks
Expand All @@ -1699,7 +1709,7 @@ function UserMessageContent(props: {
styles={props.markdownStyles.styles}
theme={props.markdownStyles.theme}
>
{props.text}
{text}
</Markdown>
);
}
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 @@ -219,6 +219,7 @@ export const make = Effect.gen(function* () {
questionAttachments: true,
fileAttachments: { maxUploadBytes: PROVIDER_SEND_TURN_MAX_FILE_BYTES },
pullRequests: true,
inlineMessageContext: true,
threadSettlement: true,
threadAutoSettlement: true,
threadRestartContinuation: true,
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
role: event.payload.role,
text: event.payload.text,
...(attachments !== undefined ? { attachments: [...attachments] } : {}),
...(event.payload.context !== undefined ? { context: event.payload.context } : {}),
createdAt: event.payload.createdAt,
updatedAt: event.payload.updatedAt,
});
Expand Down Expand Up @@ -1159,6 +1160,9 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
role: event.payload.role,
text: nextText,
...(nextAttachments !== undefined ? { attachments: [...nextAttachments] } : {}),
...((event.payload.context ?? previousMessage?.context) !== undefined
? { context: event.payload.context ?? previousMessage?.context }
: {}),
isStreaming: false,
createdAt: previousMessage?.createdAt ?? event.payload.createdAt,
updatedAt: event.payload.updatedAt,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type AgentSessionImportSource,
ChatAttachment,
ComposerContextId,
CheckpointRef,
EventId,
MessageId,
Expand All @@ -10,6 +11,7 @@ import {
ThreadLinkedPullRequest,
TurnId,
ProviderInstanceId,
OrchestrationMessageContext,
} from "@t3tools/contracts";
import { assert, it } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";
Expand Down Expand Up @@ -41,6 +43,9 @@ const encodeChatAttachments = Schema.encodeEffect(
const encodeThreadLinkedPullRequest = Schema.encodeSync(
Schema.fromJsonString(ThreadLinkedPullRequest),
);
const encodeMessageContext = Schema.encodeEffect(
Schema.fromJsonString(OrchestrationMessageContext),
);

const projectionSnapshotLayer = it.layer(
OrchestrationProjectionSnapshotQueryLive.pipe(
Expand Down Expand Up @@ -725,23 +730,39 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
},
];
const attachmentsJson = yield* encodeChatAttachments(attachments);
const messageContext: OrchestrationMessageContext = {
version: 1,
records: [
{
version: 1,
contextId: ComposerContextId.make("notes-context"),
kind: "file",
label: "notes.txt",
attachmentId: "notes",
name: "notes.txt",
mimeType: "text/plain",
sizeBytes: 8,
},
],
};
const contextJson = yield* encodeMessageContext(messageContext);
yield* sql`
WITH RECURSIVE history(n) AS (
VALUES (1) UNION ALL SELECT n + 1 FROM history WHERE n < 2000
)
INSERT INTO projection_thread_messages (
message_id, thread_id, turn_id, role, text, attachments_json,
message_id, thread_id, turn_id, role, text, attachments_json, context_json,
is_streaming, created_at, updated_at
)
SELECT 'turn-start-history:' || n, ${threadId}, 'old-turn:' || n, 'assistant',
'Unrelated assistant output', 'not-json', 0, ${createdAt}, ${createdAt}
'Unrelated assistant output', 'not-json', 'not-json', 0, ${createdAt}, ${createdAt}
FROM history
`;
yield* sql`
INSERT INTO projection_thread_messages (
message_id, thread_id, role, text, attachments_json, is_streaming, created_at, updated_at
message_id, thread_id, role, text, attachments_json, context_json, is_streaming, created_at, updated_at
) VALUES (${messageId}, ${threadId}, 'user', 'Read these notes',
${attachmentsJson}, 0, ${createdAt}, ${createdAt})
${attachmentsJson}, ${contextJson}, 0, ${createdAt}, ${createdAt})
`;
yield* sql`
INSERT INTO projection_thread_messages (
Expand All @@ -767,6 +788,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
createdAt,
updatedAt: createdAt,
attachments,
context: messageContext,
},
hasOtherUserMessages: false,
}),
Expand Down
13 changes: 12 additions & 1 deletion apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
AgentSessionImportSource,
ApprovalRequestId,
ChatAttachment,
OrchestrationMessageContext,
CheckpointRef,
IsoDateTime,
MessageId,
Expand Down Expand Up @@ -111,6 +112,7 @@ const ProjectionThreadMessageDbRowSchema = ProjectionThreadMessage.mapFields(
Struct.assign({
isStreaming: Schema.Number,
attachments: Schema.NullOr(Schema.fromJsonString(Schema.Array(ChatAttachment))),
context: Schema.NullOr(Schema.fromJsonString(OrchestrationMessageContext)),
}),
);
const ProjectionTurnStartMessageDbRowSchema = ProjectionThreadMessageDbRowSchema.mapFields(
Expand Down Expand Up @@ -677,6 +679,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
role,
text,
attachments_json AS "attachments",
context_json AS "context",
is_streaming AS "isStreaming",
created_at AS "createdAt",
updated_at AS "updatedAt"
Expand Down Expand Up @@ -1272,6 +1275,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
role,
text,
attachments_json AS "attachments",
context_json AS "context",
is_streaming AS "isStreaming",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down Expand Up @@ -1304,6 +1308,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
role,
text,
attachments_json AS "attachments",
context_json AS "context",
is_streaming AS "isStreaming",
created_at AS "createdAt",
updated_at AS "updatedAt"
Expand Down Expand Up @@ -1682,6 +1687,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
role,
text,
attachments_json AS "attachments",
context_json AS "context",
is_streaming AS "isStreaming",
created_at AS "createdAt",
updated_at AS "updatedAt"
Expand Down Expand Up @@ -2091,6 +2097,7 @@ pending_approval_requests AS (
role: row.role,
text: row.text,
...(row.attachments !== null ? { attachments: row.attachments } : {}),
...(row.context !== null ? { context: row.context } : {}),
turnId: row.turnId,
streaming: row.isStreaming === 1,
createdAt: row.createdAt,
Expand Down Expand Up @@ -3194,6 +3201,7 @@ pending_approval_requests AS (
createdAt: row.createdAt,
updatedAt: row.updatedAt,
...(row.attachments !== null ? { attachments: row.attachments } : {}),
...(row.context !== null ? { context: row.context } : {}),
},
hasOtherUserMessages: row.hasOtherUserMessages === 1,
}));
Expand Down Expand Up @@ -3453,7 +3461,10 @@ pending_approval_requests AS (
updatedAt: row.updatedAt,
};
if (row.attachments !== null) {
return Object.assign(message, { attachments: row.attachments });
Object.assign(message, { attachments: row.attachments });
}
if (row.context !== null) {
Object.assign(message, { context: row.context });
}
return message;
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { createModelSelection } from "@t3tools/shared/model";
import {
ApprovalRequestId,
CommandId,
ComposerContextId,
DEFAULT_PROVIDER_INTERACTION_MODE,
EnvironmentId,
EventId,
Expand Down Expand Up @@ -882,6 +883,51 @@ describe("ProviderCommandReactor", () => {
expect(thread?.session?.runtimeMode).toBe("approval-required");
});

effectIt.effect("projects inline context before sending the provider turn", () =>
Effect.gen(function* () {
const harness = yield* Effect.promise(() => createHarness());

yield* harness.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.make("cmd-turn-start-with-context"),
threadId: ThreadId.make("thread-1"),
message: {
messageId: asMessageId("user-message-with-context"),
role: "user",
text: "Inspect [build](t3-context://v1/terminal/terminal-1)",
attachments: [],
context: {
version: 1,
records: [
{
version: 1,
kind: "terminal",
contextId: ComposerContextId.make("terminal-1"),
label: "build",
terminalId: "terminal-1",
terminalLabel: "Build",
lineStart: 7,
lineEnd: 7,
text: "compiled successfully",
},
],
},
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
createdAt: "2026-01-01T00:00:00.000Z",
});

yield* Effect.promise(() => waitFor(() => harness.sendTurn.mock.calls.length === 1));
expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({
input: expect.stringContaining("[Terminal: build; ref=terminal-1]"),
});
expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({
input: expect.stringContaining('<context kind="terminal" id="terminal-1">'),
});
}),
);

effectIt.effect("retains a turn dispatched immediately after start until activation", () =>
Effect.gen(function* () {
const activation = yield* Deferred.make<void>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type TurnId,
} from "@t3tools/contracts";
import { assistantCitationsToPlainText } from "@t3tools/shared/assistantCitations";
import { projectComposerContextForProvider } from "@t3tools/shared/composerContextReferences";
import { isTemporaryWorktreeBranch, WORKTREE_BRANCH_PREFIX } from "@t3tools/shared/git";
import * as Cache from "effect/Cache";
import * as Cause from "effect/Cause";
Expand Down Expand Up @@ -1545,7 +1546,10 @@ const make = Effect.gen(function* () {
}
const sendTurnRequest = yield* buildSendTurnRequestForThread({
threadId: event.payload.threadId,
messageText: message.text,
messageText: projectComposerContextForProvider({
text: message.text,
records: message.context?.records ?? [],
}),
...(message.attachments !== undefined ? { attachments: message.attachments } : {}),
...(event.payload.modelSelection !== undefined
? { modelSelection: event.payload.modelSelection }
Expand Down
Loading
Loading