Public Access
Make meeting lifecycle stateful
This commit is contained in:
@@ -15,6 +15,8 @@ public sealed class MeetingSummaryTools
|
||||
private readonly MeetingSessionArtifacts artifacts;
|
||||
private readonly MeetingAssistantOptions options;
|
||||
private readonly IDictationWordStore? dictationWordStore;
|
||||
private readonly SummaryAgentWriteAudit? writeAudit;
|
||||
private readonly BoundMeetingProjectResolver projectResolver;
|
||||
|
||||
public MeetingSummaryTools(MeetingSessionArtifacts artifacts)
|
||||
: this(artifacts, new MeetingAssistantOptions(), null)
|
||||
@@ -24,11 +26,14 @@ public sealed class MeetingSummaryTools
|
||||
public MeetingSummaryTools(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
MeetingAssistantOptions options,
|
||||
IDictationWordStore? dictationWordStore = null)
|
||||
IDictationWordStore? dictationWordStore = null,
|
||||
SummaryAgentWriteAudit? writeAudit = null)
|
||||
{
|
||||
this.artifacts = artifacts;
|
||||
this.options = options;
|
||||
this.dictationWordStore = dictationWordStore;
|
||||
this.writeAudit = writeAudit;
|
||||
projectResolver = new BoundMeetingProjectResolver(options);
|
||||
}
|
||||
|
||||
public Task<string> ReadTranscript(int? @from = null, int? to = null)
|
||||
@@ -76,7 +81,16 @@ public sealed class MeetingSummaryTools
|
||||
return "Refused: word must not be empty.";
|
||||
}
|
||||
|
||||
var words = await dictationWordStore.AddWordAsync(word, CancellationToken.None);
|
||||
if (writeAudit is not null)
|
||||
{
|
||||
IReadOnlyList<string>? capturedWords = null;
|
||||
await writeAudit.CaptureFileWriteAsync(
|
||||
VaultPath.Resolve(options.Vault, options.Vault.DictationWordsPath),
|
||||
async () => capturedWords = await dictationWordStore.AddWordAsync(word, options, CancellationToken.None));
|
||||
return $"Added '{word.Trim()}'. Dictionary now contains {capturedWords?.Count ?? 0} word(s).";
|
||||
}
|
||||
|
||||
var words = await dictationWordStore.AddWordAsync(word, options, CancellationToken.None);
|
||||
return $"Added '{word.Trim()}'. Dictionary now contains {words.Count} word(s).";
|
||||
}
|
||||
|
||||
@@ -176,7 +190,17 @@ public sealed class MeetingSummaryTools
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(target.Path)!);
|
||||
await WriteProjectFileContentAsync(target.Path, content, editMode);
|
||||
if (writeAudit is not null)
|
||||
{
|
||||
await writeAudit.CaptureFileWriteAsync(
|
||||
target.Path,
|
||||
() => WriteProjectFileContentAsync(target.Path, content, editMode));
|
||||
}
|
||||
else
|
||||
{
|
||||
await WriteProjectFileContentAsync(target.Path, content, editMode);
|
||||
}
|
||||
|
||||
return $"{target.Project.Name}/{ToToolPath(path)}";
|
||||
}
|
||||
|
||||
@@ -237,7 +261,7 @@ public sealed class MeetingSummaryTools
|
||||
return new MeetingNote(artifacts.MeetingNotePath, frontmatter, await ReadUserNotes());
|
||||
}
|
||||
|
||||
private async Task<List<ProjectFolder>> GetSearchProjectsAsync(string[]? projects)
|
||||
private async Task<List<BoundMeetingProject>> GetSearchProjectsAsync(string[]? projects)
|
||||
{
|
||||
var boundProjects = await GetBoundProjectsAsync();
|
||||
if (projects is null || projects.Length == 0)
|
||||
@@ -253,7 +277,7 @@ public sealed class MeetingSummaryTools
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<ProjectFolder?> ResolveBoundProjectAsync(string project)
|
||||
private async Task<BoundMeetingProject?> ResolveBoundProjectAsync(string project)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(project))
|
||||
{
|
||||
@@ -297,7 +321,7 @@ public sealed class MeetingSummaryTools
|
||||
}
|
||||
|
||||
var projectFolder = Directory.EnumerateDirectories(projectsRoot)
|
||||
.Select(candidate => new ProjectFolder(Path.GetFileName(candidate), candidate))
|
||||
.Select(candidate => new BoundMeetingProject(Path.GetFileName(candidate), candidate))
|
||||
.FirstOrDefault(candidate => string.Equals(candidate.Name, project, StringComparison.OrdinalIgnoreCase));
|
||||
if (projectFolder is null)
|
||||
{
|
||||
@@ -414,56 +438,19 @@ public sealed class MeetingSummaryTools
|
||||
return string.Join('\n', lines);
|
||||
}
|
||||
|
||||
private async Task<List<ProjectFolder>> GetBoundProjectsAsync()
|
||||
private Task<List<BoundMeetingProject>> GetBoundProjectsAsync()
|
||||
{
|
||||
var projectsRoot = GetProjectsRoot();
|
||||
if (!Directory.Exists(projectsRoot))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var projectNames = await ReadMeetingProjectNamesAsync();
|
||||
if (projectNames.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return Directory.EnumerateDirectories(projectsRoot)
|
||||
.Select(path => new ProjectFolder(Path.GetFileName(path), path))
|
||||
.Where(project => projectNames.Contains(project.Name))
|
||||
.OrderBy(project => project.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<HashSet<string>> ReadMeetingProjectNamesAsync()
|
||||
{
|
||||
if (!File.Exists(artifacts.MeetingNotePath))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var content = await File.ReadAllTextAsync(artifacts.MeetingNotePath);
|
||||
var document = MarkdownDocumentParser.SplitOptional(content);
|
||||
if (!document.HasFrontmatter)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var note = YamlDeserializer.Deserialize<ProjectFrontmatter>(document.Frontmatter) ?? new ProjectFrontmatter();
|
||||
return (note.Projects ?? [])
|
||||
.Where(project => !string.IsNullOrWhiteSpace(project))
|
||||
.Select(project => project.Trim())
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
return projectResolver.GetBoundProjectsAsync(artifacts);
|
||||
}
|
||||
|
||||
private string GetProjectsRoot()
|
||||
{
|
||||
return VaultPath.Resolve(options.Vault, options.Vault.ProjectsFolder);
|
||||
return projectResolver.ProjectsRoot;
|
||||
}
|
||||
|
||||
private static async Task<string?> RunRipgrepAsync(
|
||||
string projectsRoot,
|
||||
IReadOnlyList<ProjectFolder> projects,
|
||||
IReadOnlyList<BoundMeetingProject> projects,
|
||||
string keywords)
|
||||
{
|
||||
try
|
||||
@@ -528,7 +515,7 @@ public sealed class MeetingSummaryTools
|
||||
return string.Join('\n', matches);
|
||||
}
|
||||
|
||||
private static string SearchWithRegexFallback(IReadOnlyList<ProjectFolder> projects, string keywords, bool singleProject)
|
||||
private static string SearchWithRegexFallback(IReadOnlyList<BoundMeetingProject> projects, string keywords, bool singleProject)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -584,9 +571,7 @@ public sealed class MeetingSummaryTools
|
||||
.Replace(Path.AltDirectorySeparatorChar.ToString(), "/", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private sealed record ProjectFolder(string Name, string Path);
|
||||
|
||||
private sealed record ProjectFileTarget(ProjectFolder Project, string Path);
|
||||
private sealed record ProjectFileTarget(BoundMeetingProject Project, string Path);
|
||||
|
||||
private sealed record FileLineEditMode(
|
||||
FileLineEditKind Kind,
|
||||
@@ -621,12 +606,6 @@ public sealed class MeetingSummaryTools
|
||||
Insert
|
||||
}
|
||||
|
||||
private sealed class ProjectFrontmatter
|
||||
{
|
||||
[YamlDotNet.Serialization.YamlMember(Alias = "projects")]
|
||||
public List<string>? Projects { get; set; }
|
||||
}
|
||||
|
||||
private sealed class MeetingNoteFrontmatterYaml
|
||||
{
|
||||
[YamlDotNet.Serialization.YamlMember(Alias = "title")]
|
||||
|
||||
Reference in New Issue
Block a user