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
110 changes: 110 additions & 0 deletions .eslint-plugin-local/code-no-bracket-notation-for-identifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as eslint from 'eslint';
import { TSESTree } from '@typescript-eslint/utils';
import * as ts from 'typescript';

/**
* Disallow bracket notation for accessing properties that are valid identifiers,
* especially private members (starting with underscore). Bracket notation should
* only be used for properties with special characters or computed property names.
*
* Bad: obj['_privateMember']
* Bad: obj['normalProperty']
* Good: obj._privateMember // TypeScript will catch private access
* Good: obj.normalProperty
* Good: obj['property-with-dashes']
* Good: obj[computedKey]
*/
export default new class NoBracketNotationForIdentifiers implements eslint.Rule.RuleModule {

readonly meta: eslint.Rule.RuleMetaData = {
type: 'problem',
docs: {
description: 'Disallow bracket notation for accessing properties that are valid identifiers'
},
messages: {
noBracketNotation: 'Use dot notation instead of bracket notation for property \'{{property}}\'. Bracket notation bypasses TypeScript\'s type checking and access modifiers.'
},
schema: [],
fixable: 'code'
};

create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {

/**
* Check if a string is a valid JavaScript identifier
*/
function isValidIdentifier(str: string): boolean {
if (str.includes('\\')) {
return false;
}
const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, str);
const token = scanner.scan();
const isIdentifierName = token === ts.SyntaxKind.Identifier
|| (token >= ts.SyntaxKind.FirstKeyword && token <= ts.SyntaxKind.LastKeyword);
return isIdentifierName && scanner.getTokenText() === str && scanner.scan() === ts.SyntaxKind.EndOfFileToken;
}

return {
MemberExpression(node: any) {
const memberExpr = node as TSESTree.MemberExpression;

// Only check computed member expressions (bracket notation)
if (!memberExpr.computed) {
return;
}

// Only check string literals in brackets
if (memberExpr.property.type !== 'Literal' || typeof memberExpr.property.value !== 'string') {
return;
}

const propertyName = memberExpr.property.value;

// If it's a valid identifier, report it
if (isValidIdentifier(propertyName)) {
context.report({
node: memberExpr.property,
messageId: 'noBracketNotation',
data: {
property: propertyName
},
fix(fixer) {
const property = memberExpr.property as unknown as eslint.Rule.Node;
const leftBracket = context.sourceCode.getTokenBefore(property);
const rightBracket = context.sourceCode.getTokenAfter(property);
if (leftBracket?.value !== '[' || rightBracket?.value !== ']') {
return null;
}
const hasComments = context.sourceCode
.getTokensBetween(leftBracket, rightBracket, { includeComments: true })
.some(token => token.type === 'Block' || token.type === 'Line');
if (hasComments) {
return null;
}

const bracketFix = fixer.replaceTextRange(
[leftBracket.range[0], rightBracket.range[1]],
`${memberExpr.optional ? '' : '.'}${propertyName}`
);
if (memberExpr.object.type === 'Literal' && typeof memberExpr.object.value === 'number') {
const object = memberExpr.object as unknown as eslint.Rule.Node;
return [
fixer.insertTextBefore(object, '('),
fixer.insertTextAfter(object, ')'),
bracketFix
];
}

return bracketFix;
}
});
}
}
};
}
};
1 change: 1 addition & 0 deletions .github/skills/sessions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Start with `src/vs/sessions/README.md`, then read only the specifications releva
|------|---------------|
| Layering, folder ownership, cross-module imports | `src/vs/sessions/LAYERS.md` |
| Session/chat model, services, provider contract, core data flow | `src/vs/sessions/SESSIONS.md` |
| Automations ownership, routing, migration, persistence, and run lifecycle | `src/vs/sessions/AUTOMATIONS.md` |
| Workbench parts, grid, title bar, editor presentation | `src/vs/sessions/LAYOUT.md` |
| Session-aware layout state and restoration | `src/vs/sessions/LAYOUT_CONTROLLER.md` |
| Single-pane behavior and expected compositions | `src/vs/sessions/SINGLE_PANE_SCENARIOS.md` |
Expand Down
19 changes: 14 additions & 5 deletions .github/skills/sweeper-fix/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
name: sweeper-fix
description: Fix a microsoft/vscode issue that the VS Code Sweeper reviewed as agent-fixable. Fetches the review's fix spec from the sweeper's public state repo, implements the narrow fix in the current vscode checkout, and — after showing the diff — opens a draft PR. Use when asked to fix a vscode issue with the sweeper-fix skill, a vscodesweeper record, or a sweeper fix spec.
description: Implement a VS Code Sweeper fix spec — fetch the sweeper's agent-fixable review record from its public state repo, implement the narrow fix in the current vscode checkout, and — after showing the diff — open a draft PR. Use ONLY when the request explicitly asks for the sweeper — "sweeper-fix", "sweeper", "vscodesweeper", a sweeper record, or a sweeper fix spec. Do NOT use for a plain "fix this issue" request that doesn't mention the sweeper; fix those directly with your normal tools instead.
---

