Compare commits
8
Commits
fc25ee45d1
...
v2.13.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c16c7b97ef | ||
|
|
cccfe2eca1 | ||
|
|
a8ec550740 | ||
|
|
40d2537486 | ||
|
|
f70503d038 | ||
|
|
9360cd2fe6 | ||
|
|
ee502883f8 | ||
|
|
e64d570554 |
@@ -1,5 +1,25 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.13.4 — 2026-09-03
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- interaktive Session ohne Arbeitsverzeichnis im User-Verzeichnis starten (f70503d0)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- update for v2.13.3 (9360cd2f)
|
||||||
|
|
||||||
|
## v2.13.3 — 2026-09-03
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- raise SignalR MaximumParallelInvocationsPerClient (e64d5705)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- update for v2.13.2 (fc25ee45)
|
||||||
|
|
||||||
## v2.13.2 — 2026-09-03
|
## v2.13.2 — 2026-09-03
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
@@ -10,8 +12,14 @@ namespace ClaudeDo.Ui.ViewModels.Modals;
|
|||||||
|
|
||||||
public sealed partial class AboutModalViewModel : ViewModelBase
|
public sealed partial class AboutModalViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
|
// MinVer sets AssemblyVersion to {Major}.0.0.0 for binding compat — Assembly.GetName().Version
|
||||||
|
// shows "2.0.0" no matter the release. FileVersion carries the real Major.Minor.Patch.
|
||||||
public string AppVersion { get; } =
|
public string AppVersion { get; } =
|
||||||
Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.0.0";
|
Version.TryParse(
|
||||||
|
FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion,
|
||||||
|
out var version)
|
||||||
|
? version.ToString(3)
|
||||||
|
: "0.0.0";
|
||||||
public string DataFolderPath { get; } = Paths.AppDataRoot();
|
public string DataFolderPath { get; } = Paths.AppDataRoot();
|
||||||
public string LogsFolderPath { get; } = Path.Combine(Paths.AppDataRoot(), "logs");
|
public string LogsFolderPath { get; } = Path.Combine(Paths.AppDataRoot(), "logs");
|
||||||
public string WorkerConfigPath { get; } = Path.Combine(Paths.AppDataRoot(), "worker.config.json");
|
public string WorkerConfigPath { get; } = Path.Combine(Paths.AppDataRoot(), "worker.config.json");
|
||||||
|
|||||||
@@ -64,8 +64,6 @@
|
|||||||
Foreground="{DynamicResource TextDimBrush}">
|
Foreground="{DynamicResource TextDimBrush}">
|
||||||
<MenuItem Header="{loc:Tr shell.menu.restartWorker}"
|
<MenuItem Header="{loc:Tr shell.menu.restartWorker}"
|
||||||
Command="{Binding RestartWorkerCommand}"/>
|
Command="{Binding RestartWorkerCommand}"/>
|
||||||
<MenuItem Header="{loc:Tr shell.menu.checkForUpdates}"
|
|
||||||
Command="{Binding CheckForUpdatesCommand}"/>
|
|
||||||
<MenuItem Header="{loc:Tr shell.menu.usageMonitor}"
|
<MenuItem Header="{loc:Tr shell.menu.usageMonitor}"
|
||||||
Command="{Binding OpenUsageMonitorCommand}"/>
|
Command="{Binding OpenUsageMonitorCommand}"/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -79,6 +77,8 @@
|
|||||||
<MenuItem Header="{loc:Tr shell.menu.help}"
|
<MenuItem Header="{loc:Tr shell.menu.help}"
|
||||||
FontSize="{StaticResource FontSizeMono}"
|
FontSize="{StaticResource FontSizeMono}"
|
||||||
Foreground="{DynamicResource TextDimBrush}">
|
Foreground="{DynamicResource TextDimBrush}">
|
||||||
|
<MenuItem Header="{loc:Tr shell.menu.checkForUpdates}"
|
||||||
|
Command="{Binding CheckForUpdatesCommand}"/>
|
||||||
<MenuItem Header="{loc:Tr shell.menu.weeklyReport}" Command="{Binding OpenWeeklyReportCommand}"/>
|
<MenuItem Header="{loc:Tr shell.menu.weeklyReport}" Command="{Binding OpenWeeklyReportCommand}"/>
|
||||||
<MenuItem Header="{loc:Tr shell.menu.about}" Command="{Binding OpenAboutCommand}"/>
|
<MenuItem Header="{loc:Tr shell.menu.about}" Command="{Binding OpenAboutCommand}"/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -78,10 +78,14 @@ builder.Services.AddHostedService<AttachmentOrphanRecovery>();
|
|||||||
builder.Services.AddHostedService<PromptFileRecovery>();
|
builder.Services.AddHostedService<PromptFileRecovery>();
|
||||||
builder.Services.AddHostedService<LegacyWorktreeFolderRecovery>();
|
builder.Services.AddHostedService<LegacyWorktreeFolderRecovery>();
|
||||||
builder.Services.AddHostedService<UntrackedWorktreeRecovery>();
|
builder.Services.AddHostedService<UntrackedWorktreeRecovery>();
|
||||||
builder.Services.AddSignalR().AddJsonProtocol(options =>
|
// Default MaximumParallelInvocationsPerClient=1 serializes every hub call on one connection —
|
||||||
{
|
// a slow one (e.g. GetTicketProjects hitting an unreachable ticket system) blocks unrelated
|
||||||
|
// calls like UpdateList/UpdateListConfig behind it for the same 10s.
|
||||||
|
builder.Services.AddSignalR(options => options.MaximumParallelInvocationsPerClient = 10)
|
||||||
|
.AddJsonProtocol(options =>
|
||||||
|
{
|
||||||
options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
|
options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Runner stack.
|
// Runner stack.
|
||||||
builder.Services.AddSingleton<IClaudeProcess, ClaudeProcess>();
|
builder.Services.AddSingleton<IClaudeProcess, ClaudeProcess>();
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ public sealed class InteractiveLaunchSpecService
|
|||||||
/// Throws KeyNotFoundException if the task doesn't exist, InvalidOperationException
|
/// Throws KeyNotFoundException if the task doesn't exist, InvalidOperationException
|
||||||
/// if it's Running/Queued. If the task has no usable worktree yet, one is created on
|
/// if it's Running/Queued. If the task has no usable worktree yet, one is created on
|
||||||
/// demand (same mechanism as an autonomous run) provided the task's list has a working
|
/// demand (same mechanism as an autonomous run) provided the task's list has a working
|
||||||
/// directory pointing at a git repo -- otherwise throws InvalidOperationException. Resumes
|
/// directory pointing at a git repo; a non-repo directory is opened directly, and a list with
|
||||||
|
/// no working directory at all (manual/reminder list) opens in the user's home dir. Resumes
|
||||||
/// (--resume) this task's own last interactive session (TaskEntity.InteractiveSessionId) if
|
/// (--resume) this task's own last interactive session (TaskEntity.InteractiveSessionId) if
|
||||||
/// it has one, else the latest autonomous run's session; a task that has never run either
|
/// it has one, else the latest autonomous run's session; a task that has never run either
|
||||||
/// way, or whose worktree was just created fresh, gets a fresh-start spec instead -- pre-
|
/// way, or whose worktree was just created fresh, gets a fresh-start spec instead -- pre-
|
||||||
@@ -94,6 +95,7 @@ public sealed class InteractiveLaunchSpecService
|
|||||||
var isFreshWorktree = false;
|
var isFreshWorktree = false;
|
||||||
string sessionDir;
|
string sessionDir;
|
||||||
bool sessionIsWorktree;
|
bool sessionIsWorktree;
|
||||||
|
var seedSkills = true;
|
||||||
|
|
||||||
if (worktree is null || worktree.State is not (WorktreeState.Active or WorktreeState.Kept))
|
if (worktree is null || worktree.State is not (WorktreeState.Active or WorktreeState.Kept))
|
||||||
{
|
{
|
||||||
@@ -105,10 +107,17 @@ public sealed class InteractiveLaunchSpecService
|
|||||||
// existing shape in the codebase (a list-handler task also has no worktree and commits
|
// existing shape in the codebase (a list-handler task also has no worktree and commits
|
||||||
// straight to the list's working dir), so no WorktreeEntity is created here either.
|
// straight to the list's working dir), so no WorktreeEntity is created here either.
|
||||||
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct);
|
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct);
|
||||||
if (list?.WorkingDir is null)
|
if (list is null || string.IsNullOrEmpty(list.WorkingDir))
|
||||||
throw new InvalidOperationException("This task has no working directory configured -- can't create a worktree.");
|
{
|
||||||
|
// A manual/reminder list has no repo at all -- open the session in the user's
|
||||||
if (await _git.IsGitRepoAsync(list.WorkingDir, ct))
|
// home directory instead of refusing it. Skills are deliberately NOT seeded
|
||||||
|
// there: the destination would be the user's REAL ~/.claude/skills, not a
|
||||||
|
// throwaway session copy.
|
||||||
|
sessionDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
|
sessionIsWorktree = false;
|
||||||
|
seedSkills = false;
|
||||||
|
}
|
||||||
|
else if (await _git.IsGitRepoAsync(list.WorkingDir, ct))
|
||||||
{
|
{
|
||||||
// A stale (Merged/Discarded) row is already tracked in `ctx`'s identity map from
|
// A stale (Merged/Discarded) row is already tracked in `ctx`'s identity map from
|
||||||
// the read above -- CreateAsync replaces it via its OWN context, so re-querying on
|
// the read above -- CreateAsync replaces it via its OWN context, so re-querying on
|
||||||
@@ -146,9 +155,12 @@ public sealed class InteractiveLaunchSpecService
|
|||||||
// history the task accumulated before its previous worktree went away.
|
// history the task accumulated before its previous worktree went away.
|
||||||
var run = isFreshWorktree ? null : await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId, ct);
|
var run = isFreshWorktree ? null : await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId, ct);
|
||||||
|
|
||||||
|
if (seedSkills)
|
||||||
|
{
|
||||||
var requestedSkills = TaskRunner.UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, globalSettings.SessionSkills);
|
var requestedSkills = TaskRunner.UnionSkillNames(task.SessionSkills, listConfig?.SessionSkills, globalSettings.SessionSkills);
|
||||||
var skillNames = await FilterToInstalledSkillsAsync(requestedSkills, ct);
|
var skillNames = await FilterToInstalledSkillsAsync(requestedSkills, ct);
|
||||||
await _skillSeeder.SeedAsync(sessionDir, skillNames, isWorktree: sessionIsWorktree, ct);
|
await _skillSeeder.SeedAsync(sessionDir, skillNames, isWorktree: sessionIsWorktree, ct);
|
||||||
|
}
|
||||||
|
|
||||||
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
||||||
?? throw new InvalidOperationException($"claude executable not found: {_claudePath}");
|
?? throw new InvalidOperationException($"claude executable not found: {_claudePath}");
|
||||||
|
|||||||
@@ -220,8 +220,15 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BuildForTaskAsync_NoWorktreeRow_NoWorkingDirConfigured_ThrowsInvalidOperation()
|
public async Task BuildForTaskAsync_NoWorktreeRow_NoWorkingDirConfigured_OpensInUserHomeWithoutSeedingSkills()
|
||||||
{
|
{
|
||||||
|
_registry.Installed.Add(new SessionSkillEntity
|
||||||
|
{
|
||||||
|
Name = "installed-skill", SourceUrl = "https://example.com/x.git",
|
||||||
|
PinnedRef = "abc", Subpath = "skills/installed-skill", Description = "d",
|
||||||
|
AddedAt = DateTimeOffset.UtcNow,
|
||||||
|
});
|
||||||
|
|
||||||
var listId = await SeedListAsync();
|
var listId = await SeedListAsync();
|
||||||
using (var ctx = _db.CreateContext())
|
using (var ctx = _db.CreateContext())
|
||||||
{
|
{
|
||||||
@@ -230,11 +237,18 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
var taskId = Guid.NewGuid().ToString();
|
var taskId = Guid.NewGuid().ToString();
|
||||||
await SeedTaskAsync(taskId, listId, TaskStatus.Idle);
|
await SeedTaskAsync(taskId, listId, TaskStatus.Idle,
|
||||||
|
sessionSkillsJson: "[\"installed-skill\"]");
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
var spec = await svc.BuildForTaskAsync(taskId, CancellationToken.None);
|
||||||
() => svc.BuildForTaskAsync(taskId, CancellationToken.None));
|
|
||||||
|
Assert.Equal(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), spec.Cwd);
|
||||||
|
// Seeding here would copy into the user's REAL ~/.claude/skills.
|
||||||
|
Assert.Empty(_seeder.Calls);
|
||||||
|
|
||||||
|
using var readCtx = _db.CreateContext();
|
||||||
|
Assert.Null(await new WorktreeRepository(readCtx).GetByTaskIdAsync(taskId));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user