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
10 changes: 10 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,10 @@ import {
appendProjectBoardToTurnInput,
formatProjectBoardPromptBlock,
} from "../projectBoardPrompt.ts";
import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "../workspaceScopePrompt.ts";
import {
ProviderCommandReactor,
type ProviderCommandReactorShape,
Expand DownExpand Up@@ -863,6 +867,12 @@ const make = Effect.gen(function* () {
if (!rulesAlreadySent) threadsWithBoardRulesSent.add(input.threadId);
turnInput = appendProjectBoardToTurnInput(turnInput, boardBlock);
}
if (turnInput && thread.worktreePath) {
turnInput = prependWorkspaceScopeToTurnInput(
turnInput,
formatWorkspaceScopePromptBlock({ cwd: thread.worktreePath, branch: thread.branch }),
);
}

return {
threadId: input.threadId,
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ import {
type ProviderRuntimeIngestionShape,
} from "../Services/ProviderRuntimeIngestion.ts";
import { projectActivityPayload } from "../ActivityPayloadProjection.ts";
import { maybeWorkspaceScopeWarningActivity } from "../workspaceScopeWarning.ts";
import { forkParked } from "../../serverActivation.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { canReplaceThreadTitle } from "../threadTitles.ts";
Expand DownExpand Up@@ -900,6 +901,7 @@ const make = Effect.gen(function* () {
crypto.randomUUIDv4.pipe(
Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)),
);
const warnedWorkspaceScopeKeys = new Set<string>();

const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({
capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY,
Expand DownExpand Up@@ -2041,6 +2043,21 @@ const make = Effect.gen(function* () {
),
),
).pipe(Effect.asVoid);
const workspaceScopeWarning = maybeWorkspaceScopeWarningActivity({
event,
worktreePath: thread.worktreePath,
warnedKeys: warnedWorkspaceScopeKeys,
});
if (workspaceScopeWarning) {
const commandId = yield* providerCommandId(event, "thread-activity-append-workspace-scope");
yield* orchestrationEngine.dispatch({
type: "thread.activity.append",
commandId,
threadId: thread.id,
activity: workspaceScopeWarning,
createdAt: workspaceScopeWarning.createdAt,
});
}
});

const processDomainEvent = (_event: TurnStartRequestedDomainEvent) => Effect.void;
Expand Down
28 changes: 28 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vite-plus/test";

import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "./workspaceScopePrompt.ts";

describe("workspaceScopePrompt", () => {
it("names the assigned worktree path and branch", () => {
const block = formatWorkspaceScopePromptBlock({
cwd: "/tmp/worktree",
branch: "t3code/update-main-from-github",
});
expect(block).toContain("<t3_workspace_scope>");
expect(block).toContain("Path: /tmp/worktree");
expect(block).toContain("Branch: t3code/update-main-from-github");
expect(block).toContain("Do not switch to another clone");
});

it("prepends the block so the constraint is visible before the user prompt", () => {
expect(
prependWorkspaceScopeToTurnInput(
"fix the bug",
"<t3_workspace_scope>\nscoped\n</t3_workspace_scope>",
),
).toBe("<t3_workspace_scope>\nscoped\n</t3_workspace_scope>\n\nfix the bug");
});
});
24 changes: 24 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
export function formatWorkspaceScopePromptBlock(input: {
readonly cwd: string;
readonly branch: string | null;
}): string {
const branchLine = input.branch ? `Branch: ${input.branch}` : "Branch: (unknown)";
return [
"<t3_workspace_scope>",
"This thread is attached to a Git worktree. All file edits, commits, git commands, and dev servers must use this directory as the working directory. Do not switch to another clone of the same repository.",
`Path: ${input.cwd}`,
branchLine,
"If you need a different path, stop and ask the user to confirm that exact path first.",
"</t3_workspace_scope>",
].join("\n");
}

export function prependWorkspaceScopeToTurnInput(
input: string | undefined,
block: string | null,
): string | undefined {
if (!block) return input;
const trimmed = input?.trim();
if (!trimmed) return block;
return `${block}\n\n${trimmed}`;
}
86 changes: 86 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
import {
EventId,
ProviderDriverKind,
ThreadId,
TurnId,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { maybeWorkspaceScopeWarningActivity } from "./workspaceScopeWarning.ts";

const worktree = "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c";
const otherClone = "/Users/me/Documents/Project/marswalk";

const base = {
provider: ProviderDriverKind.make("cursor"),
createdAt: "2026-08-31T12:00:00.000Z",
threadId: ThreadId.make("thread-1"),
turnId: TurnId.make("turn-1"),
};

function toolEvent(eventId: string, data: Record<string, unknown>): ProviderRuntimeEvent {
return {
...base,
type: "item.completed",
eventId: EventId.make(eventId),
payload: {
itemType: "command_execution",
title: "Terminal",
data,
},
} satisfies ProviderRuntimeEvent;
}

describe("workspaceScopeWarning", () => {
it("warns when a command's working directory is another clone", () => {
const warnedKeys = new Set<string>();
const activity = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", {
command: "git status",
rawInput: { working_directory: otherClone },
}),
worktreePath: worktree,
warnedKeys,
});
expect(activity?.kind).toBe("runtime.warning");
expect(activity?.summary).toContain("different checkout");
const payload = activity?.payload as { detail?: string };
expect(payload.detail).toContain(otherClone);
expect(payload.detail).toContain(worktree);
expect(payload.detail).toContain("Provider: cursor");
});

it("warns only once per escaped path on a thread", () => {
const warnedKeys = new Set<string>();
const first = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
const second = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-2", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
expect(first).not.toBeNull();
expect(second).toBeNull();
});

it("stays quiet when the thread has no worktree or the command stays in it", () => {
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: otherClone } }),
worktreePath: null,
warnedKeys: new Set(),
}),
).toBeNull();
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: worktree } }),
worktreePath: worktree,
warnedKeys: new Set(),
}),
).toBeNull();
});
});
73 changes: 73 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import {
EventId,
type OrchestrationThreadActivity,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { findOutOfWorkspaceScopePath } from "@t3tools/shared/workspaceScope";

const WARNABLE_EVENT_TYPES = new Set([
"item.started",
"item.updated",
"item.completed",
"request.opened",
]);

const FILE_PATH_ITEM_TYPES = new Set([
"command_execution",
"file_change",
"file_edit",
"create_file",
"edit",
]);

function asRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}

function payloadLooksLikeFileMutation(payload: Record<string, unknown>): boolean {
const itemType = typeof payload.itemType === "string" ? payload.itemType : undefined;
if (itemType && FILE_PATH_ITEM_TYPES.has(itemType)) {
return true;
}
const requestType = typeof payload.requestType === "string" ? payload.requestType : undefined;
return requestType === "command" || requestType === "file-change";
}

export function maybeWorkspaceScopeWarningActivity(input: {
readonly event: ProviderRuntimeEvent;
readonly worktreePath: string | null;
readonly warnedKeys: Set<string>;
}): OrchestrationThreadActivity | null {
const worktreePath = input.worktreePath?.trim() ?? "";
if (!worktreePath || !WARNABLE_EVENT_TYPES.has(input.event.type)) {
return null;
}
const payload = asRecord(input.event.payload) ?? {};
const outOfScopePath = findOutOfWorkspaceScopePath({
workspaceRoot: worktreePath,
data: payload,
includeFilePaths: payloadLooksLikeFileMutation(payload),
});
if (!outOfScopePath) {
return null;
}
const warningKey = `${input.event.threadId}:${outOfScopePath}`;
if (input.warnedKeys.has(warningKey)) {
return null;
}
input.warnedKeys.add(warningKey);
return {
id: EventId.make(`workspace-scope:${input.event.eventId}`),
createdAt: input.event.createdAt,
tone: "info",
kind: "runtime.warning",
summary: "Agent used a different checkout than this thread's worktree",
payload: {
message: "Agent used a different checkout than this thread's worktree",
detail: `Thread worktree: ${worktreePath}\nUsed path: ${outOfScopePath}\nProvider: ${input.event.provider}`,
},
turnId: input.event.turnId ?? null,
};
}
14 changes: 14 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,10 @@ import { cn } from "~/lib/utils";
import { useUiStateStore } from "~/uiStateStore";
import { type TimestampFormat } from "@t3tools/contracts/settings";
import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat";
import {
parseWorkspaceScopeWarning,
WorkspaceScopeWarningRow,
} from "../../workspaceScopeWarningUi";

import {
buildInlineTerminalContextText,
Expand DownExpand Up@@ -2718,7 +2722,17 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
isExpandedToolGroupEntry: boolean;
}) {
const { workEntry, workspaceRoot, isExpandedToolGroupEntry } = props;
const workspaceScopeMismatch = parseWorkspaceScopeWarning(workEntry);
const [expanded, setExpanded] = useState(false);
if (workspaceScopeMismatch) {
return (
<WorkspaceScopeWarningRow
label={workEntry.label}
mismatch={workspaceScopeMismatch}
isExpandedToolGroupEntry={isExpandedToolGroupEntry}
/>
);
}
const iconConfig = workToneIcon(workEntry.tone);
const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning";
const showFailedIndicator = workEntryDisplayIndicatesToolFailure(workEntry);
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/workspaceScopeWarningUi.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
import { ProviderDriverKind } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { parseWorkspaceScopeWarning } from "./workspaceScopeWarningUi";

describe("parseWorkspaceScopeWarning", () => {
it("reads aligned thread and agent paths from the warning detail", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk\nProvider: cursor",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: ProviderDriverKind.make("cursor"),
});
});

it("ignores other runtime warnings", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail: "Model rerouted to a fallback",
}),
).toBeNull();
});

