Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
3801bf7
feat(context): persist typed inline context references
chrisdeeming Sep 5, 2026
deeb7aa
fix(context): address foundation review findings and unused exports
chrisdeeming Sep 6, 2026
0be54e6
fix(context): preserve reference targets and reject ambiguous identities
chrisdeeming Sep 6, 2026
23b0c2c
fix(context): bound future context payloads
chrisdeeming Sep 6, 2026
94f851a
fix(context): load context in the optimized turn-start query
chrisdeeming Sep 6, 2026
5e79832
fix(context): preserve malformed legacy context as text
chrisdeeming Sep 6, 2026
4ac3bca
fix(context): distinguish preview boundaries from literal opening tags
chrisdeeming Sep 6, 2026
bf48981
fix(context): preserve invalid legacy input and reject ambiguous uploads
chrisdeeming Sep 7, 2026
a82bc47
fix(contracts): retain structured preview style edits for context tra…
chrisdeeming Sep 7, 2026
8913ecd
fix(context): normalize labels on manually entered references
chrisdeeming Sep 7, 2026
a45c7ee
fix(context): bound aggregate payloads and preserve terminal token bo…
chrisdeeming Sep 7, 2026
45f043d
fix(shared): upgrade terminal labels before punctuation
chrisdeeming Sep 7, 2026
3b0ed8a
fix(shared): preserve malformed legacy context and token boundaries
chrisdeeming Sep 7, 2026
febcd02
fix(shared): preserve malformed legacy context atomically
chrisdeeming Sep 7, 2026
4989ec5
fix(shared): preserve review-shaped legacy context payloads
chrisdeeming Sep 7, 2026
af0b250
fix(server): scope duplicate attachment IDs to turns
chrisdeeming Sep 8, 2026
a12086e
fix(server): project context before provider send
chrisdeeming Sep 8, 2026
72693aa
test(server): use effect test runtime for context projection
chrisdeeming Sep 8, 2026
1e915c3
fix(shared): preserve markdown hard breaks during context upgrade
chrisdeeming Sep 8, 2026
c33ff90
fix(server): tolerate renumbered context migration
chrisdeeming Sep 9, 2026
665ead2
feat(web): move composer context into inline references
chrisdeeming Sep 5, 2026
bf47ffd
refactor(web): remove unused context helper exports
chrisdeeming Sep 6, 2026
1e43230
refactor(web): defer paste helpers to the clipboard layer
chrisdeeming Sep 6, 2026
ead1dc5
fix(web): keep composer context references and draft records in sync
chrisdeeming Sep 7, 2026
cc05a7e
fix(web): serialize exact preview style targets and values
chrisdeeming Sep 7, 2026
d8f64d2
test(web): cover structured annotation output in the composer layer
chrisdeeming Sep 7, 2026
03041f6
fix(web): normalize legacy terminal identities and context-only titles
chrisdeeming Sep 7, 2026
1971afa
fix(web): preserve malformed history and focus tooltip-only context
chrisdeeming Sep 7, 2026
87dc42e
fix(web): namespace context identities and migrate persisted references
chrisdeeming Sep 7, 2026
113fe7c
fix(web): resolve scoped references in the draft chip registry
chrisdeeming Sep 7, 2026
bb4a7c2
fix(web): hide standalone images already referenced inline
chrisdeeming Sep 7, 2026
6895078
fix(web): report replacement attachments as accepted
chrisdeeming Sep 7, 2026
b46c9c5
fix(web): recall preview comments containing literal tag lines
chrisdeeming Sep 7, 2026
a4f1588
fix(web): preserve composer context identity and insertion behavior
chrisdeeming Sep 7, 2026
90400bc
fix(web): prefer canonical context links during draft migration
chrisdeeming Sep 7, 2026
e1a191a
fix(web): bind annotation screenshots only to images
chrisdeeming Sep 7, 2026
dca584e
fix(web): restore file references with draft attachments
chrisdeeming Sep 7, 2026
868941a
fix(web): synchronize bulk preview annotation references
chrisdeeming Sep 7, 2026
3fe2dd5
fix(web): preserve composer context across servers, undo and large se…
chrisdeeming Sep 7, 2026
742d6c0
fix(web): preserve single-image draft contract
chrisdeeming Sep 8, 2026
1e4440a
fix(web): retain file attachment list after rebase
chrisdeeming Sep 8, 2026
c856e03
fix(web): retain attachment context through undo
chrisdeeming Sep 8, 2026
072dc70
fix(web): keep inline files inside composer
chrisdeeming Sep 9, 2026
ac6f1f6
fix(server): renumber the context migration behind upstream's
chrisdeeming Sep 10, 2026
1958899
test(server): give the message context fixture upstream's pull reques…
chrisdeeming Sep 10, 2026
db0e74e
chore: stop tracking the local pnpm store
chrisdeeming Sep 10, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ node_modules/
*.log
.env*
!.env.example

# pnpm content-addressable store; never belongs in the repo.
.pnpm-store/
9 changes: 6 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,15 @@ 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(text);
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 +1702,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