Skip to content

feat(workbench-shell): ✨ integrate task boards and thread profiles - #98

Merged
jorben merged 12 commits into
masterfrom
feat/add-workspace-jumppage
Apr 21, 2026
Merged

feat(workbench-shell): ✨ integrate task boards and thread profiles#98
jorben merged 12 commits into
masterfrom
feat/add-workspace-jumppage

Conversation

@jorben

@jorben jorben commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Integrate the active task board into the workbench composer flow and move it below the conversation surface.
  • Persist each thread's selected profile in the database, update it on in-thread profile switches, and show a missing-profile state when the referenced profile has been deleted.
  • Reuse the new-thread activation flow across workspace entry points, refine composer spacing and shell classes, and bump tiycore to 0.1.11-rc.26042020.

Test Plan

  • npm run typecheck
  • npm run test:unit -- dashboard-workbench.test.ts
  • cargo fmt --manifest-path src-tauri/Cargo.toml
  • cargo test --manifest-path src-tauri/Cargo.toml --test m1_4_thread -- --nocapture

🤖 Generated with TiyCode

jorben added 6 commits April 20, 2026 21:26
Adjust the runtime thread surface so the active task board is rendered
outside the conversation stream and the chat content gains dynamic
bottom padding to keep the final messages visible.

This also keeps task history visible when present and updates the
spacing logic so runtime errors continue to align correctly with the
latest relevant timeline entry.
Add a unified stacked layout for the active task board and prompt
composer so they visually connect as one surface.

- Wrap the active task board in a matching shell with rounded top
  corners, blur, and border treatment
- Allow the prompt composer to accept outer wrapper and shell
  class overrides for tighter layout control
- Remove the visual gap between the task board and composer when a
  board is active while preserving the existing standalone composer
  appearance
Add thread-level profile storage and management so threads can keep
their selected profile independently of runs.

- Add a migration to introduce `threads.profile_id`, backfill existing
  threads from prior run/profile settings, and index the new column
- Persist `profile_id` when creating threads and expose a command to
  update it later
- Return `profile_id` in thread records and summaries throughout the
  backend
- Update repository queries and tests to account for the new thread
  profile field
- Adjust frontend types and runtime thread surface handling for the new
  profile data
@github-actions

github-actions Bot commented Apr 20, 2026

Copy link
Copy Markdown

AI Code Review Summary

PR: #98 (feat(workbench-shell): ✨ integrate task boards and thread profiles)
Preferred language: English

Overall Assessment

Detected 12 actionable findings, prioritize CRITICAL/HIGH before merge.

Major Findings by Severity

  • CRITICAL (1)
    • src-tauri/src/persistence/repo/thread_repo.rs:85 - Missing database migration for profile_id column on threads table
  • HIGH (4)
    • src-tauri/src/commands/agent.rs:342 - All seed_thread call sites pass None for profile_id, leaving the new column path untested
    • src-tauri/src/persistence/repo/thread_repo.rs:122 - No test coverage for thread_update_profile command and repo function
    • src/modules/settings-center/model/use-settings-controller.ts:293 - Module-level mutable inflightCreateIds singleton causes test pollution and flaky tests
    • src/modules/workbench-shell/ui/dashboard-workbench.tsx:811 - Missing unit tests for workbenchActiveAgentProfile returning null on deleted profiles
  • MEDIUM (5)
    • src-tauri/Cargo.toml:56 - Release candidate crate version used in production dependency
    • src-tauri/src/core/prompt_command_manager.rs:849 - Prompt trimming behavioral change lacks regression tests
    • src/modules/settings-center/model/use-settings-controller.ts:1304 - Missing test for pendingCreate command lifecycle and deferred backend creation
    • src/modules/workbench-shell/ui/dashboard-workbench.tsx:2455 - Missing test for async handleSelectAgentProfileForThread with threadUpdateProfile error path
    • src/modules/workbench-shell/ui/dashboard-workbench.tsx:1030 - Missing test for thread creation profile assignment and workspace state update
  • LOW (2)
    • src-tauri/tests/test_helpers.rs:67 - Inconsistent indentation on .bind(profile_id) in seed_thread
    • src/modules/workbench-shell/ui/dashboard-workbench.tsx:163 - resolveThreadProfileId and resolveActiveThreadWorkbenchProfileId are now identical

Actionable Suggestions

  • Add a database migration file in src-tauri/migrations/ to add the profile_id column to the threads table before merging
  • Replace the tiycore RC version with a stable release or add a TODO/comment tracking the upgrade path
  • Add integration tests for the new thread_update_profile command covering both Some and None cases
  • Verify that serde deserialization correctly maps the TypeScript camelCase profileId to the Rust snake_case profile_id parameter in Tauri commands
  • Add unit tests for the prompt trimming behavior changes in prompt_command_manager.rs
  • Add integration test for thread_update_profile covering: successful update, clearing to None, and not-found error
  • Add integration test for thread_create with a non-None profile_id and verify it persists correctly
  • Add at least one seed_thread call with Some(profile_id) in existing test suite to exercise the non-null SQL binding path
  • Add unit tests for prompt trimming behavioral change (trim vs trim_end, and no-trim for input)
  • Verify a database migration exists for the new profile_id column in src-tauri/migrations/
  • Run cargo test and npm run test:unit to validate the RC tiycore dependency doesn't introduce regressions
  • Run cargo fmt on src-tauri/tests/test_helpers.rs to fix the indentation inconsistency on .bind(profile_id) before committing.

Potential Risks

  • Existing users' databases will crash on any thread operation due to missing profile_id column
  • RC dependency may introduce subtle behavior changes or breakage in tiycore functionality
  • Prompt whitespace changes may affect LLM prompt quality for existing custom slash commands
  • Runtime SQL errors if profile_id column migration is missing for existing databases
  • profile_id null handling bugs in repo layer go undetected due to lack of non-null test cases
  • Prompt formatting changes from trim() to trim_end() could subtly alter agent behavior
  • RC dependency version may introduce instability or breaking changes
  • Frontend profileId serialization may not correctly map to Rust's profile_id snake_case parameter
  • If the threads table migration adding profile_id column hasn't been applied, the new INSERT with the profile_id column would fail at runtime. Ensure the corresponding migration is included in this change set.
  • Rapid edits to a newly created command can trigger promptCommandUpdate with a temp ID before the backend create completes, causing silent failures
  • Existing threads created before this change may have no profileId, leading to unexpected global-profile fallback behavior
  • Module-level inflightCreateIds may cause test flakiness if tests don't reset module state between runs

Test Suggestions

  • Add src-tauri/migrations/ migration and run cargo test to verify thread_repo operations
  • Test thread_create with and without profileId from the frontend bridge
  • Test thread_update_profile with valid ID, invalid ID (404), null profileId, and non-null profileId
  • Verify prompt parsing with leading/trailing whitespace for both file-based and input-based prompts
  • Integration test: create thread with profile_id, retrieve it, verify profile_id matches
  • Integration test: update thread profile_id from None to Some, verify persistence
  • Integration test: update thread profile_id from Some to None, verify cleared
  • Integration test: thread_update_profile with non-existent thread ID returns not-found error
  • Unit test: prompt_command_manager file-loaded prompt preserves leading whitespace with trim_end
  • Unit test: prompt_command_manager input.prompt preserves all whitespace
  • Integration test: migration adds profile_id column to existing threads table (nullable)
  • Consider adding a test that creates a thread with profile_id: None and explicitly asserts the DB value is NULL, to complement the None-clearing test and provide full coverage of the None path on creation.