it("keeps older warnings that have no provider line", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: null,
});
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
10 changes: 10 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,10 @@ import {
appendProjectBoardToTurnInput,
formatProjectBoardPromptBlock,
} from "../projectBoardPrompt.ts";
import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "../workspaceScopePrompt.ts";
import {
ProviderCommandReactor,
type ProviderCommandReactorShape,
Expand DownExpand Up@@ -863,6 +867,12 @@ const make = Effect.gen(function* () {
if (!rulesAlreadySent) threadsWithBoardRulesSent.add(input.threadId);
turnInput = appendProjectBoardToTurnInput(turnInput, boardBlock);
}
if (turnInput && thread.worktreePath) {
turnInput = prependWorkspaceScopeToTurnInput(
turnInput,
formatWorkspaceScopePromptBlock({ cwd: thread.worktreePath, branch: thread.branch }),
);
}

return {
threadId: input.threadId,
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ import {
type ProviderRuntimeIngestionShape,
} from "../Services/ProviderRuntimeIngestion.ts";
import { projectActivityPayload } from "../ActivityPayloadProjection.ts";
import { maybeWorkspaceScopeWarningActivity } from "../workspaceScopeWarning.ts";
import { forkParked } from "../../serverActivation.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { canReplaceThreadTitle } from "../threadTitles.ts";
Expand DownExpand Up@@ -900,6 +901,7 @@ const make = Effect.gen(function* () {
crypto.randomUUIDv4.pipe(
Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)),
);
const warnedWorkspaceScopeKeys = new Set<string>();

const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({
capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY,
Expand DownExpand Up@@ -2041,6 +2043,21 @@ const make = Effect.gen(function* () {
),
),
).pipe(Effect.asVoid);
const workspaceScopeWarning = maybeWorkspaceScopeWarningActivity({
event,
worktreePath: thread.worktreePath,
warnedKeys: warnedWorkspaceScopeKeys,
});
if (workspaceScopeWarning) {
const commandId = yield* providerCommandId(event, "thread-activity-append-workspace-scope");
yield* orchestrationEngine.dispatch({
type: "thread.activity.append",
commandId,
threadId: thread.id,
activity: workspaceScopeWarning,
createdAt: workspaceScopeWarning.createdAt,
});
}
});

const processDomainEvent = (_event: TurnStartRequestedDomainEvent) => Effect.void;
Expand Down
28 changes: 28 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vite-plus/test";

import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "./workspaceScopePrompt.ts";

describe("workspaceScopePrompt", () => {
it("names the assigned worktree path and branch", () => {
const block = formatWorkspaceScopePromptBlock({
cwd: "/tmp/worktree",
branch: "t3code/update-main-from-github",
});
expect(block).toContain("<t3_workspace_scope>");
expect(block).toContain("Path: /tmp/worktree");
expect(block).toContain("Branch: t3code/update-main-from-github");
expect(block).toContain("Do not switch to another clone");
});

it("prepends the block so the constraint is visible before the user prompt", () => {
expect(
prependWorkspaceScopeToTurnInput(
"fix the bug",
"<t3_workspace_scope>\nscoped\n</t3_workspace_scope>",
),
).toBe("<t3_workspace_scope>\nscoped\n</t3_workspace_scope>\n\nfix the bug");
});
});
24 changes: 24 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
export function formatWorkspaceScopePromptBlock(input: {
readonly cwd: string;
readonly branch: string | null;
}): string {
const branchLine = input.branch ? `Branch: ${input.branch}` : "Branch: (unknown)";
return [
"<t3_workspace_scope>",
"This thread is attached to a Git worktree. All file edits, commits, git commands, and dev servers must use this directory as the working directory. Do not switch to another clone of the same repository.",
`Path: ${input.cwd}`,
branchLine,
"If you need a different path, stop and ask the user to confirm that exact path first.",
"</t3_workspace_scope>",
].join("\n");
}

export function prependWorkspaceScopeToTurnInput(
input: string | undefined,
block: string | null,
): string | undefined {
if (!block) return input;
const trimmed = input?.trim();
if (!trimmed) return block;
return `${block}\n\n${trimmed}`;
}
86 changes: 86 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
import {
EventId,
ProviderDriverKind,
ThreadId,
TurnId,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { maybeWorkspaceScopeWarningActivity } from "./workspaceScopeWarning.ts";

const worktree = "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c";
const otherClone = "/Users/me/Documents/Project/marswalk";

const base = {
provider: ProviderDriverKind.make("cursor"),
createdAt: "2026-08-31T12:00:00.000Z",
threadId: ThreadId.make("thread-1"),
turnId: TurnId.make("turn-1"),
};

function toolEvent(eventId: string, data: Record<string, unknown>): ProviderRuntimeEvent {
return {
...base,
type: "item.completed",
eventId: EventId.make(eventId),
payload: {
itemType: "command_execution",
title: "Terminal",
data,
},
} satisfies ProviderRuntimeEvent;
}

describe("workspaceScopeWarning", () => {
it("warns when a command's working directory is another clone", () => {
const warnedKeys = new Set<string>();
const activity = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", {
command: "git status",
rawInput: { working_directory: otherClone },
}),
worktreePath: worktree,
warnedKeys,
});
expect(activity?.kind).toBe("runtime.warning");
expect(activity?.summary).toContain("different checkout");
const payload = activity?.payload as { detail?: string };
expect(payload.detail).toContain(otherClone);
expect(payload.detail).toContain(worktree);
expect(payload.detail).toContain("Provider: cursor");
});

it("warns only once per escaped path on a thread", () => {
const warnedKeys = new Set<string>();
const first = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
const second = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-2", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
expect(first).not.toBeNull();
expect(second).toBeNull();
});

it("stays quiet when the thread has no worktree or the command stays in it", () => {
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: otherClone } }),
worktreePath: null,
warnedKeys: new Set(),
}),
).toBeNull();
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: worktree } }),
worktreePath: worktree,
warnedKeys: new Set(),
}),
).toBeNull();
});
});
73 changes: 73 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import {
EventId,
type OrchestrationThreadActivity,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { findOutOfWorkspaceScopePath } from "@t3tools/shared/workspaceScope";

const WARNABLE_EVENT_TYPES = new Set([
"item.started",
"item.updated",
"item.completed",
"request.opened",
]);

const FILE_PATH_ITEM_TYPES = new Set([
"command_execution",
"file_change",
"file_edit",
"create_file",
"edit",
]);

function asRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}

function payloadLooksLikeFileMutation(payload: Record<string, unknown>): boolean {
const itemType = typeof payload.itemType === "string" ? payload.itemType : undefined;
if (itemType && FILE_PATH_ITEM_TYPES.has(itemType)) {
return true;
}
const requestType = typeof payload.requestType === "string" ? payload.requestType : undefined;
return requestType === "command" || requestType === "file-change";
}

export function maybeWorkspaceScopeWarningActivity(input: {
readonly event: ProviderRuntimeEvent;
readonly worktreePath: string | null;
readonly warnedKeys: Set<string>;
}): OrchestrationThreadActivity | null {
const worktreePath = input.worktreePath?.trim() ?? "";
if (!worktreePath || !WARNABLE_EVENT_TYPES.has(input.event.type)) {
return null;
}
const payload = asRecord(input.event.payload) ?? {};
const outOfScopePath = findOutOfWorkspaceScopePath({
workspaceRoot: worktreePath,
data: payload,
includeFilePaths: payloadLooksLikeFileMutation(payload),
});
if (!outOfScopePath) {
return null;
}
const warningKey = `${input.event.threadId}:${outOfScopePath}`;
if (input.warnedKeys.has(warningKey)) {
return null;
}
input.warnedKeys.add(warningKey);
return {
id: EventId.make(`workspace-scope:${input.event.eventId}`),
createdAt: input.event.createdAt,
tone: "info",
kind: "runtime.warning",
summary: "Agent used a different checkout than this thread's worktree",
payload: {
message: "Agent used a different checkout than this thread's worktree",
detail: `Thread worktree: ${worktreePath}\nUsed path: ${outOfScopePath}\nProvider: ${input.event.provider}`,
},
turnId: input.event.turnId ?? null,
};
}
14 changes: 14 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,10 @@ import { cn } from "~/lib/utils";
import { useUiStateStore } from "~/uiStateStore";
import { type TimestampFormat } from "@t3tools/contracts/settings";
import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat";
import {
parseWorkspaceScopeWarning,
WorkspaceScopeWarningRow,
} from "../../workspaceScopeWarningUi";

import {
buildInlineTerminalContextText,
Expand DownExpand Up@@ -2718,7 +2722,17 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
isExpandedToolGroupEntry: boolean;
}) {
const { workEntry, workspaceRoot, isExpandedToolGroupEntry } = props;
const workspaceScopeMismatch = parseWorkspaceScopeWarning(workEntry);
const [expanded, setExpanded] = useState(false);
if (workspaceScopeMismatch) {
return (
<WorkspaceScopeWarningRow
label={workEntry.label}
mismatch={workspaceScopeMismatch}
isExpandedToolGroupEntry={isExpandedToolGroupEntry}
/>
);
}
const iconConfig = workToneIcon(workEntry.tone);
const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning";
const showFailedIndicator = workEntryDisplayIndicatesToolFailure(workEntry);
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/workspaceScopeWarningUi.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
import { ProviderDriverKind } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { parseWorkspaceScopeWarning } from "./workspaceScopeWarningUi";

describe("parseWorkspaceScopeWarning", () => {
it("reads aligned thread and agent paths from the warning detail", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk\nProvider: cursor",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: ProviderDriverKind.make("cursor"),
});
});

it("ignores other runtime warnings", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail: "Model rerouted to a fallback",
}),
).toBeNull();
});

