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
25 changes: 24 additions & 1 deletion apps/coder-gateway/src/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,19 @@ export function makeCoderHub(options: CoderHubOptions): CoderHub {
(entry) => entry.from === requester.workspaceId && entry.to === targetWorkspaceId,
);
let outcome: CallOutcome | undefined;
if (targetWorkspaceId === undefined || rule === undefined) {
if (
targetWorkspaceId === undefined &&
(config.workspaceLinks ?? []).some(
(entry) => entry.from === requester.workspaceId && !environmentByWorkspace.has(entry.to),
)
) {
// A linked workspace that has not connected since the gateway started has no known
// environment id yet; it may be the target, so the caller retries instead of failing.
outcome = failed(
"orchestration_error",
"That workspace has not connected to T3 Coder yet. Retry once it is connected.",
);
} else if (targetWorkspaceId === undefined || rule === undefined) {
outcome = failed(
"capability_denied",
"This workspace is not linked to that one. Add a link in T3 Coder's workspace Links settings.",
Expand Down Expand Up @@ -228,6 +240,17 @@ export function makeCoderHub(options: CoderHubOptions): CoderHub {
try {
request = decodeRequest(value);
} catch {
// Answer what can be answered, so the caller fails now rather than at its timeout.
if (isRecord(value) && typeof value.id === "string") {
reply(entry, {
_tag: "Call",
id: value.id,
outcome: failed(
"orchestration_error",
"T3 Coder's gateway could not read this request. Reconnect the workspace so its helper matches the gateway.",
),
});
}
continue;
}
if (request._tag === "ListLinks") {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/coderHub.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ it.effect("refuses an invoked tool while Linked environments is off here", () =>
bound: true,
});
assert.strictEqual(outcome._tag, "Failure");
if (outcome._tag === "Failure") assert.strictEqual(outcome.failure.code, "capability_denied");
if (outcome._tag === "Failure") assert.strictEqual(outcome.failure.code, "orchestration_error");
assert.strictEqual(scopes.length, 0);
}),
);
Expand Down
6 changes: 4 additions & 2 deletions apps/server/src/coderHub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,10 +232,12 @@ export const makeHandlers = Effect.gen(function* () {
if (!enabled) {
return {
_tag: "Failure",
// As upstream's PeerMcpClient: not final, so a task already running here resumes once
// the beta is on here again.
failure: new OrchestratorMcpFailure({
code: "capability_denied",
code: "orchestration_error",
message:
"Linked environments is off in that workspace. Turn it on in its Settings → Labs.",
"The linked workspace has Linked environments turned off. Turn it on in its Settings → Labs.",
}),
} satisfies CoderHubCallOutcome;
}
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2076,6 +2076,7 @@ const make = Effect.gen(function* () {
Effect.orElseSucceed(() => "timed_out" as const),
);
yield* Effect.annotateCurrentSpan({ "secret_request.status": status });
// Coder: no orchestration metrics (no secretRequestsTotal counter).
if (status !== "saved") return { status };
if (input.delivery === "file") {
const file = yield* secretRequests
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/ContextChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const contextChipVariants = cva(
"pr-closed": "[--context-chip-accent:var(--context-chip-pr-closed)]",
skill: "[--context-chip-accent:var(--context-chip-skill)]",
thread: "[--context-chip-accent:var(--context-chip-thread)]",
// Coder: no machine (environment) chips; the @-machine picker is not carried.
citation: "[--context-chip-accent:var(--context-chip-citation)]",
"doc-pdf": "[--context-chip-accent:var(--context-chip-doc-pdf)]",
"doc-word": "[--context-chip-accent:var(--context-chip-doc-word)]",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2660,6 +2660,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)

useThreadContextDrop(composerFormRef, environmentId, attachmentTargetKey, (refs, isActive) => {
const records = refs.flatMap((ref) => {
// Coder: a thread from another environment is imported, as before #26201's linked machines.
if (ref.environmentId !== environmentId) {
void importDroppedThread(
ref,
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/chat/ComposerCommandMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type ComposerCommandItem =
label: string;
description: string;
};
// Coder: no machine (environment) chips; the @-machine picker is not carried.

export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {
listId: string;
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/chat/composerContextUndo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface RetainedContextPayloads extends RetainedAttachmentContextPayloa
terminals: Map<string, TerminalContextDraft>;
reviewComments: Map<string, ReviewCommentContext>;
threads: Map<string, ThreadContextRecord>;
// Coder: no machine (environment) chips; the @-machine picker is not carried.
}

function createRetainedContextPayloads(): RetainedContextPayloads {
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/chat/useComposerContextImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ export function useComposerContextImport(input: {
}
case "thread": {
// The agent can only read threads on its own server; a pasted foreign one is dropped.
// Coder: no linked-machine exception (#26201 is not carried).
if (record.environmentId !== environmentId) break;
addComposerDraftThreadContexts(threadDraftTarget, [record], {
appendReference: false,
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/composerContextPresentation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ function ComposerReviewCommentDetails({ comment }: { comment: ReviewCommentConte

function ComposerThreadContextChip(props: { record: ThreadContextRecord }) {
const { environmentId } = use(ComposerContextActionsContext);
// Coder: a thread on a linked machine still reads as unreadable (#26201 is not carried).
return (
<ThreadContextChip
record={props.record}
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/settings/WorkspaceLinkSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,11 @@ export function WorkspaceLinkEditor({
void operation
.run("save", "Could not save link", () =>
updateConfig((current) => {
// The id is the origin that work started over the link carries, which its fence
// compares; a rule pointed at other workspaces is a new link with a new id.
const sameEndpoints = link !== null && link.from === from && link.to === to;
const next: CoderWorkspaceLinkProfile = {
id: link?.id ?? `workspace-link-${randomUUID()}`,
id: sameEndpoints ? link.id : `workspace-link-${randomUUID()}`,
from,
to,
access,
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/contextChipAccents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const CONTEXT_CHIP_ACCENTS = {
"pr-closed": "oklch(0.62 0.16 16)",
skill: "oklch(0.62 0.16 322)",
thread: "oklch(0.62 0.12 190)",
// Coder: no machine (environment) chips; the @-machine picker is not carried.
citation: "oklch(0.62 0.16 259)",
"doc-pdf": "oklch(0.62 0.16 24)",
"doc-word": "oklch(0.62 0.136 248)",
Expand Down
15 changes: 10 additions & 5 deletions docs/internals/coder-only.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,13 +526,17 @@ listed here is drift to remove rather than fork behavior to keep.
`WorkspaceLinkSettings.tsx`), runs the call on the target workspace's helper with
`coder.hub.invoke` as that link's caller (`sessionId` `coder-link:<rule id>`, the rule's access,
the calling agent's modes as `narrowedTo`), and replies; the browser bridge refuses the hub
methods. A workspace that is not connected is unreachable, and upstream's follower retries.
methods. A workspace that is not connected is unreachable (`orchestration_error`, including a
linked workspace that has not connected since the gateway started), and upstream's follower
retries; a call with no matching rule is refused for good (`capability_denied`). Editing a
rule's endpoints mints a new rule id, since the id is the origin the fence compares.
Differences, each marked `Coder:`: no `T3-Mode-Limit` header (the hub carries the limits);
`PeerLinks` has no pairing, storage, or expiry (`expiresAt` is a fixed far-future value);
`t3_environment_link`, `t3_environment_unlink`, `PeerLinkRequests`, the link-request card, and
the `WsPeerLinks*` handlers are not carried, so `t3_environment_links`' `notListed` and the
orchestration instructions point to the Links settings; `coder.hub.invoke` refuses calls while
Labs' switch is off, as upstream's `/mcp` does; `T3ToolBridge`'s binding gains `invoke` for a
Labs' switch is off, as upstream's `/mcp` does, with upstream `PeerMcpClient`'s non-final
`orchestration_error` so a running task resumes once the switch is on again; `T3ToolBridge`'s binding gains `invoke` for a
linked caller; and upstream's peer tests (`PeerForwarding.test.ts`, `RemoteDelegation.test.ts`,
`linkOrigin.test.ts`, `PeerLinks.testkit.ts`) are not carried because they pair two HTTP
servers; `coderHub.test.ts` and the gateway's `hub.test.ts` test the transport. Migrations 070
Expand All @@ -547,7 +551,8 @@ listed here is drift to remove rather than fork behavior to keep.
exports; the `@` machine menu items and machine chips in the composer and drafts
(`useComposerMenuItems.ts`, `ComposerCommandMenu.tsx`, `composerThreadItems.ts`,
`composerDraftStore.ts`, `composerContextRecords.ts`, `composerContextPresentation.tsx`,
`useComposerSend.ts`, `chatViewSend.ts`, `useQueuedRunEdit.ts`); the link-request timeline
`composerContextUndo.ts`, `contextChipAccents.ts`, `ContextChip.tsx`, `useComposerSend.ts`,
`chatViewSend.ts`, `useQueuedRunEdit.ts`); the link-request timeline
card (`TimelineV2EventRow.tsx`, `session-logic.ts`); and the `t3_environment_link`/`unlink`
tool presentations (`t3McpToolPresentation.ts`, `t3ToolSummary.ts`). The contract's
`environment` record kind keeps upstream's definition in `contextPresentationRegistry.ts` and
Expand All @@ -571,7 +576,7 @@ listed here is drift to remove rather than fork behavior to keep.
`CODER_OMITTED_CAPABILITIES`. Web `chat/ThreadServersPanel.tsx`, its `ThreadDetailsPanel.tsx`
section, and its `thread-sidebar.md` paragraph are cut.
- The `@` picker lists no threads on linked machines (#26201): `composerThreadItems.ts`, shared
`composerContextReferences.ts` (`strandedThreadContexts`), and the composer hooks keep the
`composerContextReferences.ts` (`threadContextsOutsideEnvironment`), and the composer hooks keep the
same-server rules from before #26201. Copy thread (#26202) is upstream's.
- F6 workspace navigation (#25877) is upstream's without the desktop menu-action forwarding in
`AppSidebarLayout.tsx`; the Merge requests page's area is labelled "Merge requests"
Expand Down Expand Up @@ -1135,7 +1140,7 @@ listed here is drift to remove rather than fork behavior to keep.
`canonicalizeGitCafePullRequestUrl` is an identity function, since GitCafe is not carried. Diff output is capped
(`DIFF_MAX_OUTPUT_BYTES`) to fit the gateway's `MAX_RPC_MESSAGE_BYTES` ceiling.
- GitLab fixes for bugs still in upstream's `GitLabCli` (checked against upstream `main`
e9236274a3):
67661cfbb5):
merge requests between projects post to the source project with a numeric
`target_project_id` instead of sending the repository text as `source_project_id`; merge
request URLs reach `glab` as `--repo <url> ... -- <iid>` because older versions read URLs as
Expand Down
3 changes: 3 additions & 0 deletions docs/internals/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ mobile's `microphonePriority.ts` into `packages/shared`) arrives as an added fil
`nativeLogging.ts`).
- Desktop (`apps/desktop`, `native`, `packaging`, `assets`); mobile (`apps/mobile`); hosted web
(`apps/marketing`, `apps/server/src/cloud`, web `cloud`, `components/clerk`).
- Triage, dev servers, and dev replay (`apps/server/src/triage`, `apps/server/scripts/dev-replay*`,
web `DevReplayHost.tsx`, `chat/ThreadServersPanel.tsx`, `chat/TriageDraftHero.tsx`,
`hooks/useStartTriage.ts`, `lib/triageTarget.ts`, `docs/user/triage.md`).
- Relay (`apps/server/src/relay`, `infra`, `packages/ssh`, `packages/tailscale`); telemetry
(`apps/server/src/{telemetry,resourceTelemetry,usage,diagnostics}`, web `components/usage`).
- Upstream repository tooling (`.github`, `.repos`, `oxlint-plugin-t3code`, `docs/operations`,
Expand Down
3 changes: 3 additions & 0 deletions docs/user/keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,6 @@ an available shortcut such as `alt+w`.

Many defaults include `!terminalFocus` so they do not intercept terminal input.
Keep that condition when remapping them if you want the same behavior.

See [Keyboard focus](./keyboard-focus.md) for how focus moves between workspace areas (`F6`)
and through the sidebar.
37 changes: 37 additions & 0 deletions docs/user/keyboard-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Keyboard focus

The command palette keeps focus while open. Closing it returns focus to the composer in chat,
or to the previous control on other pages. Choosing a workspace focus command moves to that
area instead. Opening Merge requests moves focus into that page.
While the palette or model picker is open, number shortcuts select its entries instead of
switching threads. Model shortcuts work in Settings as well as the composer.
See [Keybindings](./keybindings.md) to customize these shortcuts.

If you return to typing while a terminal is starting, the composer keeps focus when the terminal
becomes ready. Opening or switching to a terminal explicitly still focuses it.

## Move between workspace areas

Press `F6` or `Shift+F6` to move between the Threads sidebar,
conversation, message input, and visible panels. Navigation wraps around and restores
the last focused control in each area. Merge requests has a page content area. In Settings,
`F6` moves between the section list and the open section. Hidden areas are skipped. Other
dialogs and form popups retain focus; a thread context menu closes when an area command
moves focus away.

In a browser, plain `F6` may move to the browser's own toolbar. Use `mod+F6` and
`mod+Shift+F6` instead (`mod` is Command on macOS and Ctrl elsewhere). **Focus Next
Workspace Area** and **Focus Previous Workspace Area** are also in the command palette and
can be remapped in Settings > Keybindings.

## Browse threads

Tab enters the sidebar once. Up and Down move between threads without opening them; Enter
opens the focused thread or toggles a group. Right and Left expand and collapse groups.
Tab moves from the focused thread to its buttons. `F2` renames the focused thread.

Press `Shift+F10` or the Context Menu key to open the focused thread's menu. Use the arrow
keys to move through it, Enter to choose, and Escape to close it.

The Settings section list works the same way: Up and Down move between sections, and Enter
opens one.
15 changes: 15 additions & 0 deletions scripts/sync-resolve-deleted.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@ const DELETED_PATHS = {
"packages/contracts/src/resourceTelemetry.ts",
"packages/contracts/src/usage.ts",
],
// Triage, dev servers, and dev replay: no in-app triage of upstream issues, no dev-server panel
// (it needs the preview port scanner), and no dev-only replay provider.
"triage, dev servers, and dev replay": [
"apps/server/src/triage/",
"apps/server/scripts/dev-replay/",
"apps/server/scripts/dev-replay-agent.ts",
"apps/server/scripts/dev-replay-agent.test.ts",
"apps/web/src/components/DevReplayHost.tsx",
"apps/web/src/components/chat/ThreadServersPanel.tsx",
"apps/web/src/components/chat/TriageDraftHero.tsx",
"apps/web/src/hooks/useStartTriage.ts",
"apps/web/src/lib/triageTarget.ts",
"apps/web/src/lib/triageTarget.test.ts",
"docs/user/triage.md",
],
// Upstream repository tooling: vendored sources, CI, hooks, editor and agent configuration,
// and operations docs. The fork's .github/ keeps only SECURITY.md.
"upstream repository tooling": [
Expand Down