File-Level Coverage Notes

  • src-tauri/Cargo.toml: risk (The RC version flag is a stability concern that should be validated through the existing test suite.)
  • src-tauri/src/commands/agent.rs: risk (The test helper was updated but no new test scenarios were added to exercise the new profile_id parameter.)
  • src-tauri/src/commands/thread.rs: risk (Two new command surfaces (thread_create profile_id param, thread_update_profile) need test coverage.)
  • src-tauri/src/core/prompt_command_manager.rs: risk (Subtle behavioral change that could affect prompt formatting in production.)
  • src-tauri/src/core/thread_manager.rs: risk (New method and modified method both need test coverage.)
  • src-tauri/src/model/thread.rs: ok (Model changes are straightforward field additions with proper From impl mapping. Risk is low if repo/command layers are tested.)
  • src-tauri/src/persistence/repo/thread_repo.rs: risk (The repo layer is the most critical gap — new SQL paths need integration test validation.)
  • src-tauri/src/lib.rs: ok (Registration of new command is straightforward.)
  • src/services/bridge/thread-commands.ts: ok (Bridge layer changes are mechanical. The camelCase/snake_case mapping between TS and Rust should be verified.)
  • src/shared/types/api.ts: ok (Type definition addition is straightforward and matches the Rust DTO.)
  • src-tauri/tests/test_helpers.rs: ok_with_minor (seed_thread correctly adds profile_id: Option<&str> parameter and updates the INSERT statement. SQLx handles Option<&str> binding to nullable columns properly.)
  • src-tauri/tests/m1_4_thread.rs: ok (Four well-scoped new tests cover create-with-profile, update-profile, clear-profile-to-none, and list-returns-profile. Existing tests correctly pass None for the new parameter.)
  • src-tauri/tests/m1_5_agent_run.rs: ok (All seed_thread calls updated to pass None. No behavioral changes.)
  • src-tauri/tests/m1_6_tool_gateway.rs: ok (All seed_thread calls updated to pass None. No behavioral changes.)
  • src-tauri/tests/m1_e2e_full_chain.rs: ok (All seed_thread calls updated to pass None. No behavioral changes.)
  • src-tauri/tests/m2_3_task_tracking.rs: ok (All seed_thread calls updated to pass None. No behavioral changes.)
  • src-tauri/tests/m1_7_frontend_integration.rs: ok (profile_id field added to ThreadSummaryDto test construction. Straightforward addition.)
  • src-tauri/tests/m2_1_terminal_manager.rs: ok (All seed_thread calls updated to pass None. No behavioral changes.)
  • src-tauri/tests/m2_4_worktree.rs: ok (All seed_thread calls updated to pass None. No behavioral changes.)
  • src-tauri/tests/m1_2_workspace.rs: ok (All seed_thread calls updated to pass None. No behavioral changes.)
  • ... and 11 more file-level entries.

Inline Downgraded Items (processed but not inline)

  • None

Coverage Status

  • Target files: 31
  • Covered files: 31
  • Uncovered files: 0
  • No-patch/binary covered as file-level: 0
  • Findings with unknown confidence (N/A): 0

Uncovered list:

  • None

No-patch covered list:

  • None

Runtime/Budget

  • Rounds used: 1/4
  • Planned batches: 3
  • Executed batches: 3
  • Sub-agent runs: 5
  • Planner calls: 1
  • Reviewer calls: 5
  • Model calls: 6/64
  • Structured-output summary-only degradation: NO

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 8
  • Findings with unknown confidence: 0
  • Inline comments attempted: 8
  • Target files: 27
  • Covered files: 27
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

) -> Result<Vec<ThreadRecord>, AppError> {
let rows = sqlx::query_as::<_, ThreadRow>(
"SELECT id, workspace_id, title, status, summary, last_active_at, created_at, updated_at
"SELECT id, workspace_id, profile_id, title, status, summary, last_active_at, created_at, updated_at

This comment was marked as outdated.

...current,
[activeThread.id]: profileId,
}));
await threadUpdateProfile(activeThread.id, profileId);

This comment was marked as outdated.

: (terminalThreadBindings[
getNewThreadTerminalBindingKey(nextWorkspaceId)
] ?? null);
let persistedThreadProfileId = activeAgentProfileId;

This comment was marked as outdated.

Comment thread src-tauri/Cargo.toml
portable-pty = "0.9"
git2 = { version = "0.20", features = ["vendored-libgit2", "vendored-openssl"] }
tiycore = "0.1.10"
tiycore = "0.1.11-rc.26042020"