it("keeps older warnings that have no provider line", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: null,
});
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
10 changes: 10 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,10 @@ import {
appendProjectBoardToTurnInput,
formatProjectBoardPromptBlock,
} from "../projectBoardPrompt.ts";
import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "../workspaceScopePrompt.ts";
import {
ProviderCommandReactor,
type ProviderCommandReactorShape,
Expand DownExpand Up@@ -863,6 +867,12 @@ const make = Effect.gen(function* () {
if (!rulesAlreadySent) threadsWithBoardRulesSent.add(input.threadId);
turnInput = appendProjectBoardToTurnInput(turnInput, boardBlock);
}
if (turnInput && thread.worktreePath) {
turnInput = prependWorkspaceScopeToTurnInput(
turnInput,
formatWorkspaceScopePromptBlock({ cwd: thread.worktreePath, branch: thread.branch }),
);
}

return {
threadId: input.threadId,
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ import {
type ProviderRuntimeIngestionShape,
} from "../Services/ProviderRuntimeIngestion.ts";
import { projectActivityPayload } from "../ActivityPayloadProjection.ts";
import { maybeWorkspaceScopeWarningActivity } from "../workspaceScopeWarning.ts";
import { forkParked } from "../../serverActivation.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { canReplaceThreadTitle } from "../threadTitles.ts";
Expand DownExpand Up@@ -900,6 +901,7 @@ const make = Effect.gen(function* () {
crypto.randomUUIDv4.pipe(
Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)),
);
const warnedWorkspaceScopeKeys = new Set<string>();

const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({
capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY,
Expand DownExpand Up@@ -2041,6 +2043,21 @@ const make = Effect.gen(function* () {
),
),
).pipe(Effect.asVoid);
const workspaceScopeWarning = maybeWorkspaceScopeWarningActivity({
event,
worktreePath: thread.worktreePath,
warnedKeys: warnedWorkspaceScopeKeys,
});
if (workspaceScopeWarning) {
const commandId = yield* providerCommandId(event, "thread-activity-append-workspace-scope");
yield* orchestrationEngine.dispatch({
type: "thread.activity.append",
commandId,
threadId: thread.id,
activity: workspaceScopeWarning,
createdAt: workspaceScopeWarning.createdAt,
});
}
});

const processDomainEvent = (_event: TurnStartRequestedDomainEvent) => Effect.void;
Expand Down
28 changes: 28 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vite-plus/test";

import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "./workspaceScopePrompt.ts";

describe("workspaceScopePrompt", () => {
it("names the assigned worktree path and branch", () => {
const block = formatWorkspaceScopePromptBlock({
cwd: "/tmp/worktree",
branch: "t3code/update-main-from-github",
});
expect(block).toContain("<t3_workspace_scope>");
expect(block).toContain("Path: /tmp/worktree");
expect(block).toContain("Branch: t3code/update-main-from-github");
expect(block).toContain("Do not switch to another clone");
});

it("prepends the block so the constraint is visible before the user prompt", () => {
expect(
prependWorkspaceScopeToTurnInput(
"fix the bug",
"<t3_workspace_scope>\nscoped\n</t3_workspace_scope>",
),
).toBe("<t3_workspace_scope>\nscoped\n</t3_workspace_scope>\n\nfix the bug");
});
});
24 changes: 24 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
export function formatWorkspaceScopePromptBlock(input: {
readonly cwd: string;
readonly branch: string | null;
}): string {
const branchLine = input.branch ? `Branch: ${input.branch}` : "Branch: (unknown)";
return [
"<t3_workspace_scope>",
"This thread is attached to a Git worktree. All file edits, commits, git commands, and dev servers must use this directory as the working directory. Do not switch to another clone of the same repository.",
`Path: ${input.cwd}`,
branchLine,
"If you need a different path, stop and ask the user to confirm that exact path first.",
"</t3_workspace_scope>",
].join("\n");
}

export function prependWorkspaceScopeToTurnInput(
input: string | undefined,
block: string | null,
): string | undefined {
if (!block) return input;
const trimmed = input?.trim();
if (!trimmed) return block;
return `${block}\n\n${trimmed}`;
}
86 changes: 86 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
import {
EventId,
ProviderDriverKind,
ThreadId,
TurnId,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { maybeWorkspaceScopeWarningActivity } from "./workspaceScopeWarning.ts";

const worktree = "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c";
const otherClone = "/Users/me/Documents/Project/marswalk";

const base = {
provider: ProviderDriverKind.make("cursor"),
createdAt: "2026-08-31T12:00:00.000Z",
threadId: ThreadId.make("thread-1"),
turnId: TurnId.make("turn-1"),
};

function toolEvent(eventId: string, data: Record<string, unknown>): ProviderRuntimeEvent {
return {
...base,
type: "item.completed",
eventId: EventId.make(eventId),
payload: {
itemType: "command_execution",
title: "Terminal",
data,
},
} satisfies ProviderRuntimeEvent;
}

describe("workspaceScopeWarning", () => {
it("warns when a command's working directory is another clone", () => {
const warnedKeys = new Set<string>();
const activity = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", {
command: "git status",
rawInput: { working_directory: otherClone },
}),
worktreePath: worktree,
warnedKeys,
});
expect(activity?.kind).toBe("runtime.warning");
expect(activity?.summary).toContain("different checkout");
const payload = activity?.payload as { detail?: string };
expect(payload.detail).toContain(otherClone);
expect(payload.detail).toContain(worktree);
expect(payload.detail).toContain("Provider: cursor");
});

it("warns only once per escaped path on a thread", () => {
const warnedKeys = new Set<string>();
const first = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
const second = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-2", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
expect(first).not.toBeNull();
expect(second).toBeNull();
});

it("stays quiet when the thread has no worktree or the command stays in it", () => {
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: otherClone } }),
worktreePath: null,
warnedKeys: new Set(),
}),
).toBeNull();
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: worktree } }),
worktreePath: worktree,
warnedKeys: new Set(),
}),
).toBeNull();
});
});
73 changes: 73 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import {
EventId,
type OrchestrationThreadActivity,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { findOutOfWorkspaceScopePath } from "@t3tools/shared/workspaceScope";

const WARNABLE_EVENT_TYPES = new Set([
"item.started",
"item.updated",
"item.completed",
"request.opened",
]);

const FILE_PATH_ITEM_TYPES = new Set([
"command_execution",
"file_change",
"file_edit",
"create_file",
"edit",
]);

function asRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}

function payloadLooksLikeFileMutation(payload: Record<string, unknown>): boolean {
const itemType = typeof payload.itemType === "string" ? payload.itemType : undefined;
if (itemType && FILE_PATH_ITEM_TYPES.has(itemType)) {
return true;
}
const requestType = typeof payload.requestType === "string" ? payload.requestType : undefined;
return requestType === "command" || requestType === "file-change";
}

export function maybeWorkspaceScopeWarningActivity(input: {
readonly event: ProviderRuntimeEvent;
readonly worktreePath: string | null;
readonly warnedKeys: Set<string>;
}): OrchestrationThreadActivity | null {
const worktreePath = input.worktreePath?.trim() ?? "";
if (!worktreePath || !WARNABLE_EVENT_TYPES.has(input.event.type)) {
return null;
}
const payload = asRecord(input.event.payload) ?? {};
const outOfScopePath = findOutOfWorkspaceScopePath({
workspaceRoot: worktreePath,
data: payload,
includeFilePaths: payloadLooksLikeFileMutation(payload),
});
if (!outOfScopePath) {
return null;
}
const warningKey = `${input.event.threadId}:${outOfScopePath}`;
if (input.warnedKeys.has(warningKey)) {
return null;
}
input.warnedKeys.add(warningKey);
return {
id: EventId.make(`workspace-scope:${input.event.eventId}`),
createdAt: input.event.createdAt,
tone: "info",
kind: "runtime.warning",
summary: "Agent used a different checkout than this thread's worktree",
payload: {
message: "Agent used a different checkout than this thread's worktree",
detail: `Thread worktree: ${worktreePath}\nUsed path: ${outOfScopePath}\nProvider: ${input.event.provider}`,
},
turnId: input.event.turnId ?? null,
};
}
14 changes: 14 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,10 @@ import { cn } from "~/lib/utils";
import { useUiStateStore } from "~/uiStateStore";
import { type TimestampFormat } from "@t3tools/contracts/settings";
import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat";
import {
parseWorkspaceScopeWarning,
WorkspaceScopeWarningRow,
} from "../../workspaceScopeWarningUi";

import {
buildInlineTerminalContextText,
Expand DownExpand Up@@ -2718,7 +2722,17 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
isExpandedToolGroupEntry: boolean;
}) {
const { workEntry, workspaceRoot, isExpandedToolGroupEntry } = props;
const workspaceScopeMismatch = parseWorkspaceScopeWarning(workEntry);
const [expanded, setExpanded] = useState(false);
if (workspaceScopeMismatch) {
return (
<WorkspaceScopeWarningRow
label={workEntry.label}
mismatch={workspaceScopeMismatch}
isExpandedToolGroupEntry={isExpandedToolGroupEntry}
/>
);
}
const iconConfig = workToneIcon(workEntry.tone);
const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning";
const showFailedIndicator = workEntryDisplayIndicatesToolFailure(workEntry);
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/workspaceScopeWarningUi.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
import { ProviderDriverKind } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { parseWorkspaceScopeWarning } from "./workspaceScopeWarningUi";

describe("parseWorkspaceScopeWarning", () => {
it("reads aligned thread and agent paths from the warning detail", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk\nProvider: cursor",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: ProviderDriverKind.make("cursor"),
});
});

it("ignores other runtime warnings", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail: "Model rerouted to a fallback",
}),
).toBeNull();
});

