Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
36a733f
feat(context): persist typed inline context references
chrisdeeming Sep 5, 2026
d294bbf
fix(context): address foundation review findings and unused exports
chrisdeeming Sep 6, 2026
4a9b263
fix(context): preserve reference targets and reject ambiguous identities
chrisdeeming Sep 6, 2026
c57b9ba
fix(context): bound future context payloads
chrisdeeming Sep 6, 2026
e17de71
fix(context): load context in the optimized turn-start query
chrisdeeming Sep 6, 2026
c26e8bc
fix(context): preserve malformed legacy context as text
chrisdeeming Sep 6, 2026
35db69b
fix(context): distinguish preview boundaries from literal opening tags
chrisdeeming Sep 6, 2026
5be285c
fix(context): preserve invalid legacy input and reject ambiguous uploads
chrisdeeming Sep 7, 2026
573436c
fix(contracts): retain structured preview style edits for context tra…
chrisdeeming Sep 7, 2026
87b0045
fix(context): normalize labels on manually entered references
chrisdeeming Sep 7, 2026
b8fc3ce
fix(context): bound aggregate payloads and preserve terminal token bo…
chrisdeeming Sep 7, 2026
3748439
fix(shared): upgrade terminal labels before punctuation
chrisdeeming Sep 7, 2026
28f6907
fix(shared): preserve malformed legacy context and token boundaries
chrisdeeming Sep 7, 2026
f6350a3
fix(shared): preserve malformed legacy context atomically
chrisdeeming Sep 7, 2026
370dfad
fix(shared): preserve review-shaped legacy context payloads
chrisdeeming Sep 7, 2026
f7f1a3f
fix(server): scope duplicate attachment IDs to turns
chrisdeeming Sep 8, 2026
d1dd284
fix(server): project context before provider send
chrisdeeming Sep 8, 2026
042bfcd
test(server): use effect test runtime for context projection
chrisdeeming Sep 8, 2026
a774d50
fix(shared): preserve markdown hard breaks during context upgrade
chrisdeeming Sep 8, 2026
0c56728
fix(server): tolerate renumbered context migration
chrisdeeming Sep 9, 2026
e3c6ee0
feat(web): move composer context into inline references
chrisdeeming Sep 5, 2026
14b1f09
refactor(web): remove unused context helper exports
chrisdeeming Sep 6, 2026
7132b69
refactor(web): defer paste helpers to the clipboard layer
chrisdeeming Sep 6, 2026
0237b01
fix(web): keep composer context references and draft records in sync
chrisdeeming Sep 7, 2026
3bd3bb6
fix(web): serialize exact preview style targets and values
chrisdeeming Sep 7, 2026
29455c7
test(web): cover structured annotation output in the composer layer
chrisdeeming Sep 7, 2026
2af2b09
fix(web): normalize legacy terminal identities and context-only titles
chrisdeeming Sep 7, 2026
bae2150
fix(web): preserve malformed history and focus tooltip-only context
chrisdeeming Sep 7, 2026
09b0ddf
fix(web): namespace context identities and migrate persisted references
chrisdeeming Sep 7, 2026
7d78ba0
fix(web): resolve scoped references in the draft chip registry
chrisdeeming Sep 7, 2026
34ac6c7
fix(web): hide standalone images already referenced inline
chrisdeeming Sep 7, 2026
24e7067
fix(web): report replacement attachments as accepted
chrisdeeming Sep 7, 2026
d198686
fix(web): recall preview comments containing literal tag lines
chrisdeeming Sep 7, 2026
63e2ae6
fix(web): preserve composer context identity and insertion behavior
chrisdeeming Sep 7, 2026
344e408
fix(web): prefer canonical context links during draft migration
chrisdeeming Sep 7, 2026
2ee15bc
fix(web): bind annotation screenshots only to images
chrisdeeming Sep 7, 2026
e08ddb1
fix(web): restore file references with draft attachments
chrisdeeming Sep 7, 2026
d1d9f43
fix(web): synchronize bulk preview annotation references
chrisdeeming Sep 7, 2026
627d113
fix(web): preserve composer context across servers, undo and large se…
chrisdeeming Sep 7, 2026
b068b0c
fix(web): preserve single-image draft contract
chrisdeeming Sep 8, 2026
f68901e
fix(web): retain file attachment list after rebase
chrisdeeming Sep 8, 2026
47b9e2c
fix(web): retain attachment context through undo
chrisdeeming Sep 8, 2026
7ab773a
fix(web): keep inline files inside composer
chrisdeeming Sep 9, 2026
3bb2ea1
feat(context): preserve references across copy paste and stash
chrisdeeming Sep 5, 2026
c33d31d
fix(context): export record limit with its clipboard consumer
chrisdeeming Sep 6, 2026
d0bbdc9
fix(web): preserve context popovers in copied selections
chrisdeeming Sep 6, 2026
261a979
refactor(context): introduce paste helpers with their consumers
chrisdeeming Sep 6, 2026
72214f7
fix(clipboard): align fragment budgets and refresh stash context call…
chrisdeeming Sep 7, 2026
68e6df3
fix(clipboard): restore preview styles from structured edit data
chrisdeeming Sep 7, 2026
c0d4095
fix(mobile): mask context labels in the compatibility review parser
chrisdeeming Sep 7, 2026
4157d37
fix(clipboard): protect plain text and bound attachment downloads
chrisdeeming Sep 7, 2026
9554cff
fix(clipboard): import draft context without an active thread
chrisdeeming Sep 7, 2026
d1b2fa7
fix(clipboard): retain canonical positions for structured message copy
chrisdeeming Sep 7, 2026
e241111
fix(web): preserve imported context ids and memoized chip rendering
chrisdeeming Sep 7, 2026
d8eaed7
fix(web): keep context renderer dependencies precise
chrisdeeming Sep 7, 2026
489370b
fix(web): preserve restorable attachment context in copy and stash
chrisdeeming Sep 7, 2026
53b51aa
fix(web): keep pasted context distinct and annotation screenshots intact
chrisdeeming Sep 7, 2026
8a8196a
fix(web): preserve dependent clipboard context
chrisdeeming Sep 8, 2026
32c8af0
fix(clipboard): reuse attachments already in draft
chrisdeeming Sep 9, 2026
43405da
feat(web): unify context chip presentation and previews
chrisdeeming Sep 5, 2026
e803598
refactor(web): share context chip slots and media dialog styling
chrisdeeming Sep 6, 2026
d90d5d7
refactor(web): reuse the shared terminal context popover
chrisdeeming Sep 6, 2026
44e74aa
refactor(web): reuse pull request state presentation
chrisdeeming Sep 6, 2026
69f36bd
fix(web): vertically align inline context chips
chrisdeeming Sep 6, 2026
5668ba3
refactor(web): share the inline context chip shell
chrisdeeming Sep 6, 2026
e7cb187
refactor(web): share the file context chip shell
chrisdeeming Sep 6, 2026
123d7b2
refactor(web): reuse the shared shell for terminal tooltips
chrisdeeming Sep 6, 2026
1e2b4ac
fix(web): expose unavailable file chip details to keyboard users
chrisdeeming Sep 7, 2026
28a3449
test(web): retain review fence and source-comment regressions
chrisdeeming Sep 7, 2026
88a8f65
fix(web): render every known transcript context kind safely
chrisdeeming Sep 7, 2026
5fdd3a1
fix(web): show unavailable state for empty annotation screenshots
chrisdeeming Sep 7, 2026
65cfcff
merge: sync inline context presentation with upstream main
chrisdeeming Sep 7, 2026
09a9e2e
fix(web): keep snapshot details components private
chrisdeeming Sep 8, 2026
97922ab
fix(web): compose context chips with button primitive
chrisdeeming Sep 8, 2026
03b121f
fix(web): preserve inline chip geometry
chrisdeeming Sep 9, 2026
f4ee873
feat(web): autocomplete repository pull request references
chrisdeeming Sep 5, 2026
e0222d9
fix(web): rank exact pull request matches ahead of substring matches
chrisdeeming Sep 7, 2026
ba4d0da
fix(web): reconcile the expanded image dialog with upstream
chrisdeeming Sep 10, 2026
9221bd1
fix(web): restore zoom, pan and snapshot details to the expanded dialog
chrisdeeming Sep 10, 2026
e8e8ffb
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