some gui implementations and fixed a gitea issue.
This commit is contained in:
parent
2c5493f249
commit
6f4ddecf44
65
.github/workflows/reliability-matrix.yml
vendored
65
.github/workflows/reliability-matrix.yml
vendored
@ -1,65 +0,0 @@
|
||||
name: reliability-matrix
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
matrix:
|
||||
name: ${{ matrix.os }} / .NET tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore DevTool.csproj
|
||||
|
||||
- name: Build
|
||||
run: dotnet build DevTool.csproj -c Release --no-restore
|
||||
|
||||
- name: Verify workflow routes (static)
|
||||
run: python scripts/verify-workflow-routes.py --project-root .
|
||||
|
||||
- name: Test
|
||||
run: dotnet test tests/DevTool.Tests/DevTool.Tests.csproj -c Release --no-build --logger "trx;LogFileName=test-results.trx"
|
||||
|
||||
- name: Generate matrix summary
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path artifacts | Out-Null
|
||||
@"
|
||||
{
|
||||
"os": "${{ matrix.os }}",
|
||||
"commit": "${{ github.sha }}",
|
||||
"workflow": "${{ github.workflow }}",
|
||||
"runId": "${{ github.run_id }}",
|
||||
"runNumber": "${{ github.run_number }}",
|
||||
"status": "passed"
|
||||
}
|
||||
"@ | Set-Content artifacts/reliability-${{ matrix.os }}.json -Encoding UTF8
|
||||
|
||||
- name: Upload test artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: reliability-${{ matrix.os }}
|
||||
path: |
|
||||
**/test-results.trx
|
||||
artifacts/reliability-${{ matrix.os }}.json
|
||||
13
ROADMAP.md
13
ROADMAP.md
@ -52,13 +52,24 @@
|
||||
- [x] GUI parity phase 1 bridge foundation shipped (`sdt bridge --stdio` + `DevTool.Host.Bridge`)
|
||||
- [x] GUI debug + failure UX slice shipped (debug run, failure card, run context/lifecycle panel)
|
||||
- [x] GUI history/events/env/doctor/setup-plan read views shipped via bridge methods
|
||||
- [ ] GUI workspace/favorites polish (switching UX + quick action ergonomics) still in progress
|
||||
- [x] GUI workspace project switcher shipped (configured projects -> set active context)
|
||||
- [x] GUI favorite quick action runner shipped (run favorite directly from workspace panel)
|
||||
- [x] GUI workspace/favorites search/filter/sort ergonomics shipped
|
||||
- [x] GUI "Switch + Run" one-click action shipped from configured projects list
|
||||
- [x] GUI view preferences persisted (workspace-scoped filters/sorts/context defaults)
|
||||
- [x] GUI workspace/favorites advanced grouping shipped (tool/path + project/workflow views)
|
||||
- [x] GUI workspace/favorites bulk actions shipped (bulk run filtered projects/favorites, bulk remove filtered favorites)
|
||||
- [x] GUI setup actions shipped (apply autofix dirs, apply legacy migration, apply recommended config with backup)
|
||||
- [x] GUI keyboard refinements shipped (`Ctrl+K` command palette, `?` shortcut help)
|
||||
- [x] GUI env profile editor shipped (select/load, set-active, create/update profile values)
|
||||
- [x] Create dedicated `src/DevTool.Host.Gui/TauriShell` scaffold to keep GUI work isolated from TUI/core
|
||||
- [x] Bootstrap first Tauri command bridge: `sdt workspace scan --json`
|
||||
- [x] Structural refactor to domain-separated projects under `src/` (`DevTool.Engine`, `DevTool.Runtime`, `DevTool.Host.Tui`)
|
||||
- [x] Add second Tauri bridge command: `sdt run <workflowId> --json` with live stream panel
|
||||
- [ ] Remove legacy PowerShell wrappers in v2
|
||||
- [x] Add workspace project inventory model (all `.slnx/.sln/.csproj`) for GUI/TUI multi-project selector
|
||||
- [x] Expand GUI command palette coverage across workspace/run/setup/history/events/favorites actions
|
||||
- [x] Add full GUI env var definition editor parity (`env[]` model editing with validation)
|
||||
|
||||
## Next Sprint (v1.3 UX Foundation)
|
||||
|
||||
|
||||
@ -36,13 +36,21 @@ Error response:
|
||||
- `workspace.add` (`candidatePath`, `initializeConfig`)
|
||||
- `favorites.list`
|
||||
- `favorites.toggle` (`favoriteProjectPath`, `workflowId`, `label`)
|
||||
- `favorites.removeMany` (`items[]` with `projectPath`, `workflowId`)
|
||||
- `history.list` (`limit`)
|
||||
- `events.listFiles`
|
||||
- `events.readFile` (`filePath`)
|
||||
- `envProfiles.list`
|
||||
- `envProfiles.resolve` (`envProfile`)
|
||||
- `envProfiles.setActive` (`profileId`)
|
||||
- `envProfiles.saveProfile` (`profile` object with `id`, `description`, `inherits[]`, `values{}`)
|
||||
- `envVars.list`
|
||||
- `envVars.save` (`env[]` array of env var definitions)
|
||||
- `doctor.run`
|
||||
- `setup.plan` (read-only preview)
|
||||
- `setup.autofixDirs` (apply missing directory fixes)
|
||||
- `setup.migrateLegacy` (apply targets->workflows migration)
|
||||
- `setup.applyRecommendedConfig` (apply recommended config updates with backup)
|
||||
|
||||
## Determinism Notes
|
||||
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"updatedAtUtc": "2026-03-02T01:00:00Z",
|
||||
"updatedAtUtc": "2026-03-02T02:00:00Z",
|
||||
"features": [
|
||||
{
|
||||
"id": "workspace.switch_and_candidates",
|
||||
"tui": true,
|
||||
"gui": true,
|
||||
"status": "in_progress",
|
||||
"status": "done",
|
||||
"owner": "bridge",
|
||||
"notes": "GUI can load configured + candidate projects and add/add+init candidates through workspace bridge."
|
||||
"notes": "GUI can load configured + candidate projects, add/add+init candidates, switch context, group/filter, and bulk run filtered projects."
|
||||
},
|
||||
{
|
||||
"id": "workflow.run",
|
||||
@ -54,17 +54,33 @@
|
||||
"id": "favorites.quick_actions",
|
||||
"tui": true,
|
||||
"gui": true,
|
||||
"status": "in_progress",
|
||||
"status": "done",
|
||||
"owner": "bridge",
|
||||
"notes": "Bridge exposes favorites list/toggle; richer quick-action UI still pending."
|
||||
"notes": "GUI supports favorites run/toggle, grouping/filtering, bulk run filtered favorites, and bulk remove via bridge."
|
||||
},
|
||||
{
|
||||
"id": "setup_wizard_autofix",
|
||||
"tui": true,
|
||||
"gui": false,
|
||||
"status": "planned",
|
||||
"gui": true,
|
||||
"status": "in_progress",
|
||||
"owner": "gui",
|
||||
"notes": "GUI currently exposes doctor + setup plan preview only."
|
||||
"notes": "GUI now supports doctor/setup plan plus apply flows for dir autofix, legacy migration, and recommended config updates."
|
||||
},
|
||||
{
|
||||
"id": "env_management_profile_editor",
|
||||
"tui": true,
|
||||
"gui": true,
|
||||
"status": "done",
|
||||
"owner": "gui",
|
||||
"notes": "GUI supports env profile selector/load, set-active, create/update profile values, and full env[] definition editing with validation."
|
||||
},
|
||||
{
|
||||
"id": "command_palette_and_shortcuts",
|
||||
"tui": true,
|
||||
"gui": true,
|
||||
"status": "done",
|
||||
"owner": "gui",
|
||||
"notes": "GUI provides Ctrl+K command palette and ? shortcut help, including dynamic workspace switch and favorites run commands plus setup/doctor/history/events actions."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -74,13 +74,21 @@ public sealed class BridgeStdioServer
|
||||
"workspace.add" => Ok(request.Id, HandleWorkspaceAdd(request.Params)),
|
||||
"favorites.list" => Ok(request.Id, HandleFavoritesList(request.Params)),
|
||||
"favorites.toggle" => Ok(request.Id, HandleFavoritesToggle(request.Params)),
|
||||
"favorites.removeMany" => Ok(request.Id, HandleFavoritesRemoveMany(request.Params)),
|
||||
"history.list" => Ok(request.Id, HandleHistoryList(request.Params)),
|
||||
"events.listFiles" => Ok(request.Id, HandleEventsListFiles(request.Params)),
|
||||
"events.readFile" => Ok(request.Id, HandleEventsReadFile(request.Params)),
|
||||
"envProfiles.list" => Ok(request.Id, HandleEnvProfilesList(request.Params)),
|
||||
"envProfiles.resolve" => Ok(request.Id, HandleEnvProfilesResolve(request.Params)),
|
||||
"envProfiles.setActive" => Ok(request.Id, HandleEnvProfilesSetActive(request.Params)),
|
||||
"envProfiles.saveProfile" => Ok(request.Id, HandleEnvProfilesSaveProfile(request.Params)),
|
||||
"envVars.list" => Ok(request.Id, HandleEnvVarsList(request.Params)),
|
||||
"envVars.save" => Ok(request.Id, HandleEnvVarsSave(request.Params)),
|
||||
"doctor.run" => Ok(request.Id, await HandleDoctorRunAsync(request.Params, cancellationToken).ConfigureAwait(false)),
|
||||
"setup.plan" => Ok(request.Id, HandleSetupPlan(request.Params)),
|
||||
"setup.autofixDirs" => Ok(request.Id, HandleSetupAutofixDirs(request.Params)),
|
||||
"setup.migrateLegacy" => Ok(request.Id, HandleSetupMigrateLegacy(request.Params)),
|
||||
"setup.applyRecommendedConfig" => Ok(request.Id, HandleSetupApplyRecommendedConfig(request.Params)),
|
||||
_ => Error(request.Id, "method_not_found", $"Unsupported bridge method: {request.Method}")
|
||||
};
|
||||
}
|
||||
@ -222,6 +230,48 @@ public sealed class BridgeStdioServer
|
||||
return HandleFavoritesList(@params);
|
||||
}
|
||||
|
||||
private object HandleFavoritesRemoveMany(JsonElement @params)
|
||||
{
|
||||
var startDir = GetString(@params, "projectRoot") ?? _startupProjectRoot ?? Directory.GetCurrentDirectory();
|
||||
var loaded = WorkspaceLoader.FindAndLoad(startDir) ?? throw new BridgeValidationException("No workspace configuration found.");
|
||||
var (workspace, workspaceRoot) = loaded;
|
||||
|
||||
if (@params.ValueKind != JsonValueKind.Object ||
|
||||
!@params.TryGetProperty("items", out var itemsProp) ||
|
||||
itemsProp.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw new BridgeValidationException("Missing required parameter 'items'.");
|
||||
}
|
||||
|
||||
var items = new List<(string ProjectPath, string WorkflowId)>();
|
||||
foreach (var item in itemsProp.EnumerateArray())
|
||||
{
|
||||
var path = item.TryGetProperty("projectPath", out var pp) && pp.ValueKind == JsonValueKind.String
|
||||
? pp.GetString()
|
||||
: null;
|
||||
var workflowId = item.TryGetProperty("workflowId", out var wf) && wf.ValueKind == JsonValueKind.String
|
||||
? wf.GetString()
|
||||
: null;
|
||||
if (string.IsNullOrWhiteSpace(path) || string.IsNullOrWhiteSpace(workflowId))
|
||||
continue;
|
||||
items.Add((Path.GetFullPath(path), workflowId!));
|
||||
}
|
||||
|
||||
if (items.Count == 0)
|
||||
return HandleFavoritesList(@params);
|
||||
|
||||
workspace.Favorites.RemoveAll(f =>
|
||||
{
|
||||
var resolved = WorkspaceLoader.ResolveFavoriteProjectRoot(workspaceRoot, f);
|
||||
return items.Any(i =>
|
||||
string.Equals(i.ProjectPath, resolved, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(i.WorkflowId, f.WorkflowId, StringComparison.OrdinalIgnoreCase));
|
||||
});
|
||||
|
||||
WorkspaceLoader.Save(workspaceRoot, workspace);
|
||||
return HandleFavoritesList(@params);
|
||||
}
|
||||
|
||||
private object HandleHistoryList(JsonElement @params)
|
||||
{
|
||||
var projectRoot = ResolveProjectRootForProjectScopedMethod(@params);
|
||||
@ -270,6 +320,150 @@ public sealed class BridgeStdioServer
|
||||
};
|
||||
}
|
||||
|
||||
private object HandleEnvProfilesSetActive(JsonElement @params)
|
||||
{
|
||||
var loaded = LoadProject(@params);
|
||||
var profileId = GetRequiredString(@params, "profileId");
|
||||
var envProfiles = loaded.Config.EnvProfiles ?? new EnvProfilesConfig
|
||||
{
|
||||
Active = profileId,
|
||||
Profiles = []
|
||||
};
|
||||
|
||||
var profileExists = envProfiles.Profiles.Any(p =>
|
||||
string.Equals(p.Id, profileId, StringComparison.OrdinalIgnoreCase));
|
||||
if (!profileExists)
|
||||
throw new BridgeValidationException($"Profile '{profileId}' does not exist.");
|
||||
|
||||
var updated = new DevToolConfig
|
||||
{
|
||||
Name = loaded.Config.Name,
|
||||
Version = loaded.Config.Version,
|
||||
Targets = loaded.Config.Targets,
|
||||
Workflows = loaded.Config.Workflows,
|
||||
Env = loaded.Config.Env,
|
||||
EnvProfiles = new EnvProfilesConfig
|
||||
{
|
||||
Active = profileId,
|
||||
Profiles = envProfiles.Profiles
|
||||
},
|
||||
Toolchains = loaded.Config.Toolchains,
|
||||
Tooling = loaded.Config.Tooling,
|
||||
Project = loaded.Config.Project,
|
||||
Debug = loaded.Config.Debug
|
||||
};
|
||||
|
||||
var save = SaveConfigWithBackup(loaded.ProjectRoot, updated);
|
||||
return new
|
||||
{
|
||||
projectRoot = loaded.ProjectRoot,
|
||||
profileId,
|
||||
save,
|
||||
envProfiles = new
|
||||
{
|
||||
active = profileId,
|
||||
profiles = envProfiles.Profiles
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private object HandleEnvProfilesSaveProfile(JsonElement @params)
|
||||
{
|
||||
var loaded = LoadProject(@params);
|
||||
if (@params.ValueKind != JsonValueKind.Object ||
|
||||
!@params.TryGetProperty("profile", out var profileProp) ||
|
||||
profileProp.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw new BridgeValidationException("Missing required parameter 'profile'.");
|
||||
}
|
||||
|
||||
var id = profileProp.TryGetProperty("id", out var idProp) && idProp.ValueKind == JsonValueKind.String
|
||||
? idProp.GetString()
|
||||
: null;
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
throw new BridgeValidationException("Profile id is required.");
|
||||
|
||||
var description = profileProp.TryGetProperty("description", out var dProp) && dProp.ValueKind == JsonValueKind.String
|
||||
? dProp.GetString() ?? ""
|
||||
: "";
|
||||
|
||||
var inherits = new List<string>();
|
||||
if (profileProp.TryGetProperty("inherits", out var inhProp) && inhProp.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var entry in inhProp.EnumerateArray())
|
||||
{
|
||||
if (entry.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var value = entry.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
inherits.Add(value!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (profileProp.TryGetProperty("values", out var valuesProp) && valuesProp.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var kvp in valuesProp.EnumerateObject())
|
||||
{
|
||||
if (kvp.Value.ValueKind == JsonValueKind.String)
|
||||
values[kvp.Name] = kvp.Value.GetString() ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
var envProfiles = loaded.Config.EnvProfiles ?? new EnvProfilesConfig
|
||||
{
|
||||
Active = id!,
|
||||
Profiles = []
|
||||
};
|
||||
var profiles = envProfiles.Profiles.ToList();
|
||||
var existingIndex = profiles.FindIndex(p => string.Equals(p.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
var newProfile = new EnvProfileDefinition
|
||||
{
|
||||
Id = id!,
|
||||
Description = description,
|
||||
Inherits = inherits,
|
||||
Values = values,
|
||||
};
|
||||
|
||||
if (existingIndex >= 0)
|
||||
profiles[existingIndex] = newProfile;
|
||||
else
|
||||
profiles.Add(newProfile);
|
||||
|
||||
var active = string.IsNullOrWhiteSpace(envProfiles.Active) ? id! : envProfiles.Active;
|
||||
var updated = new DevToolConfig
|
||||
{
|
||||
Name = loaded.Config.Name,
|
||||
Version = loaded.Config.Version,
|
||||
Targets = loaded.Config.Targets,
|
||||
Workflows = loaded.Config.Workflows,
|
||||
Env = loaded.Config.Env,
|
||||
EnvProfiles = new EnvProfilesConfig
|
||||
{
|
||||
Active = active,
|
||||
Profiles = profiles
|
||||
},
|
||||
Toolchains = loaded.Config.Toolchains,
|
||||
Tooling = loaded.Config.Tooling,
|
||||
Project = loaded.Config.Project,
|
||||
Debug = loaded.Config.Debug
|
||||
};
|
||||
|
||||
var save = SaveConfigWithBackup(loaded.ProjectRoot, updated);
|
||||
return new
|
||||
{
|
||||
projectRoot = loaded.ProjectRoot,
|
||||
profile = newProfile,
|
||||
save,
|
||||
envProfiles = new
|
||||
{
|
||||
active,
|
||||
profiles
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<object> HandleDoctorRunAsync(JsonElement @params, CancellationToken cancellationToken)
|
||||
{
|
||||
var loaded = LoadProject(@params);
|
||||
@ -303,6 +497,138 @@ public sealed class BridgeStdioServer
|
||||
};
|
||||
}
|
||||
|
||||
private object HandleEnvVarsList(JsonElement @params)
|
||||
{
|
||||
var loaded = LoadProject(@params);
|
||||
return new
|
||||
{
|
||||
projectRoot = loaded.ProjectRoot,
|
||||
env = loaded.Config.Env
|
||||
};
|
||||
}
|
||||
|
||||
private object HandleEnvVarsSave(JsonElement @params)
|
||||
{
|
||||
var loaded = LoadProject(@params);
|
||||
if (@params.ValueKind != JsonValueKind.Object ||
|
||||
!@params.TryGetProperty("env", out var envProp) ||
|
||||
envProp.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw new BridgeValidationException("Missing required parameter 'env'.");
|
||||
}
|
||||
|
||||
var env = new List<EnvVarDef>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var item in envProp.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind != JsonValueKind.Object)
|
||||
continue;
|
||||
|
||||
var key = item.TryGetProperty("key", out var keyProp) && keyProp.ValueKind == JsonValueKind.String
|
||||
? keyProp.GetString()
|
||||
: null;
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new BridgeValidationException("Each env var requires a non-empty 'key'.");
|
||||
if (!seen.Add(key!))
|
||||
throw new BridgeValidationException($"Duplicate env key detected: '{key}'.");
|
||||
|
||||
var description = item.TryGetProperty("description", out var descProp) && descProp.ValueKind == JsonValueKind.String
|
||||
? descProp.GetString() ?? ""
|
||||
: "";
|
||||
var defaultValue = item.TryGetProperty("default", out var defProp) && defProp.ValueKind == JsonValueKind.String
|
||||
? defProp.GetString() ?? ""
|
||||
: "";
|
||||
var options = new List<string>();
|
||||
if (item.TryGetProperty("options", out var optionsProp) && optionsProp.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var opt in optionsProp.EnumerateArray())
|
||||
{
|
||||
if (opt.ValueKind == JsonValueKind.String)
|
||||
options.Add(opt.GetString() ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
env.Add(new EnvVarDef
|
||||
{
|
||||
Key = key!,
|
||||
Description = description,
|
||||
DefaultValue = defaultValue,
|
||||
Options = options
|
||||
});
|
||||
}
|
||||
|
||||
var updated = new DevToolConfig
|
||||
{
|
||||
Name = loaded.Config.Name,
|
||||
Version = loaded.Config.Version,
|
||||
Targets = loaded.Config.Targets,
|
||||
Workflows = loaded.Config.Workflows,
|
||||
Env = env,
|
||||
EnvProfiles = loaded.Config.EnvProfiles,
|
||||
Toolchains = loaded.Config.Toolchains,
|
||||
Tooling = loaded.Config.Tooling,
|
||||
Project = loaded.Config.Project,
|
||||
Debug = loaded.Config.Debug
|
||||
};
|
||||
|
||||
var save = SaveConfigWithBackup(loaded.ProjectRoot, updated);
|
||||
return new
|
||||
{
|
||||
projectRoot = loaded.ProjectRoot,
|
||||
env,
|
||||
save
|
||||
};
|
||||
}
|
||||
|
||||
private object HandleSetupAutofixDirs(JsonElement @params)
|
||||
{
|
||||
var loaded = LoadProject(@params);
|
||||
var missingDirs = _doctorFixes.FindMissingWorkingDirectories(loaded.Config, loaded.ProjectRoot);
|
||||
var result = _doctorFixes.CreateMissingWorkingDirectories(missingDirs);
|
||||
return new
|
||||
{
|
||||
projectRoot = loaded.ProjectRoot,
|
||||
missingDirectories = missingDirs,
|
||||
result
|
||||
};
|
||||
}
|
||||
|
||||
private object HandleSetupMigrateLegacy(JsonElement @params)
|
||||
{
|
||||
var loaded = LoadProject(@params);
|
||||
var result = _doctorFixes.ApplyLegacyMigration(loaded.ProjectRoot);
|
||||
return new
|
||||
{
|
||||
projectRoot = loaded.ProjectRoot,
|
||||
result
|
||||
};
|
||||
}
|
||||
|
||||
private object HandleSetupApplyRecommendedConfig(JsonElement @params)
|
||||
{
|
||||
var loaded = LoadProject(@params);
|
||||
var update = _setupConfigService.ApplyRecommendedDefaults(loaded.Config);
|
||||
if (update.Changes.Count == 0)
|
||||
{
|
||||
return new
|
||||
{
|
||||
projectRoot = loaded.ProjectRoot,
|
||||
changes = update.Changes,
|
||||
applied = false,
|
||||
message = "No recommended config changes were needed."
|
||||
};
|
||||
}
|
||||
|
||||
var save = SaveConfigWithBackup(loaded.ProjectRoot, update.Config);
|
||||
return new
|
||||
{
|
||||
projectRoot = loaded.ProjectRoot,
|
||||
changes = update.Changes,
|
||||
applied = save.Success,
|
||||
save
|
||||
};
|
||||
}
|
||||
|
||||
private LoadedProjectConfig LoadProject(JsonElement @params)
|
||||
{
|
||||
var startDir = GetString(@params, "projectRoot") ?? _startupProjectRoot ?? Directory.GetCurrentDirectory();
|
||||
@ -351,6 +677,25 @@ public sealed class BridgeStdioServer
|
||||
|
||||
private static BridgeResponseEnvelope Error(string? id, string code, string message, object? details = null) =>
|
||||
new(id, false, null, new BridgeErrorEnvelope(code, message, details));
|
||||
|
||||
private static LegacyMigrationApplyResult SaveConfigWithBackup(string projectRoot, DevToolConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = ConfigLoader.FindConfigPath(projectRoot);
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return new LegacyMigrationApplyResult(false, "Could not find devtool.json for saving.");
|
||||
|
||||
var backup = path + $".bak-{DateTimeOffset.Now:yyyyMMdd-HHmmss}";
|
||||
File.Copy(path, backup, overwrite: false);
|
||||
File.WriteAllText(path, ConfigBootstrapper.ToJson(config));
|
||||
return new LegacyMigrationApplyResult(true, "Saved updated devtool.json.", BackupPath: backup, ConfigPath: path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new LegacyMigrationApplyResult(false, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class BridgeValidationException(string message) : Exception(message);
|
||||
|
||||
@ -34,5 +34,10 @@ npm run tauri dev
|
||||
- workflow + debug run
|
||||
- failure card rendering from summary payload
|
||||
- run context + lifecycle panel
|
||||
- workspace load/add/add+init
|
||||
- workspace load/add/add+init + project context switcher
|
||||
- favorite quick-action runner ("Run Favorite")
|
||||
- run history + events viewer
|
||||
- workspace/favorites grouping + bulk actions
|
||||
- setup apply actions (autofix dirs, legacy migration, recommended config)
|
||||
- keyboard UX (`Ctrl+K` command palette, `?` shortcut help)
|
||||
- env management editor (`envProfiles` + grid-based `env[]` definitions editor)
|
||||
|
||||
@ -45,6 +45,45 @@
|
||||
<input id="favorite-label" placeholder="Quick Build" autocomplete="off" />
|
||||
<button id="toggle-favorite-btn" type="button">Toggle Favorite</button>
|
||||
</div>
|
||||
<h3>Workspace Controls</h3>
|
||||
<div class="row">
|
||||
<input id="project-filter" placeholder="Search configured projects..." autocomplete="off" />
|
||||
<select id="project-sort">
|
||||
<option value="name">Sort: name</option>
|
||||
<option value="path">Sort: path</option>
|
||||
</select>
|
||||
<select id="project-group">
|
||||
<option value="tool">Group: tool family</option>
|
||||
<option value="path">Group: top folder</option>
|
||||
<option value="none">Group: none</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input id="switch-run-workflow" placeholder="workflow id for Switch + Run (e.g. build)" autocomplete="off" />
|
||||
<button id="bulk-run-projects-btn" type="button">Run Filtered Projects</button>
|
||||
</div>
|
||||
<h3>Configured Projects (switch context)</h3>
|
||||
<div id="configured-projects" class="list"></div>
|
||||
<h3>Favorites Controls</h3>
|
||||
<div class="row">
|
||||
<input id="favorite-filter" placeholder="Search favorites..." autocomplete="off" />
|
||||
<select id="favorite-sort">
|
||||
<option value="label">Sort: label</option>
|
||||
<option value="workflow">Sort: workflow</option>
|
||||
<option value="project">Sort: project</option>
|
||||
</select>
|
||||
<select id="favorite-group">
|
||||
<option value="project">Group: project</option>
|
||||
<option value="workflow">Group: workflow</option>
|
||||
<option value="none">Group: none</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="bulk-run-favorites-btn" type="button">Run Filtered Favorites</button>
|
||||
<button id="bulk-remove-favorites-btn" type="button">Remove Filtered Favorites</button>
|
||||
</div>
|
||||
<h3>Favorites (run now)</h3>
|
||||
<div id="favorite-actions" class="list"></div>
|
||||
<pre id="workspace-output"></pre>
|
||||
</section>
|
||||
|
||||
@ -98,15 +137,80 @@
|
||||
<button id="load-doctor-btn" type="button">Run Doctor</button>
|
||||
<button id="load-setup-plan-btn" type="button">Setup Plan (read-only)</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="apply-autofix-dirs-btn" type="button">Apply Autofix: Missing Dirs</button>
|
||||
<button id="apply-migrate-legacy-btn" type="button">Apply Legacy Migration</button>
|
||||
<button id="apply-recommended-config-btn" type="button">Apply Recommended Config</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input id="env-resolve-id" placeholder="env profile id (optional)" autocomplete="off" />
|
||||
<button id="load-env-btn" type="button">Load Env Profiles + Resolve</button>
|
||||
</div>
|
||||
<h3>Environment</h3>
|
||||
<pre id="env-output"></pre>
|
||||
<h3>Env Profile Editor</h3>
|
||||
<div class="stack">
|
||||
<div class="row">
|
||||
<select id="env-profile-select"></select>
|
||||
<button id="env-load-profile-btn" type="button">Load Profile</button>
|
||||
<button id="env-set-active-btn" type="button">Set Active</button>
|
||||
</div>
|
||||
<label for="env-profile-id">Profile Id</label>
|
||||
<input id="env-profile-id" placeholder="dev" autocomplete="off" />
|
||||
<label for="env-profile-desc">Description</label>
|
||||
<input id="env-profile-desc" placeholder="Local development defaults" autocomplete="off" />
|
||||
<label for="env-profile-inherits">Inherits (comma separated)</label>
|
||||
<input id="env-profile-inherits" placeholder="dev,ci" autocomplete="off" />
|
||||
<label for="env-profile-values">Values (KEY=VALUE per line)</label>
|
||||
<textarea id="env-profile-values" rows="8"></textarea>
|
||||
<div class="row">
|
||||
<button id="env-save-profile-btn" type="button">Save Profile</button>
|
||||
<button id="env-new-profile-btn" type="button">New Profile Draft</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Env Var Definitions Editor</h3>
|
||||
<div class="stack">
|
||||
<p>Format: <code>KEY|description|default|option1,option2</code> (one per line)</p>
|
||||
<div class="row">
|
||||
<button id="env-vars-add-row-btn" type="button">Add Row</button>
|
||||
<button id="env-vars-sync-to-advanced-btn" type="button">Sync Grid → Advanced</button>
|
||||
<button id="env-vars-sync-from-advanced-btn" type="button">Sync Advanced → Grid</button>
|
||||
<button id="env-vars-toggle-advanced-btn" type="button">Toggle Advanced Mode</button>
|
||||
</div>
|
||||
<div id="env-vars-grid" class="env-grid"></div>
|
||||
<textarea id="env-vars-editor" rows="10"></textarea>
|
||||
<div class="row">
|
||||
<button id="env-vars-load-btn" type="button">Load env[]</button>
|
||||
<button id="env-vars-validate-btn" type="button">Validate</button>
|
||||
<button id="env-vars-save-btn" type="button">Save env[]</button>
|
||||
</div>
|
||||
<pre id="env-vars-validation"></pre>
|
||||
</div>
|
||||
<h3>Diagnostics</h3>
|
||||
<pre id="diagnostics-output"></pre>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Keyboard</h2>
|
||||
<p><code>Ctrl+K</code> command palette, <code>?</code> shortcut help.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="palette" class="overlay hidden">
|
||||
<div class="dialog">
|
||||
<h3>Command Palette</h3>
|
||||
<input id="palette-filter" placeholder="Filter commands..." autocomplete="off" />
|
||||
<div id="palette-list" class="list"></div>
|
||||
<button id="palette-close-btn" type="button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="shortcuts" class="overlay hidden">
|
||||
<div class="dialog">
|
||||
<h3>Shortcut Help</h3>
|
||||
<pre id="shortcut-help"></pre>
|
||||
<button id="shortcuts-close-btn" type="button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -101,3 +101,10 @@ export type SetupPlanResult = {
|
||||
}>;
|
||||
recommendedChanges: string[];
|
||||
};
|
||||
|
||||
export type EnvVarDefinition = {
|
||||
key: string;
|
||||
description: string;
|
||||
default: string;
|
||||
options: string[];
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type {
|
||||
DoctorReport,
|
||||
EnvVarDefinition,
|
||||
EnvProfilesResult,
|
||||
EnvResolveResult,
|
||||
RunEventLogFile,
|
||||
@ -78,6 +79,17 @@ export function toggleFavorite(
|
||||
);
|
||||
}
|
||||
|
||||
export function removeFavoritesMany(
|
||||
projectRoot: string | null,
|
||||
items: Array<{ projectPath: string; workflowId: string }>,
|
||||
): Promise<WorkspaceFavorite[]> {
|
||||
return bridgeCall<WorkspaceFavorite[]>(
|
||||
"favorites.removeMany",
|
||||
{ items },
|
||||
projectRoot,
|
||||
);
|
||||
}
|
||||
|
||||
export function listHistory(
|
||||
projectRoot: string | null,
|
||||
limit: number,
|
||||
@ -115,6 +127,54 @@ export function resolveEnvProfile(
|
||||
);
|
||||
}
|
||||
|
||||
export function setActiveEnvProfile(
|
||||
projectRoot: string | null,
|
||||
profileId: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
return bridgeCall<Record<string, unknown>>(
|
||||
"envProfiles.setActive",
|
||||
{ profileId },
|
||||
projectRoot,
|
||||
);
|
||||
}
|
||||
|
||||
export function saveEnvProfile(
|
||||
projectRoot: string | null,
|
||||
profile: {
|
||||
id: string;
|
||||
description: string;
|
||||
inherits: string[];
|
||||
values: Record<string, string>;
|
||||
},
|
||||
): Promise<Record<string, unknown>> {
|
||||
return bridgeCall<Record<string, unknown>>(
|
||||
"envProfiles.saveProfile",
|
||||
{ profile },
|
||||
projectRoot,
|
||||
);
|
||||
}
|
||||
|
||||
export function listEnvVars(
|
||||
projectRoot: string | null,
|
||||
): Promise<{ projectRoot: string; env: EnvVarDefinition[] }> {
|
||||
return bridgeCall<{ projectRoot: string; env: EnvVarDefinition[] }>(
|
||||
"envVars.list",
|
||||
{},
|
||||
projectRoot,
|
||||
);
|
||||
}
|
||||
|
||||
export function saveEnvVars(
|
||||
projectRoot: string | null,
|
||||
env: EnvVarDefinition[],
|
||||
): Promise<Record<string, unknown>> {
|
||||
return bridgeCall<Record<string, unknown>>(
|
||||
"envVars.save",
|
||||
{ env },
|
||||
projectRoot,
|
||||
);
|
||||
}
|
||||
|
||||
export function runDoctor(projectRoot: string | null): Promise<DoctorReport> {
|
||||
return bridgeCall<DoctorReport>("doctor.run", {}, projectRoot);
|
||||
}
|
||||
@ -122,3 +182,25 @@ export function runDoctor(projectRoot: string | null): Promise<DoctorReport> {
|
||||
export function setupPlan(projectRoot: string | null): Promise<SetupPlanResult> {
|
||||
return bridgeCall<SetupPlanResult>("setup.plan", {}, projectRoot);
|
||||
}
|
||||
|
||||
export function setupAutofixDirs(
|
||||
projectRoot: string | null,
|
||||
): Promise<Record<string, unknown>> {
|
||||
return bridgeCall<Record<string, unknown>>("setup.autofixDirs", {}, projectRoot);
|
||||
}
|
||||
|
||||
export function setupMigrateLegacy(
|
||||
projectRoot: string | null,
|
||||
): Promise<Record<string, unknown>> {
|
||||
return bridgeCall<Record<string, unknown>>("setup.migrateLegacy", {}, projectRoot);
|
||||
}
|
||||
|
||||
export function setupApplyRecommendedConfig(
|
||||
projectRoot: string | null,
|
||||
): Promise<Record<string, unknown>> {
|
||||
return bridgeCall<Record<string, unknown>>(
|
||||
"setup.applyRecommendedConfig",
|
||||
{},
|
||||
projectRoot,
|
||||
);
|
||||
}
|
||||
|
||||
@ -45,12 +45,49 @@ body {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin: 8px 0 10px;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border: 1px solid #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: #0b1629;
|
||||
}
|
||||
|
||||
.group {
|
||||
border: 1px dashed #334155;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 0.9em;
|
||||
color: #93c5fd;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.list-item code {
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #3f4f69;
|
||||
@ -65,6 +102,63 @@ input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #3f4f69;
|
||||
padding: 0.6em 0.8em;
|
||||
font-size: 0.95em;
|
||||
font-family: Consolas, "Courier New", monospace;
|
||||
color: #e8ecf1;
|
||||
background-color: #122033;
|
||||
}
|
||||
|
||||
.env-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.env-grid-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1.4fr 1fr 1.4fr auto;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.env-grid-header {
|
||||
font-size: 0.85em;
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.chip-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid #334155;
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.8em;
|
||||
background: #0f172a;
|
||||
color: #dbeafe;
|
||||
}
|
||||
|
||||
.chip button {
|
||||
margin-left: 6px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fca5a5;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
select {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
@ -120,3 +214,27 @@ code {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(2, 6, 23, 0.75);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: min(760px, 92vw);
|
||||
max-height: 82vh;
|
||||
overflow: auto;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: #0b1220;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user