<!-- Generated by vscodesweeper (sweeper-fix skill v4) — do not edit by hand.
Source: prompts/sweeper-fix-skill.md in the vscodesweeper repo; getting started:
<!-- Generated by vscodesweeper (sweeper-fix skill v5) — do not edit by hand.
Source: skills/sweeper-fix/SKILL.template.md in the vscodesweeper repo; getting started:
https://egamma.github.io/vscodesweeper-state/fix-skill.html -->

# sweeper-fix — implement a sweeper-reviewed fix
Expand Down Expand Up @@ -34,8 +34,17 @@ access):
gh api "repos/egamma/vscodesweeper-state/contents/records/microsoft/vscode/items/<issue-number>.md?ref=state" -H "Accept: application/vnd.github.raw"
```

No record → stop: this issue hasn't been reviewed by the sweeper; the skill only fixes
reviewed, agent-fixable issues.
No record → this skill does not apply: the issue hasn't been reviewed by the sweeper, and
the skill only implements sweeper fix specs. Say so in one line, then **continue fixing the
issue by your normal means** — fetch it with the repo pinned explicitly (never bare
`gh issue view`, which a fork remote can redirect to the wrong repo's issue `<n>`), then
analyze and implement:

```
gh issue view <issue-number> --repo microsoft/vscode
```

The absence of a sweeper record is never a reason to refuse the fix itself.

## 2 · Gate — every check against LIVE GitHub state, not just the record

Expand Down
2 changes: 1 addition & 1 deletion .vscode/notebooks/endgame.github-issues
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{
"kind": 2,
"language": "github-issues",
"value": "$MILESTONE=milestone:\"1.135.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified"
"value": "$MILESTONE=milestone:\"1.136.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified"
},
{
"kind": 1,
Expand Down
2 changes: 1 addition & 1 deletion .vscode/notebooks/my-endgame.github-issues
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{
"kind": 2,
"language": "github-issues",
"value": "$MILESTONE=milestone:\"1.135.0\"\n\n$MINE=assignee:@me"
"value": "$MILESTONE=milestone:\"1.136.0\"\n\n$MINE=assignee:@me"
},
{
"kind": 2,
Expand Down
5 changes: 1 addition & 4 deletions build/lib/preLaunch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async function getElectron() {
// directory is being removed and re-extracted. Skip the refresh when the
// already-present Electron matches the expected version; any detection
// failure falls back to a (re)download to preserve the previous behavior.
if (await isExpectedElectronInstalled()) {
if (!process.env['VSCODE_FORCE_PRELAUNCH'] && await isExpectedElectronInstalled()) {
return;
}
await runProcess(npm, ['run', 'electron']);
Expand All @@ -66,9 +66,6 @@ async function ensureCompiled() {
async function main() {
await ensureNodeModules();
await getElectron();
if (process.argv.includes('--only-electron')) {
return;
}
await ensureCompiled();

// Can't require this until after dependencies are installed
Expand Down
2 changes: 0 additions & 2 deletions build/linux/rpm/dep-lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export const referenceGeneratedDepsByArch = {
'libgtk-3.so.0()(64bit)',
'libm.so.6()(64bit)',
'libm.so.6(GLIBC_2.2.5)(64bit)',
'libm.so.6(GLIBC_2.27)(64bit)',
'libnspr4.so()(64bit)',
'libnss3.so()(64bit)',
'libnss3.so(NSS_3.11)(64bit)',
Expand Down Expand Up @@ -276,7 +275,6 @@ export const referenceGeneratedDepsByArch = {
'libgtk-3.so.0()(64bit)',
'libm.so.6()(64bit)',
'libm.so.6(GLIBC_2.17)(64bit)',
'libm.so.6(GLIBC_2.27)(64bit)',
'libnspr4.so()(64bit)',
'libnss3.so()(64bit)',
'libnss3.so(NSS_3.11)(64bit)',
Expand Down
27 changes: 23 additions & 4 deletions cli/src/commands/agent_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ fn decide_foreground_action(
) {
return ForegroundAction::ConflictError(format!(
"Agent host already running on {host_str}:{port} (PID {pid}), but {conflict}.\n\
Use `code agent kill` to stop it, or pass `--replace` to take over.",
Use `{application_name} agent kill` to stop it, or pass `--replace` to take over.",
application_name = constants::APPLICATION_NAME,
host_str = host.as_deref().unwrap_or("127.0.0.1"),
));
}
Expand Down Expand Up @@ -461,7 +462,7 @@ async fn run_supervisor(mut ctx: CommandContext, mut args: AgentHostArgs) -> Res
.and_then(|h| h.parse::<std::net::IpAddr>().ok())
.unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
output::print_network_lines(bound_port, banner_listen_ip, &token_suffix);
output::print_banner_line("Manage", "code agent ps | code agent kill");
print_manage_banner_line();
output::print_banner_footer();
let _ = std::io::stdout().flush();

Expand Down Expand Up @@ -554,15 +555,24 @@ fn print_reuse_banner(
.and_then(|h| h.parse::<std::net::IpAddr>().ok())
.unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
output::print_network_lines(port, banner_listen_ip, &token_suffix);
output::print_banner_line("Manage", "code agent ps | code agent kill");
print_manage_banner_line();
output::print_banner_footer();
let _ = std::io::stdout().flush();
log.result(format!(
"Agent host supervisor already running (PID {pid}). \
Use `code agent kill` to stop it, or `code agent host --replace` to start a fresh one."
Use `{application_name} agent kill` to stop it, or `{application_name} agent host --replace` to start a fresh one.",
application_name = constants::APPLICATION_NAME,
));
}

fn print_manage_banner_line() {
let application_name = constants::APPLICATION_NAME;
output::print_banner_line(
"Manage",
&format!("{application_name} agent ps | {application_name} agent kill"),
);
}

/// Compare the user's requested supervisor configuration with what's
/// recorded for the running supervisor's registry entry. Returns a short
/// human description of the first conflict found (e.g. `"--host 0.0.0.0
Expand Down Expand Up @@ -665,6 +675,15 @@ async fn daemonize_supervisor() -> Result<i32, AnyError> {
// passed in foreground.
cmd.args(std::env::args_os().skip(1));
cmd.env(SUPERVISOR_ENV, "1");
#[cfg(windows)]
cmd.env(
output::PARENT_STDOUT_SUPPORTS_UTF8_ENV,
if output::stdout_supports_utf8() {
"1"
} else {
"0"
},
);
cmd.stdin(std::process::Stdio::null());
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
Expand Down
41 changes: 34 additions & 7 deletions cli/src/commands/agent_logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::util::errors::AnyError;
use super::agent;
use super::agent_discovery;
use super::args::AgentLogsArgs;
use super::output::Styles;
use super::output::{self, Styles};
use super::CommandContext;

/// Subscribes to a session and streams actions/notifications in real time.
Expand Down Expand Up @@ -55,7 +55,10 @@ pub async fn agent_logs(ctx: CommandContext, args: AgentLogsArgs) -> Result<i32,
"\n{}",
header.apply_to("Streaming events (Ctrl+C to quit)...")
);
println!("{}", header.apply_to("─".repeat(50)));
println!(
"{}",
header.apply_to(output::utf8_or_ascii("─", "-").repeat(50))
);

