some gui implementations and fixed a gitea issue.

This commit is contained in:
stan44 2026-03-01 22:33:42 -06:00
parent 2c5493f249
commit 6f4ddecf44
11 changed files with 1767 additions and 87 deletions

View File

@ -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

View File

@ -52,13 +52,24 @@
- [x] GUI parity phase 1 bridge foundation shipped (`sdt bridge --stdio` + `DevTool.Host.Bridge`) - [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 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 - [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] 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] 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] 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 - [x] Add second Tauri bridge command: `sdt run <workflowId> --json` with live stream panel
- [ ] Remove legacy PowerShell wrappers in v2 - [ ] Remove legacy PowerShell wrappers in v2
- [x] Add workspace project inventory model (all `.slnx/.sln/.csproj`) for GUI/TUI multi-project selector - [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) ## Next Sprint (v1.3 UX Foundation)

View File

@ -36,13 +36,21 @@ Error response:
- `workspace.add` (`candidatePath`, `initializeConfig`) - `workspace.add` (`candidatePath`, `initializeConfig`)
- `favorites.list` - `favorites.list`
- `favorites.toggle` (`favoriteProjectPath`, `workflowId`, `label`) - `favorites.toggle` (`favoriteProjectPath`, `workflowId`, `label`)
- `favorites.removeMany` (`items[]` with `projectPath`, `workflowId`)
- `history.list` (`limit`) - `history.list` (`limit`)
- `events.listFiles` - `events.listFiles`
- `events.readFile` (`filePath`) - `events.readFile` (`filePath`)
- `envProfiles.list` - `envProfiles.list`
- `envProfiles.resolve` (`envProfile`) - `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` - `doctor.run`
- `setup.plan` (read-only preview) - `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 ## Determinism Notes

View File

@ -1,14 +1,14 @@
{ {
"version": "1.0", "version": "1.0",
"updatedAtUtc": "2026-03-02T01:00:00Z", "updatedAtUtc": "2026-03-02T02:00:00Z",
"features": [ "features": [
{ {
"id": "workspace.switch_and_candidates", "id": "workspace.switch_and_candidates",
"tui": true, "tui": true,
"gui": true, "gui": true,
"status": "in_progress", "status": "done",
"owner": "bridge", "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", "id": "workflow.run",
@ -54,17 +54,33 @@
"id": "favorites.quick_actions", "id": "favorites.quick_actions",
"tui": true, "tui": true,
"gui": true, "gui": true,
"status": "in_progress", "status": "done",
"owner": "bridge", "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", "id": "setup_wizard_autofix",
"tui": true, "tui": true,
"gui": false, "gui": true,
"status": "planned", "status": "in_progress",
"owner": "gui", "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."
} }
] ]
} }

View File

@ -74,13 +74,21 @@ public sealed class BridgeStdioServer
"workspace.add" => Ok(request.Id, HandleWorkspaceAdd(request.Params)), "workspace.add" => Ok(request.Id, HandleWorkspaceAdd(request.Params)),
"favorites.list" => Ok(request.Id, HandleFavoritesList(request.Params)), "favorites.list" => Ok(request.Id, HandleFavoritesList(request.Params)),
"favorites.toggle" => Ok(request.Id, HandleFavoritesToggle(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)), "history.list" => Ok(request.Id, HandleHistoryList(request.Params)),
"events.listFiles" => Ok(request.Id, HandleEventsListFiles(request.Params)), "events.listFiles" => Ok(request.Id, HandleEventsListFiles(request.Params)),
"events.readFile" => Ok(request.Id, HandleEventsReadFile(request.Params)), "events.readFile" => Ok(request.Id, HandleEventsReadFile(request.Params)),
"envProfiles.list" => Ok(request.Id, HandleEnvProfilesList(request.Params)), "envProfiles.list" => Ok(request.Id, HandleEnvProfilesList(request.Params)),
"envProfiles.resolve" => Ok(request.Id, HandleEnvProfilesResolve(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)), "doctor.run" => Ok(request.Id, await HandleDoctorRunAsync(request.Params, cancellationToken).ConfigureAwait(false)),
"setup.plan" => Ok(request.Id, HandleSetupPlan(request.Params)), "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}") _ => Error(request.Id, "method_not_found", $"Unsupported bridge method: {request.Method}")
}; };
} }
@ -222,6 +230,48 @@ public sealed class BridgeStdioServer
return HandleFavoritesList(@params); 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) private object HandleHistoryList(JsonElement @params)
{ {
var projectRoot = ResolveProjectRootForProjectScopedMethod(@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) private async Task<object> HandleDoctorRunAsync(JsonElement @params, CancellationToken cancellationToken)
{ {
var loaded = LoadProject(@params); 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) private LoadedProjectConfig LoadProject(JsonElement @params)
{ {
var startDir = GetString(@params, "projectRoot") ?? _startupProjectRoot ?? Directory.GetCurrentDirectory(); 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) => private static BridgeResponseEnvelope Error(string? id, string code, string message, object? details = null) =>
new(id, false, null, new BridgeErrorEnvelope(code, message, details)); 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); public sealed class BridgeValidationException(string message) : Exception(message);

View File

@ -34,5 +34,10 @@ npm run tauri dev
- workflow + debug run - workflow + debug run
- failure card rendering from summary payload - failure card rendering from summary payload
- run context + lifecycle panel - 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 - 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)

View File

@ -45,6 +45,45 @@
<input id="favorite-label" placeholder="Quick Build" autocomplete="off" /> <input id="favorite-label" placeholder="Quick Build" autocomplete="off" />
<button id="toggle-favorite-btn" type="button">Toggle Favorite</button> <button id="toggle-favorite-btn" type="button">Toggle Favorite</button>
</div> </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> <pre id="workspace-output"></pre>
</section> </section>
@ -98,15 +137,80 @@
<button id="load-doctor-btn" type="button">Run Doctor</button> <button id="load-doctor-btn" type="button">Run Doctor</button>
<button id="load-setup-plan-btn" type="button">Setup Plan (read-only)</button> <button id="load-setup-plan-btn" type="button">Setup Plan (read-only)</button>
</div> </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"> <div class="row">
<input id="env-resolve-id" placeholder="env profile id (optional)" autocomplete="off" /> <input id="env-resolve-id" placeholder="env profile id (optional)" autocomplete="off" />
<button id="load-env-btn" type="button">Load Env Profiles + Resolve</button> <button id="load-env-btn" type="button">Load Env Profiles + Resolve</button>
</div> </div>
<h3>Environment</h3> <h3>Environment</h3>
<pre id="env-output"></pre> <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> <h3>Diagnostics</h3>
<pre id="diagnostics-output"></pre> <pre id="diagnostics-output"></pre>
</section> </section>
<section class="panel">
<h2>Keyboard</h2>
<p><code>Ctrl+K</code> command palette, <code>?</code> shortcut help.</p>
</section>
</main> </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> </body>
</html> </html>

View File

@ -101,3 +101,10 @@ export type SetupPlanResult = {
}>; }>;
recommendedChanges: string[]; recommendedChanges: string[];
}; };
export type EnvVarDefinition = {
key: string;
description: string;
default: string;
options: string[];
};

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import type { import type {
DoctorReport, DoctorReport,
EnvVarDefinition,
EnvProfilesResult, EnvProfilesResult,
EnvResolveResult, EnvResolveResult,
RunEventLogFile, 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( export function listHistory(
projectRoot: string | null, projectRoot: string | null,
limit: number, 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> { export function runDoctor(projectRoot: string | null): Promise<DoctorReport> {
return bridgeCall<DoctorReport>("doctor.run", {}, projectRoot); 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> { export function setupPlan(projectRoot: string | null): Promise<SetupPlanResult> {
return bridgeCall<SetupPlanResult>("setup.plan", {}, projectRoot); 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,
);
}

View File

@ -45,12 +45,49 @@ body {
gap: 8px; 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 { label {
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
} }
input, input,
select,
textarea,
button { button {
border-radius: 8px; border-radius: 8px;
border: 1px solid #3f4f69; border: 1px solid #3f4f69;
@ -65,6 +102,63 @@ input {
width: 100%; 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 { button {
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
@ -120,3 +214,27 @@ code {
width: 100%; 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;
}