it("keeps older warnings that have no provider line", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: null,
});
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
10 changes: 10 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,10 @@ import {
appendProjectBoardToTurnInput,
formatProjectBoardPromptBlock,
} from "../projectBoardPrompt.ts";
import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "../workspaceScopePrompt.ts";
import {
ProviderCommandReactor,
type ProviderCommandReactorShape,
Expand DownExpand Up@@ -863,6 +867,12 @@ const make = Effect.gen(function* () {
if (!rulesAlreadySent) threadsWithBoardRulesSent.add(input.threadId);
turnInput = appendProjectBoardToTurnInput(turnInput, boardBlock);
}
if (turnInput && thread.worktreePath) {
turnInput = prependWorkspaceScopeToTurnInput(
turnInput,
formatWorkspaceScopePromptBlock({ cwd: thread.worktreePath, branch: thread.branch }),
);
}

return {
threadId: input.threadId,
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ import {
type ProviderRuntimeIngestionShape,
} from "../Services/ProviderRuntimeIngestion.ts";
import { projectActivityPayload } from "../ActivityPayloadProjection.ts";
import { maybeWorkspaceScopeWarningActivity } from "../workspaceScopeWarning.ts";
import { forkParked } from "../../serverActivation.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { canReplaceThreadTitle } from "../threadTitles.ts";
Expand DownExpand Up@@ -900,6 +901,7 @@ const make = Effect.gen(function* () {
crypto.randomUUIDv4.pipe(
Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)),
);
const warnedWorkspaceScopeKeys = new Set<string>();

const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({
capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY,
Expand DownExpand Up@@ -2041,6 +2043,21 @@ const make = Effect.gen(function* () {
),
),
).pipe(Effect.asVoid);
const workspaceScopeWarning = maybeWorkspaceScopeWarningActivity({
event,
worktreePath: thread.worktreePath,
warnedKeys: warnedWorkspaceScopeKeys,
});
if (workspaceScopeWarning) {
const commandId = yield* providerCommandId(event, "thread-activity-append-workspace-scope");
yield* orchestrationEngine.dispatch({
type: "thread.activity.append",
commandId,
threadId: thread.id,
activity: workspaceScopeWarning,
createdAt: workspaceScopeWarning.createdAt,
});
}
});

const processDomainEvent = (_event: TurnStartRequestedDomainEvent) => Effect.void;
Expand Down
28 changes: 28 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vite-plus/test";

import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "./workspaceScopePrompt.ts";

describe("workspaceScopePrompt", () => {
it("names the assigned worktree path and branch", () => {
const block = formatWorkspaceScopePromptBlock({
cwd: "/tmp/worktree",
branch: "t3code/update-main-from-github",
});
expect(block).toContain("<t3_workspace_scope>");
expect(block).toContain("Path: /tmp/worktree");
expect(block).toContain("Branch: t3code/update-main-from-github");
expect(block).toContain("Do not switch to another clone");
});

it("prepends the block so the constraint is visible before the user prompt", () => {
expect(
prependWorkspaceScopeToTurnInput(
"fix the bug",
"<t3_workspace_scope>\nscoped\n</t3_workspace_scope>",
),
).toBe("<t3_workspace_scope>\nscoped\n</t3_workspace_scope>\n\nfix the bug");
});
});
24 changes: 24 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
export function formatWorkspaceScopePromptBlock(input: {
readonly cwd: string;
readonly branch: string | null;
}): string {
const branchLine = input.branch ? `Branch: ${input.branch}` : "Branch: (unknown)";
return [
"<t3_workspace_scope>",
"This thread is attached to a Git worktree. All file edits, commits, git commands, and dev servers must use this directory as the working directory. Do not switch to another clone of the same repository.",
`Path: ${input.cwd}`,
branchLine,
"If you need a different path, stop and ask the user to confirm that exact path first.",
"</t3_workspace_scope>",
].join("\n");
}

export function prependWorkspaceScopeToTurnInput(
input: string | undefined,
block: string | null,
): string | undefined {
if (!block) return input;
const trimmed = input?.trim();
if (!trimmed) return block;
return `${block}\n\n${trimmed}`;
}
86 changes: 86 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
import {
EventId,
ProviderDriverKind,
ThreadId,
TurnId,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { maybeWorkspaceScopeWarningActivity } from "./workspaceScopeWarning.ts";

const worktree = "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c";
const otherClone = "/Users/me/Documents/Project/marswalk";

const base = {
provider: ProviderDriverKind.make("cursor"),
createdAt: "2026-08-31T12:00:00.000Z",
threadId: ThreadId.make("thread-1"),
turnId: TurnId.make("turn-1"),
};

function toolEvent(eventId: string, data: Record<string, unknown>): ProviderRuntimeEvent {
return {
...base,
type: "item.completed",
eventId: EventId.make(eventId),
payload: {
itemType: "command_execution",
title: "Terminal",
data,
},
} satisfies ProviderRuntimeEvent;
}

describe("workspaceScopeWarning", () => {
it("warns when a command's working directory is another clone", () => {
const warnedKeys = new Set<string>();
const activity = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", {
command: "git status",
rawInput: { working_directory: otherClone },
}),
worktreePath: worktree,
warnedKeys,
});
expect(activity?.kind).toBe("runtime.warning");
expect(activity?.summary).toContain("different checkout");
const payload = activity?.payload as { detail?: string };
expect(payload.detail).toContain(otherClone);
expect(payload.detail).toContain(worktree);
expect(payload.detail).toContain("Provider: cursor");
});

it("warns only once per escaped path on a thread", () => {
const warnedKeys = new Set<string>();
const first = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
const second = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-2", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
expect(first).not.toBeNull();
expect(second).toBeNull();
});

it("stays quiet when the thread has no worktree or the command stays in it", () => {
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: otherClone } }),
worktreePath: null,
warnedKeys: new Set(),
}),
).toBeNull();
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: worktree } }),
worktreePath: worktree,
warnedKeys: new Set(),
}),
).toBeNull();
});
});
73 changes: 73 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import {
EventId,
type OrchestrationThreadActivity,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { findOutOfWorkspaceScopePath } from "@t3tools/shared/workspaceScope";

const WARNABLE_EVENT_TYPES = new Set([
"item.started",
"item.updated",
"item.completed",
"request.opened",
]);

const FILE_PATH_ITEM_TYPES = new Set([
"command_execution",
"file_change",
"file_edit",
"create_file",
"edit",
]);

function asRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}

function payloadLooksLikeFileMutation(payload: Record<string, unknown>): boolean {
const itemType = typeof payload.itemType === "string" ? payload.itemType : undefined;
if (itemType && FILE_PATH_ITEM_TYPES.has(itemType)) {
return true;
}
const requestType = typeof payload.requestType === "string" ? payload.requestType : undefined;
return requestType === "command" || requestType === "file-change";
}

export function maybeWorkspaceScopeWarningActivity(input: {
readonly event: ProviderRuntimeEvent;
readonly worktreePath: string | null;
readonly warnedKeys: Set<string>;
}): OrchestrationThreadActivity | null {
const worktreePath = input.worktreePath?.trim() ?? "";
if (!worktreePath || !WARNABLE_EVENT_TYPES.has(input.event.type)) {
return null;
}
const payload = asRecord(input.event.payload) ?? {};
const outOfScopePath = findOutOfWorkspaceScopePath({
workspaceRoot: worktreePath,
data: payload,
includeFilePaths: payloadLooksLikeFileMutation(payload),
});
if (!outOfScopePath) {
return null;
}
const warningKey = `${input.event.threadId}:${outOfScopePath}`;
if (input.warnedKeys.has(warningKey)) {
return null;
}
input.warnedKeys.add(warningKey);
return {
id: EventId.make(`workspace-scope:${input.event.eventId}`),
createdAt: input.event.createdAt,
tone: "info",
kind: "runtime.warning",
summary: "Agent used a different checkout than this thread's worktree",
payload: {
message: "Agent used a different checkout than this thread's worktree",
detail: `Thread worktree: ${worktreePath}\nUsed path: ${outOfScopePath}\nProvider: ${input.event.provider}`,
},
turnId: input.event.turnId ?? null,
};
}
14 changes: 14 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,10 @@ import { cn } from "~/lib/utils";
import { useUiStateStore } from "~/uiStateStore";
import { type TimestampFormat } from "@t3tools/contracts/settings";
import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat";
import {
parseWorkspaceScopeWarning,
WorkspaceScopeWarningRow,
} from "../../workspaceScopeWarningUi";

import {
buildInlineTerminalContextText,
Expand DownExpand Up@@ -2718,7 +2722,17 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
isExpandedToolGroupEntry: boolean;
}) {
const { workEntry, workspaceRoot, isExpandedToolGroupEntry } = props;
const workspaceScopeMismatch = parseWorkspaceScopeWarning(workEntry);
const [expanded, setExpanded] = useState(false);
if (workspaceScopeMismatch) {
return (
<WorkspaceScopeWarningRow
label={workEntry.label}
mismatch={workspaceScopeMismatch}
isExpandedToolGroupEntry={isExpandedToolGroupEntry}
/>
);
}
const iconConfig = workToneIcon(workEntry.tone);
const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning";
const showFailedIndicator = workEntryDisplayIndicatesToolFailure(workEntry);
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/workspaceScopeWarningUi.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
import { ProviderDriverKind } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { parseWorkspaceScopeWarning } from "./workspaceScopeWarningUi";

describe("parseWorkspaceScopeWarning", () => {
it("reads aligned thread and agent paths from the warning detail", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk\nProvider: cursor",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: ProviderDriverKind.make("cursor"),
});
});

it("ignores other runtime warnings", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail: "Model rerouted to a fallback",
}),
).toBeNull();
});

