Skip to content
Closed
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
120 changes: 120 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
private readonly string? _optionsHost;
private int? _actualPort;
private int? _negotiatedProtocolVersion;
private readonly SessionFsConfig? _sessionFsConfig;
private List<ModelInfo>? _modelsCache;
private readonly SemaphoreSlim _modelsCacheLock = new(1, 1);
private readonly Func<CancellationToken, Task<List<ModelInfo>>>? _onListModels;
Expand Down Expand Up @@ -143,6 +144,7 @@ public CopilotClient(CopilotClientOptions? options = null)

_logger = _options.Logger ?? NullLogger.Instance;
_onListModels = _options.OnListModels;
_sessionFsConfig = _options.SessionFs;

// Parse CliUrl if provided
if (!string.IsNullOrEmpty(_options.CliUrl))
Expand Down Expand Up @@ -227,6 +229,12 @@ async Task<Connection> StartCoreAsync(CancellationToken ct)
// Verify protocol version compatibility
await VerifyProtocolVersionAsync(connection, ct);

// Register sessionFs provider if configured
if (_sessionFsConfig is not null)
{
await RegisterSessionFsProviderAsync(connection, ct);
}

_logger.LogInformation("Copilot client connected");
return connection;
}
Expand Down Expand Up @@ -462,6 +470,18 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
{
session.RegisterUserInputHandler(config.OnUserInputRequest);
}
if (_sessionFsConfig is not null)
{
if (config.CreateSessionFsHandler is not null)
{
session.RegisterSessionFsHandler(config.CreateSessionFsHandler(session));
}
else
{
throw new InvalidOperationException(
"CreateSessionFsHandler is required in session config when SessionFs is enabled in client options.");
}
}
if (config.Hooks != null)
{
session.RegisterHooks(config.Hooks);
Expand Down Expand Up @@ -582,6 +602,18 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
{
session.RegisterUserInputHandler(config.OnUserInputRequest);
}
if (_sessionFsConfig is not null)
{
if (config.CreateSessionFsHandler is not null)
{
session.RegisterSessionFsHandler(config.CreateSessionFsHandler(session));
}
else
{
throw new InvalidOperationException(
"CreateSessionFsHandler is required in session config when SessionFs is enabled in client options.");
}
}
if (config.Hooks != null)
{
session.RegisterHooks(config.Hooks);
Expand Down Expand Up @@ -1104,6 +1136,20 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
_negotiatedProtocolVersion = serverVersion;
}

private async Task RegisterSessionFsProviderAsync(Connection connection, CancellationToken cancellationToken)
{
var config = _sessionFsConfig!;
await _rpc!.SessionFs.SetProviderAsync(
config.InitialCwd,
config.SessionStatePath,
config.Conventions switch
{
SessionFsConventions.Windows => SessionFsSetProviderRequestConventions.Windows,
_ => SessionFsSetProviderRequestConventions.Posix,
},
cancellationToken);
}

private static async Task<(Process Process, int? DetectedLocalhostTcpPort, StringBuilder StderrBuffer)> StartCliServerAsync(CopilotClientOptions options, ILogger logger, CancellationToken cancellationToken)
{
// Use explicit path, COPILOT_CLI_PATH env var (from options.Environment or process env), or bundled CLI - no PATH fallback
Expand Down Expand Up @@ -1319,6 +1365,19 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
rpc.AddLocalRpcMethod("userInput.request", handler.OnUserInputRequest);
rpc.AddLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke);
rpc.AddLocalRpcMethod("systemMessage.transform", handler.OnSystemMessageTransform);

// SessionFs client session API handlers
rpc.AddLocalRpcMethod("sessionFs.readFile", handler.OnSessionFsReadFile);
rpc.AddLocalRpcMethod("sessionFs.writeFile", handler.OnSessionFsWriteFile);
rpc.AddLocalRpcMethod("sessionFs.appendFile", handler.OnSessionFsAppendFile);
rpc.AddLocalRpcMethod("sessionFs.exists", handler.OnSessionFsExists);
rpc.AddLocalRpcMethod("sessionFs.stat", handler.OnSessionFsStat);
rpc.AddLocalRpcMethod("sessionFs.mkdir", handler.OnSessionFsMkdir);
rpc.AddLocalRpcMethod("sessionFs.readdir", handler.OnSessionFsReaddir);
rpc.AddLocalRpcMethod("sessionFs.readdirWithTypes", handler.OnSessionFsReaddirWithTypes);
rpc.AddLocalRpcMethod("sessionFs.rm", handler.OnSessionFsRm);
rpc.AddLocalRpcMethod("sessionFs.rename", handler.OnSessionFsRename);

rpc.StartListening();

// Transition state to Disconnected if the JSON-RPC connection drops
Expand Down Expand Up @@ -1554,6 +1613,67 @@ public async Task<PermissionRequestResponseV2> OnPermissionRequestV2(string sess
});
}
}

// SessionFs handler methods
public async Task<SessionFsReadFileResult> OnSessionFsReadFile(string sessionId, string path)
{
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
return await session.HandleSessionFsReadFileAsync(new SessionFsReadFileParams { SessionId = sessionId, Path = path });
}

public async Task OnSessionFsWriteFile(string sessionId, string path, string content, int? mode = null)
{
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
await session.HandleSessionFsWriteFileAsync(new SessionFsWriteFileParams { SessionId = sessionId, Path = path, Content = content, Mode = mode });
}

public async Task OnSessionFsAppendFile(string sessionId, string path, string content, int? mode = null)
{
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
await session.HandleSessionFsAppendFileAsync(new SessionFsAppendFileParams { SessionId = sessionId, Path = path, Content = content, Mode = mode });
}

public async Task<SessionFsExistsResult> OnSessionFsExists(string sessionId, string path)
{
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
return await session.HandleSessionFsExistsAsync(new SessionFsExistsParams { SessionId = sessionId, Path = path });
}

public async Task<SessionFsStatResult> OnSessionFsStat(string sessionId, string path)
{
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
return await session.HandleSessionFsStatAsync(new SessionFsStatParams { SessionId = sessionId, Path = path });
}

public async Task OnSessionFsMkdir(string sessionId, string path, bool? recursive = null, int? mode = null)
{
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
await session.HandleSessionFsMkdirAsync(new SessionFsMkdirParams { SessionId = sessionId, Path = path, Recursive = recursive, Mode = mode });
}

public async Task<SessionFsReaddirResult> OnSessionFsReaddir(string sessionId, string path)
{
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
return await session.HandleSessionFsReaddirAsync(new SessionFsReaddirParams { SessionId = sessionId, Path = path });
}

public async Task<SessionFsReaddirWithTypesResult> OnSessionFsReaddirWithTypes(string sessionId, string path)
{
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
return await session.HandleSessionFsReaddirWithTypesAsync(new SessionFsReaddirWithTypesParams { SessionId = sessionId, Path = path });
}

public async Task OnSessionFsRm(string sessionId, string path, bool? recursive = null, bool? force = null)
{
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
await session.HandleSessionFsRmAsync(new SessionFsRmParams { SessionId = sessionId, Path = path, Recursive = recursive, Force = force });
}

public async Task OnSessionFsRename(string sessionId, string src, string dest)
{
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
await session.HandleSessionFsRenameAsync(new SessionFsRenameParams { SessionId = sessionId, Src = src, Dest = dest });
}
}

private class Connection(
Expand Down
24 changes: 24 additions & 0 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public sealed partial class CopilotSession : IAsyncDisposable
private volatile PermissionRequestHandler? _permissionHandler;
private volatile UserInputHandler? _userInputHandler;
private volatile ElicitationHandler? _elicitationHandler;
private volatile ISessionFsHandler? _sessionFsHandler;
private ImmutableArray<SessionEventHandler> _eventHandlers = ImmutableArray<SessionEventHandler>.Empty;

private SessionHooks? _hooks;
Expand Down Expand Up @@ -664,6 +665,29 @@ internal void RegisterElicitationHandler(ElicitationHandler? handler)
_elicitationHandler = handler;
}

/// <summary>
/// Registers a session filesystem handler for this session.
/// </summary>
/// <param name="handler">The handler to invoke for filesystem operations.</param>
internal void RegisterSessionFsHandler(ISessionFsHandler handler)
{
_sessionFsHandler = handler;
}

internal ISessionFsHandler GetSessionFsHandler() =>
_sessionFsHandler ?? throw new InvalidOperationException($"No sessionFs handler registered for session: {SessionId}");

internal Task<SessionFsReadFileResult> HandleSessionFsReadFileAsync(SessionFsReadFileParams request) => GetSessionFsHandler().ReadFileAsync(request);
internal Task HandleSessionFsWriteFileAsync(SessionFsWriteFileParams request) => GetSessionFsHandler().WriteFileAsync(request);
internal Task HandleSessionFsAppendFileAsync(SessionFsAppendFileParams request) => GetSessionFsHandler().AppendFileAsync(request);
internal Task<SessionFsExistsResult> HandleSessionFsExistsAsync(SessionFsExistsParams request) => GetSessionFsHandler().ExistsAsync(request);
internal Task<SessionFsStatResult> HandleSessionFsStatAsync(SessionFsStatParams request) => GetSessionFsHandler().StatAsync(request);
internal Task HandleSessionFsMkdirAsync(SessionFsMkdirParams request) => GetSessionFsHandler().MkdirAsync(request);
internal Task<SessionFsReaddirResult> HandleSessionFsReaddirAsync(SessionFsReaddirParams request) => GetSessionFsHandler().ReaddirAsync(request);
internal Task<SessionFsReaddirWithTypesResult> HandleSessionFsReaddirWithTypesAsync(SessionFsReaddirWithTypesParams request) => GetSessionFsHandler().ReaddirWithTypesAsync(request);
internal Task HandleSessionFsRmAsync(SessionFsRmParams request) => GetSessionFsHandler().RmAsync(request);
internal Task HandleSessionFsRenameAsync(SessionFsRenameParams request) => GetSessionFsHandler().RenameAsync(request);

/// <summary>
/// Sets the capabilities reported by the host for this session.
/// </summary>
Expand Down
Loading