This comment was marked as outdated.

let manager = ThreadManager::new(pool.clone());

manager
.update_profile("t-update-profile", Some("profile-new"))

This comment was marked as outdated.

expect(
resolveThreadProfileId("t-1", bindings, new Set(), globalActive, null),
).toBe(globalActive);
it("preserves deleted profile ids instead of silently falling back", () => {

This comment was marked as outdated.

// default profile because it defines the profile new conversations inherit.
const handleSelectAgentProfileForThread = useCallback(
(profileId: string) => {
async (profileId: string) => {

This comment was marked as outdated.

)
.bind(thread_id)
.bind(workspace_id)
.bind(profile_id)

This comment was marked as outdated.

jorben added 2 commits April 21, 2026 07:34
- Change nullish coalescing (`??`) to logical OR (`||`) in
  resolveThreadProfileId and resolveActiveThreadWorkbenchProfileId
  so empty-string profile IDs correctly fall back to the global
  active profile instead of being preserved as valid values
- Add try/catch around threadUpdateProfile with user-facing error
  message via new i18n key dashboard.error.switchProfile
- Add backend test verifying profile can be cleared to None
- Add frontend tests for empty-string fallback behavior

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 12
  • Findings with unknown confidence: 0
  • Inline comments attempted: 12
  • Target files: 27
  • Covered files: 27
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

) -> Result<Vec<ThreadRecord>, AppError> {
let rows = sqlx::query_as::<_, ThreadRow>(
"SELECT id, workspace_id, title, status, summary, last_active_at, created_at, updated_at
"SELECT id, workspace_id, profile_id, title, status, summary, last_active_at, created_at, updated_at

This comment was marked as outdated.

state.thread_manager.update_title(&id, &title).await
}

#[tauri::command]

This comment was marked as outdated.

Ok(())
}

pub async fn update_profile(

This comment was marked as outdated.

@@ -1470,24 +1434,6 @@ export function DashboardWorkbench() {
await waitForBackendReady();

This comment was marked as outdated.

Comment thread src-tauri/Cargo.toml
portable-pty = "0.9"
git2 = { version = "0.20", features = ["vendored-libgit2", "vendored-openssl"] }
tiycore = "0.1.10"
tiycore = "0.1.11-rc.26042020"

This comment was marked as outdated.

assert_eq!(row.get::<String, _>("status"), "idle");
}

#[tokio::test]

This comment was marked as outdated.

)
.bind(thread_id)
.bind(workspace_id)
.bind(profile_id)

This comment was marked as outdated.

expect(
resolveThreadProfileId(null, { "t-1": "p-2" }, profileIds, globalActive, firstProfile),
).toBe(globalActive);
it("returns the global active profile when the thread has no persisted profile", () => {

This comment was marked as outdated.

}

#[tauri::command]
pub async fn thread_update_profile(

This comment was marked as outdated.

// Intentional: new-thread mode always uses the global active profile as the
// default for the new conversation; switching to an existing thread reads
// its own persisted profile_id via resolveThreadProfileId instead.
let persistedThreadProfileId = activeAgentProfileId;

This comment was marked as outdated.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 9
  • Findings with unknown confidence: 0
  • Inline comments attempted: 9
  • Target files: 27
  • Covered files: 27
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

sqlx::query(
"INSERT INTO threads (id, workspace_id, title, status, summary, last_active_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO threads (id, workspace_id, profile_id, title, status, summary, last_active_at, created_at, updated_at)

This comment was marked as outdated.

}

#[tauri::command]
pub async fn thread_update_profile(

This comment was marked as outdated.

[activeThread.id]: profileId,
}));
try {
await threadUpdateProfile(activeThread.id, profileId);

This comment was marked as outdated.

if (matchedProfile) {
return matchedProfile;
}
return isNewThreadMode ? (agentProfiles[0] ?? null) : null;

This comment was marked as outdated.

}
return agentProfiles[0] ?? null;
}, [activeAgentProfileId, agentProfiles, allowMissingActiveProfile]);
const hasMissingActiveProfile =

This comment was marked as outdated.

state: State<'_, AppState>,
workspace_id: String,
title: Option<String>,
profile_id: Option<String>,

This comment was marked as outdated.

/// Update the profile bound to a thread.
pub async fn update_profile(&self, id: &str, profile_id: Option<&str>) -> Result<(), AppError> {
let updated = thread_repo::update_profile(&self.pool, id, profile_id).await?;
if !updated {

This comment was marked as outdated.

).toBe("p-2");
});