// Stream events until Ctrl+C or the subscription closes.
let mut shutdown = ShutdownRequest::create_rx([ShutdownRequest::CtrlC]);
Expand Down Expand Up @@ -118,11 +121,14 @@ fn print_initial_state(uri: &str, result: &SubscribeResult) {
for chat in &session.chats {
let status = SessionStatus::from_bits(chat.status);
let marker = if status.contains(SessionStatus::InProgress) {
Style::new().green().bold().apply_to("►")
Style::new()
.green()
.bold()
.apply_to(output::utf8_or_ascii("►", ">"))
} else if status.contains(SessionStatus::Error) {
Styles::error().apply_to("✗")
Styles::error().apply_to(output::utf8_or_ascii("✗", "x"))
} else {
Styles::muted().apply_to("○")
Styles::muted().apply_to(output::utf8_or_ascii("○", "o"))
};
let title = if chat.title.is_empty() {
"(untitled)".to_string()
Expand Down Expand Up @@ -190,9 +196,30 @@ fn action_style(type_name: &str) -> Style {

fn truncate(s: &str, max: usize) -> String {
let s = s.replace('\n', " ");
if s.len() <= max {
if s.chars().count() <= max {
s
} else {
format!("{}…", &s[..max - 1])
let suffix = output::utf8_or_ascii("…", "...");
let prefix_length = max.saturating_sub(suffix.chars().count());
format!(
"{}{suffix}",
s.chars().take(prefix_length).collect::<String>()
)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn truncate_respects_character_limit() {
assert_eq!(
(
truncate("abcdef", 5).chars().count(),
truncate("áéíóúñ", 5).chars().count()
),
(5, 5)
);
}
}
17 changes: 11 additions & 6 deletions cli/src/commands/agent_ps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,13 @@ async fn agent_ps_multi(
/// complete).
fn print_host_outcome_human(outcome: &HostSessions, all: bool) -> Result<(), AnyError> {
let header = Styles::title();
let separator = output::utf8_or_ascii("──", "--");
println!(
"\n{}",
header.apply_to(format!("── {} ──", outcome.endpoint.label()))
header.apply_to(format!(
"{separator} {} {separator}",
outcome.endpoint.label()
))
);

match &outcome.sessions {
Expand All @@ -216,7 +220,8 @@ fn print_host_outcome_human(outcome: &HostSessions, all: bool) -> Result<(), Any
}
}
Err(e) => {
println!(" {}", Styles::error().apply_to(format!("⚠ {e}")));
let warning = output::utf8_or_ascii("⚠", "!");
println!(" {}", Styles::error().apply_to(format!("{warning} {e}")));
}
}

Expand Down Expand Up @@ -385,13 +390,13 @@ fn format_sessions_list(sessions: &[&SessionSummary]) -> String {
fn status_styled(status: u32) -> console::StyledObject<String> {
let status = SessionStatus::from_bits(status);
if status.contains(SessionStatus::InputNeeded) {
Styles::warning().apply_to("● input needed".to_string())
Styles::warning().apply_to(format!("{} input needed", output::utf8_or_ascii("●", "*")))
} else if status.contains(SessionStatus::InProgress) {
Styles::success().apply_to("● in progress".to_string())
Styles::success().apply_to(format!("{} in progress", output::utf8_or_ascii("●", "*")))
} else if status.contains(SessionStatus::Error) {
Styles::error().apply_to("● error".to_string())
Styles::error().apply_to(format!("{} error", output::utf8_or_ascii("●", "*")))
} else if status.contains(SessionStatus::Idle) {
Styles::muted().apply_to("○ idle".to_string())
Styles::muted().apply_to(format!("{} idle", output::utf8_or_ascii("○", "o")))
} else {
Styles::muted().apply_to(format!("? unknown ({})", status.bits()))
}
Expand Down
Loading
Loading