it("keeps older warnings that have no provider line", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: null,
});
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
10 changes: 10 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,10 @@ import {
appendProjectBoardToTurnInput,
formatProjectBoardPromptBlock,
} from "../projectBoardPrompt.ts";
import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "../workspaceScopePrompt.ts";
import {
ProviderCommandReactor,
type ProviderCommandReactorShape,
Expand DownExpand Up@@ -863,6 +867,12 @@ const make = Effect.gen(function* () {
if (!rulesAlreadySent) threadsWithBoardRulesSent.add(input.threadId);
turnInput = appendProjectBoardToTurnInput(turnInput, boardBlock);
}
if (turnInput && thread.worktreePath) {
turnInput = prependWorkspaceScopeToTurnInput(
turnInput,
formatWorkspaceScopePromptBlock({ cwd: thread.worktreePath, branch: thread.branch }),
);
}

return {
threadId: input.threadId,
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ import {
type ProviderRuntimeIngestionShape,
} from "../Services/ProviderRuntimeIngestion.ts";
import { projectActivityPayload } from "../ActivityPayloadProjection.ts";
import { maybeWorkspaceScopeWarningActivity } from "../workspaceScopeWarning.ts";
import { forkParked } from "../../serverActivation.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { canReplaceThreadTitle } from "../threadTitles.ts";
Expand DownExpand Up@@ -900,6 +901,7 @@ const make = Effect.gen(function* () {
crypto.randomUUIDv4.pipe(
Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)),
);
const warnedWorkspaceScopeKeys = new Set<string>();

const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({
capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY,
Expand DownExpand Up@@ -2041,6 +2043,21 @@ const make = Effect.gen(function* () {
),
),
).pipe(Effect.asVoid);
const workspaceScopeWarning = maybeWorkspaceScopeWarningActivity({
event,
worktreePath: thread.worktreePath,
warnedKeys: warnedWorkspaceScopeKeys,
});
if (workspaceScopeWarning) {
const commandId = yield* providerCommandId(event, "thread-activity-append-workspace-scope");
yield* orchestrationEngine.dispatch({
type: "thread.activity.append",
commandId,
threadId: thread.id,
activity: workspaceScopeWarning,
createdAt: workspaceScopeWarning.createdAt,
});
}
});

const processDomainEvent = (_event: TurnStartRequestedDomainEvent) => Effect.void;
Expand Down
28 changes: 28 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vite-plus/test";

import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "./workspaceScopePrompt.ts";

describe("workspaceScopePrompt", () => {
it("names the assigned worktree path and branch", () => {
const block = formatWorkspaceScopePromptBlock({
cwd: "/tmp/worktree",
branch: "t3code/update-main-from-github",
});
expect(block).toContain("<t3_workspace_scope>");
expect(block).toContain("Path: /tmp/worktree");
expect(block).toContain("Branch: t3code/update-main-from-github");
expect(block).toContain("Do not switch to another clone");
});

it("prepends the block so the constraint is visible before the user prompt", () => {
expect(
prependWorkspaceScopeToTurnInput(
"fix the bug",
"<t3_workspace_scope>\nscoped\n</t3_workspace_scope>",
),
).toBe("<t3_workspace_scope>\nscoped\n</t3_workspace_scope>\n\nfix the bug");
});
});
24 changes: 24 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
export function formatWorkspaceScopePromptBlock(input: {
readonly cwd: string;
readonly branch: string | null;
}): string {
const branchLine = input.branch ? `Branch: ${input.branch}` : "Branch: (unknown)";
return [
"<t3_workspace_scope>",
"This thread is attached to a Git worktree. All file edits, commits, git commands, and dev servers must use this directory as the working directory. Do not switch to another clone of the same repository.",
`Path: ${input.cwd}`,
branchLine,
"If you need a different path, stop and ask the user to confirm that exact path first.",
"</t3_workspace_scope>",
].join("\n");
}

export function prependWorkspaceScopeToTurnInput(
input: string | undefined,
block: string | null,
): string | undefined {
if (!block) return input;
const trimmed = input?.trim();
if (!trimmed) return block;
return `${block}\n\n${trimmed}`;
}
86 changes: 86 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
import {
EventId,
ProviderDriverKind,
ThreadId,
TurnId,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { maybeWorkspaceScopeWarningActivity } from "./workspaceScopeWarning.ts";

const worktree = "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c";
const otherClone = "/Users/me/Documents/Project/marswalk";

const base = {
provider: ProviderDriverKind.make("cursor"),
createdAt: "2026-08-31T12:00:00.000Z",
threadId: ThreadId.make("thread-1"),
turnId: TurnId.make("turn-1"),
};

function toolEvent(eventId: string, data: Record<string, unknown>): ProviderRuntimeEvent {
return {
...base,
type: "item.completed",
eventId: EventId.make(eventId),
payload: {
itemType: "command_execution",
title: "Terminal",
data,
},
} satisfies ProviderRuntimeEvent;
}

describe("workspaceScopeWarning", () => {
it("warns when a command's working directory is another clone", () => {
const warnedKeys = new Set<string>();
const activity = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", {
command: "git status",
rawInput: { working_directory: otherClone },
}),
worktreePath: worktree,
warnedKeys,
});
expect(activity?.kind).toBe("runtime.warning");
expect(activity?.summary).toContain("different checkout");
const payload = activity?.payload as { detail?: string };
expect(payload.detail).toContain(otherClone);
expect(payload.detail).toContain(worktree);
expect(payload.detail).toContain("Provider: cursor");
});

it("warns only once per escaped path on a thread", () => {
const warnedKeys = new Set<string>();
const first = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
const second = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-2", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
expect(first).not.toBeNull();
expect(second).toBeNull();
});

it("stays quiet when the thread has no worktree or the command stays in it", () => {
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: otherClone } }),
worktreePath: null,
warnedKeys: new Set(),
}),
).toBeNull();
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: worktree } }),
worktreePath: worktree,
warnedKeys: new Set(),
}),
).toBeNull();
});
});
73 changes: 73 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import {
EventId,
type OrchestrationThreadActivity,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { findOutOfWorkspaceScopePath } from "@t3tools/shared/workspaceScope";

const WARNABLE_EVENT_TYPES = new Set([
"item.started",
"item.updated",
"item.completed",
"request.opened",
]);

const FILE_PATH_ITEM_TYPES = new Set([
"command_execution",
"file_change",
"file_edit",
"create_file",
"edit",
]);

function asRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}

function payloadLooksLikeFileMutation(payload: Record<string, unknown>): boolean {
const itemType = typeof payload.itemType === "string" ? payload.itemType : undefined;
if (itemType && FILE_PATH_ITEM_TYPES.has(itemType)) {
return true;
}
const requestType = typeof payload.requestType === "string" ? payload.requestType : undefined;
return requestType === "command" || requestType === "file-change";
}

export function maybeWorkspaceScopeWarningActivity(input: {
readonly event: ProviderRuntimeEvent;
readonly worktreePath: string | null;
readonly warnedKeys: Set<string>;
}): OrchestrationThreadActivity | null {
const worktreePath = input.worktreePath?.trim() ?? "";
if (!worktreePath || !WARNABLE_EVENT_TYPES.has(input.event.type)) {
return null;
}
const payload = asRecord(input.event.payload) ?? {};
const outOfScopePath = findOutOfWorkspaceScopePath({
workspaceRoot: worktreePath,
data: payload,
includeFilePaths: payloadLooksLikeFileMutation(payload),
});
if (!outOfScopePath) {
return null;
}
const warningKey = `${input.event.threadId}:${outOfScopePath}`;
if (input.warnedKeys.has(warningKey)) {
return null;
}
input.warnedKeys.add(warningKey);
return {
id: EventId.make(`workspace-scope:${input.event.eventId}`),
createdAt: input.event.createdAt,
tone: "info",
kind: "runtime.warning",
summary: "Agent used a different checkout than this thread's worktree",
payload: {
message: "Agent used a different checkout than this thread's worktree",
detail: `Thread worktree: ${worktreePath}\nUsed path: ${outOfScopePath}\nProvider: ${input.event.provider}`,
},
turnId: input.event.turnId ?? null,
};
}
14 changes: 14 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,10 @@ import { cn } from "~/lib/utils";
import { useUiStateStore } from "~/uiStateStore";
import { type TimestampFormat } from "@t3tools/contracts/settings";
import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat";
import {
parseWorkspaceScopeWarning,
WorkspaceScopeWarningRow,
} from "../../workspaceScopeWarningUi";

import {
buildInlineTerminalContextText,
Expand DownExpand Up@@ -2718,7 +2722,17 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
isExpandedToolGroupEntry: boolean;
}) {
const { workEntry, workspaceRoot, isExpandedToolGroupEntry } = props;
const workspaceScopeMismatch = parseWorkspaceScopeWarning(workEntry);
const [expanded, setExpanded] = useState(false);
if (workspaceScopeMismatch) {
return (
<WorkspaceScopeWarningRow
label={workEntry.label}
mismatch={workspaceScopeMismatch}
isExpandedToolGroupEntry={isExpandedToolGroupEntry}
/>
);
}
const iconConfig = workToneIcon(workEntry.tone);
const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning";
const showFailedIndicator = workEntryDisplayIndicatesToolFailure(workEntry);
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/workspaceScopeWarningUi.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
import { ProviderDriverKind } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { parseWorkspaceScopeWarning } from "./workspaceScopeWarningUi";

describe("parseWorkspaceScopeWarning", () => {
it("reads aligned thread and agent paths from the warning detail", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk\nProvider: cursor",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: ProviderDriverKind.make("cursor"),
});
});

it("ignores other runtime warnings", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail: "Model rerouted to a fallback",
}),
).toBeNull();
});

it("keeps older warnings that have no provider line", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: null,
});
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
10 changes: 10 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,10 @@ import {
appendProjectBoardToTurnInput,
formatProjectBoardPromptBlock,
} from "../projectBoardPrompt.ts";
import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "../workspaceScopePrompt.ts";
import {
ProviderCommandReactor,
type ProviderCommandReactorShape,
Expand DownExpand Up@@ -863,6 +867,12 @@ const make = Effect.gen(function* () {
if (!rulesAlreadySent) threadsWithBoardRulesSent.add(input.threadId);
turnInput = appendProjectBoardToTurnInput(turnInput, boardBlock);
}
if (turnInput && thread.worktreePath) {
turnInput = prependWorkspaceScopeToTurnInput(
turnInput,
formatWorkspaceScopePromptBlock({ cwd: thread.worktreePath, branch: thread.branch }),
);
}

return {
threadId: input.threadId,
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ import {
type ProviderRuntimeIngestionShape,
} from "../Services/ProviderRuntimeIngestion.ts";
import { projectActivityPayload } from "../ActivityPayloadProjection.ts";
import { maybeWorkspaceScopeWarningActivity } from "../workspaceScopeWarning.ts";
import { forkParked } from "../../serverActivation.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { canReplaceThreadTitle } from "../threadTitles.ts";
Expand DownExpand Up@@ -900,6 +901,7 @@ const make = Effect.gen(function* () {
crypto.randomUUIDv4.pipe(
Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)),
);
const warnedWorkspaceScopeKeys = new Set<string>();

const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({
capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY,
Expand DownExpand Up@@ -2041,6 +2043,21 @@ const make = Effect.gen(function* () {
),
),
).pipe(Effect.asVoid);
const workspaceScopeWarning = maybeWorkspaceScopeWarningActivity({
event,
worktreePath: thread.worktreePath,
warnedKeys: warnedWorkspaceScopeKeys,
});
if (workspaceScopeWarning) {
const commandId = yield* providerCommandId(event, "thread-activity-append-workspace-scope");
yield* orchestrationEngine.dispatch({
type: "thread.activity.append",
commandId,
threadId: thread.id,
activity: workspaceScopeWarning,
createdAt: workspaceScopeWarning.createdAt,
});
}
});

const processDomainEvent = (_event: TurnStartRequestedDomainEvent) => Effect.void;
Expand Down
28 changes: 28 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vite-plus/test";

import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "./workspaceScopePrompt.ts";

describe("workspaceScopePrompt", () => {
it("names the assigned worktree path and branch", () => {
const block = formatWorkspaceScopePromptBlock({
cwd: "/tmp/worktree",
branch: "t3code/update-main-from-github",
});
expect(block).toContain("<t3_workspace_scope>");
expect(block).toContain("Path: /tmp/worktree");
expect(block).toContain("Branch: t3code/update-main-from-github");
expect(block).toContain("Do not switch to another clone");
});

it("prepends the block so the constraint is visible before the user prompt", () => {
expect(
prependWorkspaceScopeToTurnInput(
"fix the bug",
"<t3_workspace_scope>\nscoped\n</t3_workspace_scope>",
),
).toBe("<t3_workspace_scope>\nscoped\n</t3_workspace_scope>\n\nfix the bug");
});
});
24 changes: 24 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
export function formatWorkspaceScopePromptBlock(input: {
readonly cwd: string;
readonly branch: string | null;
}): string {
const branchLine = input.branch ? `Branch: ${input.branch}` : "Branch: (unknown)";
return [
"<t3_workspace_scope>",
"This thread is attached to a Git worktree. All file edits, commits, git commands, and dev servers must use this directory as the working directory. Do not switch to another clone of the same repository.",
`Path: ${input.cwd}`,
branchLine,
"If you need a different path, stop and ask the user to confirm that exact path first.",
"</t3_workspace_scope>",
].join("\n");
}

export function prependWorkspaceScopeToTurnInput(
input: string | undefined,
block: string | null,
): string | undefined {
if (!block) return input;
const trimmed = input?.trim();
if (!trimmed) return block;
return `${block}\n\n${trimmed}`;
}
86 changes: 86 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
import {
EventId,
ProviderDriverKind,
ThreadId,
TurnId,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { maybeWorkspaceScopeWarningActivity } from "./workspaceScopeWarning.ts";

const worktree = "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c";
const otherClone = "/Users/me/Documents/Project/marswalk";

const base = {
provider: ProviderDriverKind.make("cursor"),
createdAt: "2026-08-31T12:00:00.000Z",
threadId: ThreadId.make("thread-1"),
turnId: TurnId.make("turn-1"),
};

function toolEvent(eventId: string, data: Record<string, unknown>): ProviderRuntimeEvent {
return {
...base,
type: "item.completed",
eventId: EventId.make(eventId),
payload: {
itemType: "command_execution",
title: "Terminal",
data,
},
} satisfies ProviderRuntimeEvent;
}

describe("workspaceScopeWarning", () => {
it("warns when a command's working directory is another clone", () => {
const warnedKeys = new Set<string>();
const activity = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", {
command: "git status",
rawInput: { working_directory: otherClone },
}),
worktreePath: worktree,
warnedKeys,
});
expect(activity?.kind).toBe("runtime.warning");
expect(activity?.summary).toContain("different checkout");
const payload = activity?.payload as { detail?: string };
expect(payload.detail).toContain(otherClone);
expect(payload.detail).toContain(worktree);
expect(payload.detail).toContain("Provider: cursor");
});

it("warns only once per escaped path on a thread", () => {
const warnedKeys = new Set<string>();
const first = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
const second = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-2", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
expect(first).not.toBeNull();
expect(second).toBeNull();
});

it("stays quiet when the thread has no worktree or the command stays in it", () => {
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: otherClone } }),
worktreePath: null,
warnedKeys: new Set(),
}),
).toBeNull();
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: worktree } }),
worktreePath: worktree,
warnedKeys: new Set(),
}),
).toBeNull();
});
});
73 changes: 73 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import {
EventId,
type OrchestrationThreadActivity,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { findOutOfWorkspaceScopePath } from "@t3tools/shared/workspaceScope";

const WARNABLE_EVENT_TYPES = new Set([
"item.started",
"item.updated",
"item.completed",
"request.opened",
]);

const FILE_PATH_ITEM_TYPES = new Set([
"command_execution",
"file_change",
"file_edit",
"create_file",
"edit",
]);

function asRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}

function payloadLooksLikeFileMutation(payload: Record<string, unknown>): boolean {
const itemType = typeof payload.itemType === "string" ? payload.itemType : undefined;
if (itemType && FILE_PATH_ITEM_TYPES.has(itemType)) {
return true;
}
const requestType = typeof payload.requestType === "string" ? payload.requestType : undefined;
return requestType === "command" || requestType === "file-change";
}

export function maybeWorkspaceScopeWarningActivity(input: {
readonly event: ProviderRuntimeEvent;
readonly worktreePath: string | null;
readonly warnedKeys: Set<string>;
}): OrchestrationThreadActivity | null {
const worktreePath = input.worktreePath?.trim() ?? "";
if (!worktreePath || !WARNABLE_EVENT_TYPES.has(input.event.type)) {
return null;
}
const payload = asRecord(input.event.payload) ?? {};
const outOfScopePath = findOutOfWorkspaceScopePath({
workspaceRoot: worktreePath,
data: payload,
includeFilePaths: payloadLooksLikeFileMutation(payload),
});
if (!outOfScopePath) {
return null;
}
const warningKey = `${input.event.threadId}:${outOfScopePath}`;
if (input.warnedKeys.has(warningKey)) {
return null;
}
input.warnedKeys.add(warningKey);
return {
id: EventId.make(`workspace-scope:${input.event.eventId}`),
createdAt: input.event.createdAt,
tone: "info",
kind: "runtime.warning",
summary: "Agent used a different checkout than this thread's worktree",
payload: {
message: "Agent used a different checkout than this thread's worktree",
detail: `Thread worktree: ${worktreePath}\nUsed path: ${outOfScopePath}\nProvider: ${input.event.provider}`,
},
turnId: input.event.turnId ?? null,
};
}
14 changes: 14 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,10 @@ import { cn } from "~/lib/utils";
import { useUiStateStore } from "~/uiStateStore";
import { type TimestampFormat } from "@t3tools/contracts/settings";
import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat";
import {
parseWorkspaceScopeWarning,
WorkspaceScopeWarningRow,
} from "../../workspaceScopeWarningUi";

import {
buildInlineTerminalContextText,
Expand DownExpand Up@@ -2718,7 +2722,17 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
isExpandedToolGroupEntry: boolean;
}) {
const { workEntry, workspaceRoot, isExpandedToolGroupEntry } = props;
const workspaceScopeMismatch = parseWorkspaceScopeWarning(workEntry);
const [expanded, setExpanded] = useState(false);
if (workspaceScopeMismatch) {
return (
<WorkspaceScopeWarningRow
label={workEntry.label}
mismatch={workspaceScopeMismatch}
isExpandedToolGroupEntry={isExpandedToolGroupEntry}
/>
);
}
const iconConfig = workToneIcon(workEntry.tone);
const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning";
const showFailedIndicator = workEntryDisplayIndicatesToolFailure(workEntry);
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/workspaceScopeWarningUi.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
import { ProviderDriverKind } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { parseWorkspaceScopeWarning } from "./workspaceScopeWarningUi";

describe("parseWorkspaceScopeWarning", () => {
it("reads aligned thread and agent paths from the warning detail", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk\nProvider: cursor",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: ProviderDriverKind.make("cursor"),
});
});

it("ignores other runtime warnings", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail: "Model rerouted to a fallback",
}),
).toBeNull();
});

it("keeps older warnings that have no provider line", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: null,
});
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
10 changes: 10 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,10 @@ import {
appendProjectBoardToTurnInput,
formatProjectBoardPromptBlock,
} from "../projectBoardPrompt.ts";
import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "../workspaceScopePrompt.ts";
import {
ProviderCommandReactor,
type ProviderCommandReactorShape,
Expand DownExpand Up@@ -863,6 +867,12 @@ const make = Effect.gen(function* () {
if (!rulesAlreadySent) threadsWithBoardRulesSent.add(input.threadId);
turnInput = appendProjectBoardToTurnInput(turnInput, boardBlock);
}
if (turnInput && thread.worktreePath) {
turnInput = prependWorkspaceScopeToTurnInput(
turnInput,
formatWorkspaceScopePromptBlock({ cwd: thread.worktreePath, branch: thread.branch }),
);
}

return {
threadId: input.threadId,
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ import {
type ProviderRuntimeIngestionShape,
} from "../Services/ProviderRuntimeIngestion.ts";
import { projectActivityPayload } from "../ActivityPayloadProjection.ts";
import { maybeWorkspaceScopeWarningActivity } from "../workspaceScopeWarning.ts";
import { forkParked } from "../../serverActivation.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { canReplaceThreadTitle } from "../threadTitles.ts";
Expand DownExpand Up@@ -900,6 +901,7 @@ const make = Effect.gen(function* () {
crypto.randomUUIDv4.pipe(
Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)),
);
const warnedWorkspaceScopeKeys = new Set<string>();

const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({
capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY,
Expand DownExpand Up@@ -2041,6 +2043,21 @@ const make = Effect.gen(function* () {
),
),
).pipe(Effect.asVoid);
const workspaceScopeWarning = maybeWorkspaceScopeWarningActivity({
event,
worktreePath: thread.worktreePath,
warnedKeys: warnedWorkspaceScopeKeys,
});
if (workspaceScopeWarning) {
const commandId = yield* providerCommandId(event, "thread-activity-append-workspace-scope");
yield* orchestrationEngine.dispatch({
type: "thread.activity.append",
commandId,
threadId: thread.id,
activity: workspaceScopeWarning,
createdAt: workspaceScopeWarning.createdAt,
});
}
});

const processDomainEvent = (_event: TurnStartRequestedDomainEvent) => Effect.void;
Expand Down
28 changes: 28 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vite-plus/test";

import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "./workspaceScopePrompt.ts";

describe("workspaceScopePrompt", () => {
it("names the assigned worktree path and branch", () => {
const block = formatWorkspaceScopePromptBlock({
cwd: "/tmp/worktree",
branch: "t3code/update-main-from-github",
});
expect(block).toContain("<t3_workspace_scope>");
expect(block).toContain("Path: /tmp/worktree");
expect(block).toContain("Branch: t3code/update-main-from-github");
expect(block).toContain("Do not switch to another clone");
});

it("prepends the block so the constraint is visible before the user prompt", () => {
expect(
prependWorkspaceScopeToTurnInput(
"fix the bug",
"<t3_workspace_scope>\nscoped\n</t3_workspace_scope>",
),
).toBe("<t3_workspace_scope>\nscoped\n</t3_workspace_scope>\n\nfix the bug");
});
});
24 changes: 24 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
export function formatWorkspaceScopePromptBlock(input: {
readonly cwd: string;
readonly branch: string | null;
}): string {
const branchLine = input.branch ? `Branch: ${input.branch}` : "Branch: (unknown)";
return [
"<t3_workspace_scope>",
"This thread is attached to a Git worktree. All file edits, commits, git commands, and dev servers must use this directory as the working directory. Do not switch to another clone of the same repository.",
`Path: ${input.cwd}`,
branchLine,
"If you need a different path, stop and ask the user to confirm that exact path first.",
"</t3_workspace_scope>",
].join("\n");
}

export function prependWorkspaceScopeToTurnInput(
input: string | undefined,
block: string | null,
): string | undefined {
if (!block) return input;
const trimmed = input?.trim();
if (!trimmed) return block;
return `${block}\n\n${trimmed}`;
}
86 changes: 86 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
import {
EventId,
ProviderDriverKind,
ThreadId,
TurnId,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { maybeWorkspaceScopeWarningActivity } from "./workspaceScopeWarning.ts";

const worktree = "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c";
const otherClone = "/Users/me/Documents/Project/marswalk";

const base = {
provider: ProviderDriverKind.make("cursor"),
createdAt: "2026-08-31T12:00:00.000Z",
threadId: ThreadId.make("thread-1"),
turnId: TurnId.make("turn-1"),
};

function toolEvent(eventId: string, data: Record<string, unknown>): ProviderRuntimeEvent {
return {
...base,
type: "item.completed",
eventId: EventId.make(eventId),
payload: {
itemType: "command_execution",
title: "Terminal",
data,
},
} satisfies ProviderRuntimeEvent;
}

describe("workspaceScopeWarning", () => {
it("warns when a command's working directory is another clone", () => {
const warnedKeys = new Set<string>();
const activity = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", {
command: "git status",
rawInput: { working_directory: otherClone },
}),
worktreePath: worktree,
warnedKeys,
});
expect(activity?.kind).toBe("runtime.warning");
expect(activity?.summary).toContain("different checkout");
const payload = activity?.payload as { detail?: string };
expect(payload.detail).toContain(otherClone);
expect(payload.detail).toContain(worktree);
expect(payload.detail).toContain("Provider: cursor");
});

it("warns only once per escaped path on a thread", () => {
const warnedKeys = new Set<string>();
const first = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
const second = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-2", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
expect(first).not.toBeNull();
expect(second).toBeNull();
});

it("stays quiet when the thread has no worktree or the command stays in it", () => {
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: otherClone } }),
worktreePath: null,
warnedKeys: new Set(),
}),
).toBeNull();
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: worktree } }),
worktreePath: worktree,
warnedKeys: new Set(),
}),
).toBeNull();
});
});
73 changes: 73 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import {
EventId,
type OrchestrationThreadActivity,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { findOutOfWorkspaceScopePath } from "@t3tools/shared/workspaceScope";

const WARNABLE_EVENT_TYPES = new Set([
"item.started",
"item.updated",
"item.completed",
"request.opened",
]);

const FILE_PATH_ITEM_TYPES = new Set([
"command_execution",
"file_change",
"file_edit",
"create_file",
"edit",
]);

function asRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}

function payloadLooksLikeFileMutation(payload: Record<string, unknown>): boolean {
const itemType = typeof payload.itemType === "string" ? payload.itemType : undefined;
if (itemType && FILE_PATH_ITEM_TYPES.has(itemType)) {
return true;
}
const requestType = typeof payload.requestType === "string" ? payload.requestType : undefined;
return requestType === "command" || requestType === "file-change";
}

export function maybeWorkspaceScopeWarningActivity(input: {
readonly event: ProviderRuntimeEvent;
readonly worktreePath: string | null;
readonly warnedKeys: Set<string>;
}): OrchestrationThreadActivity | null {
const worktreePath = input.worktreePath?.trim() ?? "";
if (!worktreePath || !WARNABLE_EVENT_TYPES.has(input.event.type)) {
return null;
}
const payload = asRecord(input.event.payload) ?? {};
const outOfScopePath = findOutOfWorkspaceScopePath({
workspaceRoot: worktreePath,
data: payload,
includeFilePaths: payloadLooksLikeFileMutation(payload),
});
if (!outOfScopePath) {
return null;
}
const warningKey = `${input.event.threadId}:${outOfScopePath}`;
if (input.warnedKeys.has(warningKey)) {
return null;
}
input.warnedKeys.add(warningKey);
return {
id: EventId.make(`workspace-scope:${input.event.eventId}`),
createdAt: input.event.createdAt,
tone: "info",
kind: "runtime.warning",
summary: "Agent used a different checkout than this thread's worktree",
payload: {
message: "Agent used a different checkout than this thread's worktree",
detail: `Thread worktree: ${worktreePath}\nUsed path: ${outOfScopePath}\nProvider: ${input.event.provider}`,
},
turnId: input.event.turnId ?? null,
};
}
14 changes: 14 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,10 @@ import { cn } from "~/lib/utils";
import { useUiStateStore } from "~/uiStateStore";
import { type TimestampFormat } from "@t3tools/contracts/settings";
import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat";
import {
parseWorkspaceScopeWarning,
WorkspaceScopeWarningRow,
} from "../../workspaceScopeWarningUi";

import {
buildInlineTerminalContextText,
Expand DownExpand Up@@ -2718,7 +2722,17 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
isExpandedToolGroupEntry: boolean;
}) {
const { workEntry, workspaceRoot, isExpandedToolGroupEntry } = props;
const workspaceScopeMismatch = parseWorkspaceScopeWarning(workEntry);
const [expanded, setExpanded] = useState(false);
if (workspaceScopeMismatch) {
return (
<WorkspaceScopeWarningRow
label={workEntry.label}
mismatch={workspaceScopeMismatch}
isExpandedToolGroupEntry={isExpandedToolGroupEntry}
/>
);
}
const iconConfig = workToneIcon(workEntry.tone);
const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning";
const showFailedIndicator = workEntryDisplayIndicatesToolFailure(workEntry);
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/workspaceScopeWarningUi.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
import { ProviderDriverKind } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { parseWorkspaceScopeWarning } from "./workspaceScopeWarningUi";

describe("parseWorkspaceScopeWarning", () => {
it("reads aligned thread and agent paths from the warning detail", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk\nProvider: cursor",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: ProviderDriverKind.make("cursor"),
});
});

it("ignores other runtime warnings", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail: "Model rerouted to a fallback",
}),
).toBeNull();
});

it("keeps older warnings that have no provider line", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: null,
});
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
10 changes: 10 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,10 @@ import {
appendProjectBoardToTurnInput,
formatProjectBoardPromptBlock,
} from "../projectBoardPrompt.ts";
import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "../workspaceScopePrompt.ts";
import {
ProviderCommandReactor,
type ProviderCommandReactorShape,
Expand DownExpand Up@@ -863,6 +867,12 @@ const make = Effect.gen(function* () {
if (!rulesAlreadySent) threadsWithBoardRulesSent.add(input.threadId);
turnInput = appendProjectBoardToTurnInput(turnInput, boardBlock);
}
if (turnInput && thread.worktreePath) {
turnInput = prependWorkspaceScopeToTurnInput(
turnInput,
formatWorkspaceScopePromptBlock({ cwd: thread.worktreePath, branch: thread.branch }),
);
}

return {
threadId: input.threadId,
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@ import {
type ProviderRuntimeIngestionShape,
} from "../Services/ProviderRuntimeIngestion.ts";
import { projectActivityPayload } from "../ActivityPayloadProjection.ts";
import { maybeWorkspaceScopeWarningActivity } from "../workspaceScopeWarning.ts";
import { forkParked } from "../../serverActivation.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { canReplaceThreadTitle } from "../threadTitles.ts";
Expand DownExpand Up@@ -900,6 +901,7 @@ const make = Effect.gen(function* () {
crypto.randomUUIDv4.pipe(
Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)),
);
const warnedWorkspaceScopeKeys = new Set<string>();

const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({
capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY,
Expand DownExpand Up@@ -2041,6 +2043,21 @@ const make = Effect.gen(function* () {
),
),
).pipe(Effect.asVoid);
const workspaceScopeWarning = maybeWorkspaceScopeWarningActivity({
event,
worktreePath: thread.worktreePath,
warnedKeys: warnedWorkspaceScopeKeys,
});
if (workspaceScopeWarning) {
const commandId = yield* providerCommandId(event, "thread-activity-append-workspace-scope");
yield* orchestrationEngine.dispatch({
type: "thread.activity.append",
commandId,
threadId: thread.id,
activity: workspaceScopeWarning,
createdAt: workspaceScopeWarning.createdAt,
});
}
});

const processDomainEvent = (_event: TurnStartRequestedDomainEvent) => Effect.void;
Expand Down
28 changes: 28 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vite-plus/test";

import {
formatWorkspaceScopePromptBlock,
prependWorkspaceScopeToTurnInput,
} from "./workspaceScopePrompt.ts";

describe("workspaceScopePrompt", () => {
it("names the assigned worktree path and branch", () => {
const block = formatWorkspaceScopePromptBlock({
cwd: "/tmp/worktree",
branch: "t3code/update-main-from-github",
});
expect(block).toContain("<t3_workspace_scope>");
expect(block).toContain("Path: /tmp/worktree");
expect(block).toContain("Branch: t3code/update-main-from-github");
expect(block).toContain("Do not switch to another clone");
});

it("prepends the block so the constraint is visible before the user prompt", () => {
expect(
prependWorkspaceScopeToTurnInput(
"fix the bug",
"<t3_workspace_scope>\nscoped\n</t3_workspace_scope>",
),
).toBe("<t3_workspace_scope>\nscoped\n</t3_workspace_scope>\n\nfix the bug");
});
});
24 changes: 24 additions & 0 deletions apps/server/src/orchestration/workspaceScopePrompt.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
export function formatWorkspaceScopePromptBlock(input: {
readonly cwd: string;
readonly branch: string | null;
}): string {
const branchLine = input.branch ? `Branch: ${input.branch}` : "Branch: (unknown)";
return [
"<t3_workspace_scope>",
"This thread is attached to a Git worktree. All file edits, commits, git commands, and dev servers must use this directory as the working directory. Do not switch to another clone of the same repository.",
`Path: ${input.cwd}`,
branchLine,
"If you need a different path, stop and ask the user to confirm that exact path first.",
"</t3_workspace_scope>",
].join("\n");
}

export function prependWorkspaceScopeToTurnInput(
input: string | undefined,
block: string | null,
): string | undefined {
if (!block) return input;
const trimmed = input?.trim();
if (!trimmed) return block;
return `${block}\n\n${trimmed}`;
}
86 changes: 86 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
import {
EventId,
ProviderDriverKind,
ThreadId,
TurnId,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { maybeWorkspaceScopeWarningActivity } from "./workspaceScopeWarning.ts";

const worktree = "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c";
const otherClone = "/Users/me/Documents/Project/marswalk";

const base = {
provider: ProviderDriverKind.make("cursor"),
createdAt: "2026-08-31T12:00:00.000Z",
threadId: ThreadId.make("thread-1"),
turnId: TurnId.make("turn-1"),
};

function toolEvent(eventId: string, data: Record<string, unknown>): ProviderRuntimeEvent {
return {
...base,
type: "item.completed",
eventId: EventId.make(eventId),
payload: {
itemType: "command_execution",
title: "Terminal",
data,
},
} satisfies ProviderRuntimeEvent;
}

describe("workspaceScopeWarning", () => {
it("warns when a command's working directory is another clone", () => {
const warnedKeys = new Set<string>();
const activity = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", {
command: "git status",
rawInput: { working_directory: otherClone },
}),
worktreePath: worktree,
warnedKeys,
});
expect(activity?.kind).toBe("runtime.warning");
expect(activity?.summary).toContain("different checkout");
const payload = activity?.payload as { detail?: string };
expect(payload.detail).toContain(otherClone);
expect(payload.detail).toContain(worktree);
expect(payload.detail).toContain("Provider: cursor");
});

it("warns only once per escaped path on a thread", () => {
const warnedKeys = new Set<string>();
const first = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
const second = maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-2", { rawInput: { cwd: otherClone } }),
worktreePath: worktree,
warnedKeys,
});
expect(first).not.toBeNull();
expect(second).toBeNull();
});

it("stays quiet when the thread has no worktree or the command stays in it", () => {
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: otherClone } }),
worktreePath: null,
warnedKeys: new Set(),
}),
).toBeNull();
expect(
maybeWorkspaceScopeWarningActivity({
event: toolEvent("event-1", { rawInput: { working_directory: worktree } }),
worktreePath: worktree,
warnedKeys: new Set(),
}),
).toBeNull();
});
});
73 changes: 73 additions & 0 deletions apps/server/src/orchestration/workspaceScopeWarning.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import {
EventId,
type OrchestrationThreadActivity,
type ProviderRuntimeEvent,
} from "@t3tools/contracts";
import { findOutOfWorkspaceScopePath } from "@t3tools/shared/workspaceScope";

const WARNABLE_EVENT_TYPES = new Set([
"item.started",
"item.updated",
"item.completed",
"request.opened",
]);

const FILE_PATH_ITEM_TYPES = new Set([
"command_execution",
"file_change",
"file_edit",
"create_file",
"edit",
]);

function asRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}

function payloadLooksLikeFileMutation(payload: Record<string, unknown>): boolean {
const itemType = typeof payload.itemType === "string" ? payload.itemType : undefined;
if (itemType && FILE_PATH_ITEM_TYPES.has(itemType)) {
return true;
}
const requestType = typeof payload.requestType === "string" ? payload.requestType : undefined;
return requestType === "command" || requestType === "file-change";
}

export function maybeWorkspaceScopeWarningActivity(input: {
readonly event: ProviderRuntimeEvent;
readonly worktreePath: string | null;
readonly warnedKeys: Set<string>;
}): OrchestrationThreadActivity | null {
const worktreePath = input.worktreePath?.trim() ?? "";
if (!worktreePath || !WARNABLE_EVENT_TYPES.has(input.event.type)) {
return null;
}
const payload = asRecord(input.event.payload) ?? {};
const outOfScopePath = findOutOfWorkspaceScopePath({
workspaceRoot: worktreePath,
data: payload,
includeFilePaths: payloadLooksLikeFileMutation(payload),
});
if (!outOfScopePath) {
return null;
}
const warningKey = `${input.event.threadId}:${outOfScopePath}`;
if (input.warnedKeys.has(warningKey)) {
return null;
}
input.warnedKeys.add(warningKey);
return {
id: EventId.make(`workspace-scope:${input.event.eventId}`),
createdAt: input.event.createdAt,
tone: "info",
kind: "runtime.warning",
summary: "Agent used a different checkout than this thread's worktree",
payload: {
message: "Agent used a different checkout than this thread's worktree",
detail: `Thread worktree: ${worktreePath}\nUsed path: ${outOfScopePath}\nProvider: ${input.event.provider}`,
},
turnId: input.event.turnId ?? null,
};
}
14 changes: 14 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,10 @@ import { cn } from "~/lib/utils";
import { useUiStateStore } from "~/uiStateStore";
import { type TimestampFormat } from "@t3tools/contracts/settings";
import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat";
import {
parseWorkspaceScopeWarning,
WorkspaceScopeWarningRow,
} from "../../workspaceScopeWarningUi";

import {
buildInlineTerminalContextText,
Expand DownExpand Up@@ -2718,7 +2722,17 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
isExpandedToolGroupEntry: boolean;
}) {
const { workEntry, workspaceRoot, isExpandedToolGroupEntry } = props;
const workspaceScopeMismatch = parseWorkspaceScopeWarning(workEntry);
const [expanded, setExpanded] = useState(false);
if (workspaceScopeMismatch) {
return (
<WorkspaceScopeWarningRow
label={workEntry.label}
mismatch={workspaceScopeMismatch}
isExpandedToolGroupEntry={isExpandedToolGroupEntry}
/>
);
}
const iconConfig = workToneIcon(workEntry.tone);
const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning";
const showFailedIndicator = workEntryDisplayIndicatesToolFailure(workEntry);
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/workspaceScopeWarningUi.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
import { ProviderDriverKind } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { parseWorkspaceScopeWarning } from "./workspaceScopeWarningUi";

describe("parseWorkspaceScopeWarning", () => {
it("reads aligned thread and agent paths from the warning detail", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk\nProvider: cursor",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: ProviderDriverKind.make("cursor"),
});
});

it("ignores other runtime warnings", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail: "Model rerouted to a fallback",
}),
).toBeNull();
});

it("keeps older warnings that have no provider line", () => {
expect(
parseWorkspaceScopeWarning({
sourceActivityKind: "runtime.warning",
detail:
"Thread worktree: /Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c\nUsed path: /Users/me/Documents/Project/marswalk",
}),
).toEqual({
threadWorktree: "/Users/me/.t3-jcode/worktrees/marswalk/t3code-dae6736c",
usedPath: "/Users/me/Documents/Project/marswalk",
provider: null,
});
});
});
Loading
Loading