it("falls back to firstProfileId when bound profile was deleted", () => {

This comment was marked as outdated.

@@ -1470,24 +1434,6 @@ export function DashboardWorkbench() {
await waitForBackendReady();

This comment was marked as outdated.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 13
  • Findings with unknown confidence: 0
  • Inline comments attempted: 13
  • Target files: 27
  • Covered files: 27
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

) -> Result<Vec<ThreadRecord>, AppError> {
let rows = sqlx::query_as::<_, ThreadRow>(
"SELECT id, workspace_id, title, status, summary, last_active_at, created_at, updated_at
"SELECT id, workspace_id, profile_id, title, status, summary, last_active_at, created_at, updated_at

This comment was marked as outdated.

}

#[tauri::command]
pub async fn thread_update_profile(

This comment was marked as outdated.

Ok(())
}

pub async fn update_profile(

This comment was marked as outdated.

if (matchedProfile) {
return matchedProfile;
}
return isNewThreadMode ? (agentProfiles[0] ?? null) : null;

This comment was marked as outdated.

[activeThread.id]: profileId,
}));
try {
await threadUpdateProfile(activeThread.id, profileId);

This comment was marked as outdated.

.await;
let manager = ThreadManager::new(pool.clone());

manager

This comment was marked as outdated.

if (profileIds.has(boundId)) return boundId;
// Bound profile was deleted → fall back to first available profile.
return firstProfileId ?? globalActiveProfileId;
return threadProfileId || globalActiveProfileId;

This comment was marked as outdated.

)
.bind(thread_id)
.bind(workspace_id)
.bind(profile_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW] Misaligned indentation on .bind(profile_id) in seed_thread

The newly added .bind(profile_id) line is indented 2 spaces less than the other .bind() calls in the same chain, creating inconsistent formatting.

Suggestion: Run cargo fmt --manifest-path src-tauri/Cargo.toml to auto-fix the indentation before committing, as required by the project post-implementation checklist.

Risk: Cosmetic only; cargo fmt will fix it, but if skipped the diff will contain noise.

Confidence: 0.95

[From SubAgent: general]

hasMissingActiveProfile
? t("composer.profileDeletedHint")
: "Select an agent profile with an enabled model before starting a run.",
);

This comment was marked as outdated.

? "assistant"
: lastPresentationRole;

const conversationBottomPadding = BASE_CONVERSATION_BOTTOM_PADDING;

This comment was marked as outdated.

jorben added 2 commits April 21, 2026 08:35
…ands

Restructure command creation to always create entries locally first,
then persist to backend only when a valid name is provided. This
improves UX by allowing immediate local creation without waiting for
async backend calls.

- Add `pendingCreate` flag to `CommandEntry` type for tracking
  un-persisted entries
- Add `inflightCreateIds` set to prevent duplicate backend calls
- Simplify `removeCommand` to always remove locally first, skipping
  backend delete for pending entries
- Simplify `addCommand` to create local-only entry immediately
- Defer backend creation in `updateCommand` until name is non-empty
- Change Rust prompt parsing to use `trim_end()` instead of `trim()`
  to preserve intentional leading whitespace
- Add defaultCollapsed prop to control initial collapse state
- Replace static title div with interactive button containing chevron toggle
- Conditionally render task list and status badge based on collapsed state
- Animate chevron rotation on collapse/expand transition

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 12
  • Findings with unknown confidence: 0
  • Inline comments attempted: 12
  • Target files: 31
  • Covered files: 31
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

sqlx::query(
"INSERT INTO threads (id, workspace_id, title, status, summary, last_active_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO threads (id, workspace_id, profile_id, title, status, summary, last_active_at, created_at, updated_at)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] Missing database migration for profile_id column on threads table

All thread_repo queries now reference a profile_id column that does not yet exist in the threads table. Without a corresponding migration in src-tauri/migrations/, any existing database will throw a SQL error at runtime.

Suggestion: Add a migration file (e.g., src-tauri/migrations/XXXX_add_thread_profile_id.sql) with: ALTER TABLE threads ADD COLUMN profile_id TEXT;

Risk: Runtime crash on any thread operation for existing installations; new installs may work if the schema is built from DDL that already includes the column, but existing users will break.

Confidence: 0.95

[From SubAgent: general]


seed_workspace(&pool, "ws-dup-approval", workspace_path.to_str().unwrap()).await;
seed_thread(&pool, "t-dup-approval", "ws-dup-approval").await;
seed_thread(&pool, "t-dup-approval", "ws-dup-approval", None).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] All seed_thread call sites pass None for profile_id, leaving the new column path untested

While the schema and insert now support profile_id, no test actually inserts a thread with a non-null profile_id, meaning the SQL binding and retrieval for non-null profile_id is untested.

Suggestion: Add at least one integration test that seeds a thread with Some(profile_id) and verifies it is correctly persisted and returned via find_by_id or list_by_workspace.

Risk: A bug in profile_id binding or retrieval (e.g., null handling, type mismatch) would go undetected until runtime.

Confidence: 0.93

[From SubAgent: testing]

Ok(())
}

pub async fn update_profile(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] No test coverage for thread_update_profile command and repo function

The new update_profile repo function and thread_update_profile command have zero test coverage. The existing test file (agent.rs) only exercises seed_thread with None for profile_id.

Suggestion: Add integration tests for thread_update_profile covering: successful update (Some(profile_id)), clearing profile (None), and not-found error case. Also add a test for thread_create with a non-None profile_id.

Risk: The update_profile SQL could have binding issues, the not-found error path could be wrong, or the profile_id could be persisted incorrectly — none of these are verified.

Confidence: 0.95

[From SubAgent: testing]

}

/** Track IDs of pending entries whose backend create call is in-flight. */
const inflightCreateIds = new Set<string>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Module-level mutable inflightCreateIds singleton causes test pollution and flaky tests

A module-level mutable Set persists across test runs. If any test triggers command creation without awaiting the promise, stale IDs remain in the set, causing subsequent tests to skip backend calls they should exercise.

Suggestion: Either move inflightCreateIds inside the hook (useRef), expose a reset function for tests, or add an afterEach cleanup that clears the set. Add unit tests that verify the dedup logic works correctly under concurrent calls.

Risk: Silent test pollution where later tests skip backend creation because an ID leaked from a prior test, leading to false passes.

Confidence: 0.92

[From SubAgent: testing]

agentProfiles[0] ??
null,
[workbenchActiveProfileId, agentProfiles],
() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Missing unit tests for workbenchActiveAgentProfile returning null on deleted profiles

The useMemo now returns null for existing threads whose profile was deleted, rather than falling back. This is a key behavioral change with no dedicated unit test covering the null case, the new-thread fallback, or downstream consumers that receive null.

Suggestion: Extract the profile resolution logic into a testable pure function (similar to resolveThreadProfileId) and add unit tests for: existing thread with deleted profile → null, new thread mode with deleted profile → first profile, new thread mode with no profiles → null.

Risk: Downstream consumers may not handle null activeProfile correctly, causing runtime errors that only manifest in production when a user deletes a profile bound to an existing thread.

Confidence: 0.90

[From SubAgent: testing]

