Skip to content
Open
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
2 changes: 2 additions & 0 deletions build/lib/stylelint/vscode-known-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,8 @@
"--agents-floating-panel-gap",
"--agents-part-corner-radius",
"--agents-part-frame-border-width",
"--agents-session-frame-inset",
"--agents-side-pane-frame-inset",
"--agents-window-corner-radius",
"--session-chat-base-height",
"--editor-font-size",
Expand Down
15 changes: 15 additions & 0 deletions cli/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,18 @@ an interactive prompt.

Without `--tunnel`, `code agent` and `code agent host` still start or reuse a local
agent host supervisor.

## Tunnel request correlation

Management requests and relay WebSocket handshakes carry the
`X-Tunnels-VSCode-Session-Id`, `X-Tunnels-VSCode-Client-Operation-Id`, and
`X-Tunnels-VSCode-Client-Request-Id` headers. Each request has a fresh request ID.
VS Code passes its telemetry session and operation IDs through
`VSCODE_TUNNEL_SESSION_ID` and `VSCODE_TUNNEL_OPERATION_ID` when launching the CLI.
These IDs are preserved verbatim; telemetry session IDs are not necessarily UUIDs.
Standalone commands use a process-local session ID and a new operation ID for each
management client. Correlation IDs are sent in service headers, not written to CLI logs.
Local CLI status, login, and service-management invocations retain correlation IDs but
do not emit host-operation telemetry.
Attaching to an already-running tunnel does not replace that process's correlation IDs;
its service requests retain the IDs of the original host process.
2 changes: 1 addition & 1 deletion cli/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jiff = { version = "0.2", default-features = false, features = ["std", "serde"]
http = "1"
gethostname = "0.4.3"
libc = "0.2.144"
tunnels = { git = "https://github.com/microsoft/dev-tunnels", rev = "64048c1409ff56cb958b879de7ea069ec71edc8b", default-features = false, features = ["connections"] }
tunnels = { git = "https://github.com/microsoft/dev-tunnels", rev = "b73b0eb774b86a5b519c300f2f41c9072a66f663", default-features = false, features = ["connections"] }
keyring = { version = "2.0.3", default-features = false, features = ["linux-secret-service-rt-tokio-crypto-openssl", "platform-windows", "platform-macos", "linux-keyutils"] }
dialoguer = "0.10.4"
hyper = { version = "1", features = ["server", "http1", "client"] }
Expand Down
123 changes: 119 additions & 4 deletions cli/src/tunnels/dev_tunnels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::util::errors::{
use crate::util::input::prompt_placeholder;
use futures::future::BoxFuture;
use futures::{FutureExt, TryFutureExt};
use http::header::{HeaderName, HeaderValue};
use http::StatusCode;
use rand::prelude::IteratorRandom;
use regex::Regex;
Expand All @@ -30,12 +31,73 @@ use tunnels::contracts::{
TUNNEL_ACCESS_SCOPES_CONNECT, TUNNEL_PROTOCOL_AUTO,
};
use tunnels::management::{
new_tunnel_management, HttpError, TunnelLocator, TunnelManagementClient, TunnelRequestOptions,
NO_REQUEST_OPTIONS,
new_tunnel_management, HttpError, TunnelClientBuilder, TunnelLocator, TunnelManagementClient,
TunnelRequestOptions, NO_REQUEST_OPTIONS,
};

static TUNNEL_COUNT_LIMIT_NAME: &str = "TunnelsPerUserPerLocation";
static TUNNEL_PORT_PROTOCOL_CONFLICT_DETAIL: &str = "The tunnel port protocol cannot be changed.";
static TUNNEL_SESSION_ID: LazyLock<uuid::Uuid> = LazyLock::new(uuid::Uuid::new_v4);

fn tunnel_correlation_id(
log: &log::Logger,
name: &str,
value: Result<String, std::env::VarError>,
fallback: uuid::Uuid,
) -> HeaderValue {
let fallback = HeaderValue::from_str(&fallback.to_string()).unwrap();
match value {
Ok(value) if !value.is_empty() => match HeaderValue::from_str(&value) {
Ok(id) => id,
Err(error) => {
warning!(log, "Invalid {}: {}", name, error);
fallback
}
},
Ok(_) => {
warning!(log, "Invalid {}: empty correlation ID", name);
fallback
}
Err(std::env::VarError::NotPresent) => fallback,
Err(error) => {
warning!(log, "Invalid {}: {}", name, error);
fallback
}
}
}

fn tunnel_management_client(log: &log::Logger) -> TunnelClientBuilder {
let session_id = tunnel_correlation_id(
log,
"VSCODE_TUNNEL_SESSION_ID",
std::env::var("VSCODE_TUNNEL_SESSION_ID"),
*TUNNEL_SESSION_ID,
);
let operation_id = tunnel_correlation_id(
log,
"VSCODE_TUNNEL_OPERATION_ID",
std::env::var("VSCODE_TUNNEL_OPERATION_ID"),
uuid::Uuid::new_v4(),
);
let mut client = new_tunnel_management(&TUNNEL_SERVICE_USER_AGENT);
client.additional_headers(vec![
(
HeaderName::from_static("x-tunnels-vscode-session-id"),
session_id,
),
(
HeaderName::from_static("x-tunnels-vscode-client-operation-id"),
operation_id,
),
]);
client.additional_headers_provider(|| {
vec![(
HeaderName::from_static("x-tunnels-vscode-client-request-id"),
HeaderValue::from_str(&uuid::Uuid::new_v4().to_string()).unwrap(),
)]
});
client
}

#[allow(dead_code)]
mod tunnel_flags {
Expand Down Expand Up @@ -329,7 +391,7 @@ impl DevTunnels {
auth: auth::Auth,
paths: &LauncherPaths,
) -> DevTunnels {
let mut client = new_tunnel_management(&TUNNEL_SERVICE_USER_AGENT);
let mut client = tunnel_management_client(log);
client.authorization_provider(auth.clone());

DevTunnels {
Expand All @@ -347,7 +409,7 @@ impl DevTunnels {
auth: auth::Auth,
paths: &LauncherPaths,
) -> DevTunnels {
let mut client = new_tunnel_management(&TUNNEL_SERVICE_USER_AGENT);
let mut client = tunnel_management_client(log);
client.authorization_provider(auth.clone());

DevTunnels {
Expand Down Expand Up @@ -1328,6 +1390,59 @@ fn tunnel_has_host_connection(tunnel: &Tunnel) -> bool {
mod test {
use super::*;

#[test]
fn test_tunnel_correlation_preserves_telemetry_session_id() {
let session_id = "7c86e247-3438-4f26-959d-061b3299f4de1791567000000";
assert_eq!(
tunnel_correlation_id(
&log::Logger::test(),
"VSCODE_TUNNEL_SESSION_ID",
Ok(session_id.to_string()),
uuid::Uuid::new_v4(),
),
HeaderValue::from_static(session_id),
);
}

#[test]
fn test_tunnel_correlation_rejects_invalid_header_values() {
let fallback = uuid::Uuid::new_v4();
for value in ["", "session\r\ninjected-header: value"] {
assert_eq!(
tunnel_correlation_id(
&log::Logger::test(),
"VSCODE_TUNNEL_SESSION_ID",
Ok(value.to_string()),
fallback,
),
HeaderValue::from_str(&fallback.to_string()).unwrap(),
);
}
}

#[test]
fn test_tunnel_request_correlation() {
let client: TunnelManagementClient = tunnel_management_client(&log::Logger::test()).into();
let first = client.request_headers();
let second = client.request_headers();
assert_eq!(
first
.iter()
.map(|(name, _)| name.as_str())
.collect::<Vec<_>>(),
vec![
"x-tunnels-vscode-session-id",
"x-tunnels-vscode-client-operation-id",
"x-tunnels-vscode-client-request-id",
]
);
for (_, value) in &first {
assert!(uuid::Uuid::parse_str(value.to_str().unwrap()).is_ok());
}
assert_eq!(&first[..2], &second[..2]);
assert_ne!(first[2].1, second[2].1);
}

#[test]
fn test_clean_hostname_for_tunnel() {
assert_eq!(
Expand Down
11 changes: 11 additions & 0 deletions extensions/markdown-language-features/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,23 @@ Build outputs are written to `out/` (desktop), `dist/` (web), and `notebook-out/

Markdown editor commands and their default keybindings are defined in `vscode-packages/vscode-team-tools/packages/markdown-editor/src/editorCommands.ts`. Do not manually edit entries marked with `"$generated": true` in this extension's `package.json` or their titles in `package.nls.json`: `npm run build-markdown-editor` and `npm run watch-markdown-editor` regenerate them while preserving manual entries. Run `npm run check-markdown-editor-package-json` to verify that the checked-in manifests are current without modifying them.

### Active block highlighting

`markdown.editor.highlightActiveBlock` controls the rich editor's active-block
background and glow, and defaults to enabled. Resource-specific changes are sent
to the renderer without reloading the document or changing source-marker visibility.
The webview reserves symmetric scrollbar gutters so the document remains centered.

### Outline and breadcrumbs

The rich editor opts into `customTextEditorNavigation`. The workbench reuses the document's symbol providers for Outline and breadcrumbs; the extension reports each panel's source selection and routes reveal and view-state requests through the Markdown editor RPC bridge. Offset mapping uses the renderer's synchronized text, including when its line endings differ from the document.

Keep navigation independent of editing mode. Breadcrumb previews must preserve focus and selection, canceled previews must not override newer navigation, and opening a symbol to the side must retain the custom editor type. Navigation regression coverage lives in `src/test/markdownEditorNavigation.test.ts` and `src/test/markdownEditorBridge.test.ts`, with workbench coverage in the custom-editor navigation and document-symbol Outline suites.

Chat also consumes this per-panel navigation selection as implicit context, including in the Agents window. It attaches a range in the underlying Markdown document, not rendered HTML, and respects the existing implicit-context settings and ignored-file checks. A collapsed or cleared selection falls back to the document URI. No separate Markdown chat bridge is needed. Workbench regression coverage lives in `src/vs/workbench/contrib/chat/test/browser/attachments/chatImplicitContext.test.ts`.

Selection, document, and webview changes share a 500 ms debounce before refreshing chat context, matching code-editor behavior. Navigation availability changes refresh immediately, and registering a chat widget refreshes existing widgets as well so a superseded asynchronous refresh cannot leave them stale.

### Running tests

You can run the VS Code extension tests by running the `Markdown Extension Tests` target in VS Code. This will run the tests under `./src/test`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ interface InitialState {
/** Identifies the authoritative text baseline against which local edits are computed. */
readonly editEpoch: number;
readonly readonly: boolean;
readonly highlightActiveBlock: boolean;
readonly richLinksEnabled: boolean;
readonly linkPresentationRules: readonly { id: string; source: string; flags: string; kind: LinkPresentationKind }[];
}
Expand Down Expand Up @@ -271,11 +272,14 @@ class Editor extends Disposable {
highlightThemeChanged: () => {
this.#syntaxHighlighter.themeChanged();
},
configurationChanged: ({ highlightActiveBlock }) => {
this.#view?.highlightActiveBlock.set(highlightActiveBlock, undefined);
},
richLinkPresentations: ({ presentations }) => {
this.#linkPresentationProvider?.updatePresentations(presentations);
},
}));
this.#createView(host, initialState.content);
this.#createView(host, initialState.content, initialState.highlightActiveBlock);
this.#send('ready', this.#host.ready({
documentVersion: initialState.documentVersion,
editEpoch: this.#editEpoch,
Expand Down Expand Up @@ -355,7 +359,7 @@ class Editor extends Disposable {
this.#connection.close();
}

#createView(host: HTMLElement, content: string): void {
#createView(host: HTMLElement, content: string, highlightActiveBlock: boolean): void {
const model = this.model;
const scriptNonce = document.querySelector<HTMLMetaElement>('meta[name="vscode-markdown-editor-script-nonce"]')?.content;
const iframeBootstrapUrl = new URL(location.href);
Expand Down Expand Up @@ -387,6 +391,7 @@ class Editor extends Disposable {

const view = this._register(new EditorView(model, {
classNames: ['md-theme-vscode-default'],
highlightActiveBlock,
presentation: model.readonlyMode.get() ? 'reading' : 'editing',
syntaxHighlighter: this.#syntaxHighlighter,
linkPresentationProvider: this.#linkPresentationProvider,
Expand Down Expand Up @@ -703,6 +708,7 @@ function isInitialState(value: unknown): value is InitialState {
&& Number.isInteger(candidate.editEpoch)
&& candidate.editEpoch >= 0
&& typeof candidate.readonly === 'boolean'
&& typeof candidate.highlightActiveBlock === 'boolean'
&& typeof candidate.richLinksEnabled === 'boolean'
&& Array.isArray(candidate.linkPresentationRules);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class ImagePasteController extends Disposable implements IClipboardStrate
throw new Error('The clipboard formats are not available to this editor. Try copying the image itself instead of its file.');
}
return { text, images };
}));
}, plainText));
this.#status.className = 'md-language-status';
this.#status.setAttribute('role', 'status');
this.#status.hidden = true;
Expand Down Expand Up @@ -78,7 +78,7 @@ export class ImagePasteController extends Disposable implements IClipboardStrate
};
}

async #paste(context: IClipboardContext, read: () => Promise<{ readonly images: readonly File[]; readonly text: string }>): Promise<void> {
async #paste(context: IClipboardContext, read: () => Promise<{ readonly images: readonly File[]; readonly text: string }>, plainText = false): Promise<void> {
const request = ++this.#request;
const source = this.editor.sourceText.get();
const selection = this.editor.selection.get();
Expand Down Expand Up @@ -108,7 +108,7 @@ export class ImagePasteController extends Disposable implements IClipboardStrate
}
this.#status.hidden = true;
if (!payload.length) {
if (text) { context.insertText(text); }
if (text) { context.insertText(text, plainText); }
return;
}
this.#status.textContent = 'Pasting image...';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ body {
#editor {
height: 100%;
width: 100%;
overflow: scroll;
overflow: auto;
scrollbar-gutter: stable both-edges;
scrollbar-width: thin;
scrollbar-color: var(--vscode-scrollbarSlider-background) transparent;
}

.md-comment-widget {
Expand Down
33 changes: 28 additions & 5 deletions extensions/markdown-language-features/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading