-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathCopilotService.Continuation.cs
More file actions
152 lines (129 loc) · 5.79 KB
/
CopilotService.Continuation.cs
File metadata and controls
152 lines (129 loc) · 5.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
using System.Text;
using PolyPilot.Models;
namespace PolyPilot.Services;
public partial class CopilotService
{
/// <summary>
/// Creates a new session pre-filled with context from an existing session's conversation history.
/// Returns (newSessionName, transcript) so the caller can set the draft in the chat input.
/// </summary>
public async Task<(string NewSessionName, string Transcript)> ContinueInNewSessionAsync(
string sourceSessionName, CancellationToken ct = default)
{
if (!_sessions.TryGetValue(sourceSessionName, out var sourceState))
throw new InvalidOperationException($"Session '{sourceSessionName}' not found");
var info = sourceState.Info;
List<ChatMessage> history;
lock (info.HistoryLock)
{
history = info.History.ToList();
}
var transcript = BuildContinuationTranscript(history, sourceSessionName, info.SessionId);
// Inherit model, working directory, and group from source session
var groupId = Organization.Sessions.FirstOrDefault(m => m.SessionName == sourceSessionName)?.GroupId;
var newName = GenerateContinuationName(sourceSessionName, _sessions.Keys);
_ = await CreateSessionAsync(newName, info.Model, info.WorkingDirectory, ct, groupId);
return (newName, transcript);
}
/// <summary>
/// Builds a markdown transcript from conversation history, suitable for pre-filling
/// a new session's chat input. Caps at ~6000 chars, trimming oldest turns first.
/// </summary>
internal static string BuildContinuationTranscript(
List<ChatMessage> history, string sourceSessionName, string? sessionId)
{
const int maxChars = 6000;
const int assistantTruncateLen = 400;
// Build per-turn summaries
var turns = new List<string>();
foreach (var msg in history)
{
switch (msg.MessageType)
{
case ChatMessageType.User:
turns.Add($"**User:** {msg.Content}");
break;
case ChatMessageType.Assistant:
var content = msg.Content;
if (content.Length > assistantTruncateLen)
content = content[..assistantTruncateLen] + "…";
turns.Add($"**Assistant:** {content}");
break;
case ChatMessageType.ToolCall:
var status = msg.IsComplete ? (msg.IsSuccess ? "✅" : "❌") : "⏳";
var toolDisplay = msg.ToolName ?? "unknown";
turns.Add($" 🔧 {toolDisplay} {status}");
break;
case ChatMessageType.Error:
turns.Add($" ⚠️ Error: {Truncate(msg.Content, 150)}");
break;
case ChatMessageType.System:
// Skip system messages — not useful for context
break;
case ChatMessageType.Image:
turns.Add($" 🖼️ Image{(string.IsNullOrEmpty(msg.Caption) ? "" : $": {msg.Caption}")}");
break;
default:
// Reasoning, ShellOutput, Diff, Reflection — skip for brevity
break;
}
}
// Trim oldest turns to fit within budget
while (turns.Count > 2 && EstimateLength(turns, sourceSessionName, sessionId) > maxChars)
{
turns.RemoveAt(0);
}
var sb = new StringBuilder();
sb.AppendLine($"I'm continuing work from the session \"{sourceSessionName}\". Here's the conversation context:");
sb.AppendLine();
sb.AppendLine("---");
foreach (var turn in turns)
{
sb.AppendLine(turn);
}
sb.AppendLine("---");
sb.AppendLine();
sb.AppendLine("Please read the above context and continue where we left off. If you need more detail, the full session log is available at:");
if (!string.IsNullOrEmpty(sessionId))
{
var eventsPath = $"~/.copilot/session-state/{sessionId}/events.jsonl";
sb.AppendLine($"`{eventsPath}`");
}
else
{
sb.AppendLine("(session ID not available — check ~/.copilot/session-state/ for recent sessions)");
}
return sb.ToString();
}
/// <summary>
/// Generates a continuation session name. Strips existing " (cont'd)" or " (cont'd N)" suffix,
/// then appends a counter if the name already exists among active sessions.
/// </summary>
internal static string GenerateContinuationName(string sourceName, IEnumerable<string>? existingNames = null)
{
const string suffix = " (cont'd)";
// Strip existing continuation suffixes: " (cont'd)" or " (cont'd 2)", " (cont'd 3)", etc.
var baseName = sourceName;
if (baseName.EndsWith(suffix))
baseName = baseName[..^suffix.Length];
else if (System.Text.RegularExpressions.Regex.Match(baseName, @" \(cont'd \d+\)$") is { Success: true } m)
baseName = baseName[..m.Index];
var existing = existingNames != null ? new HashSet<string>(existingNames) : new HashSet<string>();
var candidate = baseName + suffix;
if (!existing.Contains(candidate))
return candidate;
for (int i = 2; i < 100; i++)
{
candidate = $"{baseName} (cont'd {i})";
if (!existing.Contains(candidate))
return candidate;
}
return $"{baseName} (cont'd {DateTime.UtcNow.Ticks})";
}
private static int EstimateLength(List<string> turns, string sourceName, string? sessionId)
{
var turnLen = 0;
foreach (var t in turns) turnLen += t.Length + 1;
return 120 + turnLen + 200;
}
}