argument_hint: meta.argument_hint.unwrap_or_default(),
description: meta.description.unwrap_or_default(),
prompt: body.trim().to_string(),
prompt: body.trim_end().to_string(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Prompt trimming behavioral change lacks regression tests

Changing trim() to trim_end() preserves leading whitespace but removes trailing whitespace differently. Removing trim on input.prompt means user-supplied leading/trailing whitespace is now preserved. Neither change has test coverage.

Suggestion: Add unit tests verifying: (1) file-loaded prompts with leading whitespace are preserved after trim_end(), (2) input.prompt with leading/trailing whitespace is stored as-is, (3) the previous behavior (trim both ends) is intentionally no longer expected.

Risk: Subtle prompt formatting differences could affect agent behavior without being caught by tests.

Confidence: 0.88

[From SubAgent: testing]

@@ -1295,67 +1302,41 @@ export function useSettingsController() {
};

const addCommand = (entry: Omit<CommandEntry, "id">) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Missing test for pendingCreate command lifecycle and deferred backend creation

The deferred command creation flow is complex: addCommand creates a local entry with pendingCreate flag, updateCommand may strip or keep it, and the backend call is triggered only when name becomes non-empty and not already in-flight. None of these paths have unit tests.

Suggestion: Add unit tests for: addCommand in Tauri mode sets pendingCreate=true; updateCommand with empty name keeps pendingCreate; updateCommand with non-empty name triggers backend create and removes pendingCreate; inflightCreateIds dedup prevents duplicate calls; failed creation cleans up inflightCreateIds; removeCommand on pending entry skips backend delete.

Risk: Edge cases like double-updating a pending command before the first create resolves, or removing a pending command while its create is in-flight, could lead to orphaned backend entries or UI inconsistency.

Confidence: 0.87

[From SubAgent: testing]

// default profile because it defines the profile new conversations inherit.
const handleSelectAgentProfileForThread = useCallback(
(profileId: string) => {
async (profileId: string) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Missing test for async handleSelectAgentProfileForThread with threadUpdateProfile error path

The profile switch handler is now async with a backend call and error handling. Neither the success path (workspace state update + override set) nor the failure path (composerError display) has test coverage.

Suggestion: Add integration or unit tests covering: successful profile switch updates workspace thread profileId and override; failed threadUpdateProfile sets composerError and does not mutate state; the function is not called when activeThread?.id is null.

Risk: A failed threadUpdateProfile call silently swallows the error if the catch path has a bug, or leaves the UI in an inconsistent state where local override is set but backend disagrees.

Confidence: 0.88

[From SubAgent: testing]

}

const creationPromise = threadCreate(workspaceId, "")
const creationPromise = threadCreate(workspaceId, "", activeAgentProfileId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Missing test for thread creation profile assignment and workspace state update

threadCreate now receives activeAgentProfileId as a third argument. After creation, the code updates both the profile override and the workspace thread's profileId. Neither the argument passing nor the state update has test coverage, and the fallback thread.profileId ?? activeAgentProfileId handles a null response that isn't tested.

Suggestion: Add tests verifying: activeAgentProfileId is passed to threadCreate; workspace threads are updated with the returned profileId; fallback to activeAgentProfileId when thread.profileId is null.

Risk: If the backend doesn't return profileId, the fallback logic may create a mismatch between the local override and the workspace thread object.

Confidence: 0.85

[From SubAgent: testing]

* persisted thread-level profile_id; deleted profile ids are preserved so the
* UI can render a missing-profile state instead of silently falling back.
*/
export function resolveThreadProfileId(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW] resolveThreadProfileId and resolveActiveThreadWorkbenchProfileId are now identical

After the refactor, both resolve functions have identical logic (threadProfileId || globalActiveProfileId). They should be consolidated to reduce duplication and avoid divergence.

Suggestion: Replace one function with the other or create a single shared helper. Remove the unused export and update callers.

Risk: The two functions could diverge in future edits, causing subtle bugs.

Confidence: 0.95

[From SubAgent: general]

@jorben
jorben merged commit fa7fb59 into master Apr 21, 2026
5 checks passed
@jorben
jorben deleted the feat/add-workspace-jumppage branch April 21, 2026 02:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant