Skip to content

feat(wework): generate friendly task titles - #2471

Merged
qdaxb merged 9 commits into
mainfrom
feature/wework-friendly-task-titles
Aug 7, 2026
Merged

feat(wework): generate friendly task titles#2471
qdaxb merged 9 commits into
mainfrom
feature/wework-friendly-task-titles

Conversation

@qdaxb

@qdaxb qdaxb commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What changed

  • Adds an opt-in Use friendly titles setting with an independently selected title model.
  • Generates concise task titles asynchronously with a separate ephemeral model turn.
  • Updates the Wework sidebar title in place with a shimmer, without changing the task lifecycle state.
  • Synchronizes the generated title to explicitly linked local or cloud project-space tasks.
  • Keeps ordinary new tasks standalone unless the user explicitly links a project space.
  • Expands the official Codex model picker to the models exposed by the Codex catalog, including hidden picker entries.

Why

Users should not have to manually rename every task, but title generation must not block task creation, create a visible conversation, or incorrectly show a completed task as running.

The status regression was caused by title-update events triggering full runtime-work refreshes while the main turn was still settling. The title event now applies a targeted title-only state update instead.

Validation

  • Isolated Wework Tauri instance starts successfully.
  • Full pre-push checks passed:
    • Wework ESLint, TypeScript build, and unit tests
    • Executor cargo fmt --check, cargo test --all-features --lib, and Clippy
  • Added/update tests for title event routing, title-only runtime state updates, tracking sync, settings preferences, model filtering, and UI shimmer behavior.

Summary by CodeRabbit

  • New Features

    • Added an optional Use friendly titles setting with selectable models.
    • New tasks can receive concise, asynchronously generated titles without delaying creation or changing task status.
    • Manual renames and generation failures preserve the existing title.
    • Updated titles synchronize to linked project boards, with visual shimmer feedback.
    • New chats now start as standalone tasks.
    • Added support for the latest Codex model listings.
  • Documentation

    • Documented friendly-title behavior, synchronization rules, visual model setup, and failure handling.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d7c07953-24db-4b92-aeda-4dd56d739abf

📥 Commits

Reviewing files that changed from the base of the PR and between 33da32a and 9196e19.

📒 Files selected for processing (1)
  • wework/e2e/desktop/task-flow.e2e.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • wework/e2e/desktop/task-flow.e2e.mjs

📝 Walkthrough

Walkthrough

The pull request adds configurable asynchronous friendly-title generation, runtime title-update events, linked-task synchronization, title-update animation, standalone-chat handling, and Codex model catalog updates.

Changes

Friendly title workflow

Layer / File(s) Summary
Configuration and model selection
wework/src/types/api.ts, wework/src/tauri/*, wework/src-tauri/src/lib.rs, wework/src/components/settings/GeneralSettingsPage.tsx, wework/src/api/local/localServices.ts, docs/*
Preferences, settings controls, runtime request types, and documentation support friendly-title generation.
Codex model catalog updates
wework/src/features/model-settings/*, wework/src/api/local/codexOfficialModels.ts, wework/e2e/desktop/task-flow.e2e.mjs
Model listing includes hidden models and normalizes the current official picker models, including gpt-5.5.
Asynchronous title generation
executor/src/runtime_work/handler/tasks.rs
Task creation can queue an ephemeral title-generation turn. The handler normalizes responses, protects manual changes, updates task state, and emits completion events.
Title event propagation and synchronization
wework/src/stream/*, wework/src/features/workbench/*, wework/src/api/{deliveries.ts,local,hybrid}/*
Title events update runtime state and synchronize linked project tasks with deduplication and retry handling.
Title update presentation
wework/src/components/layout/*, wework/src/styles/globals.css
Runtime task rows use updated titles and display a timed shimmer with reduced-motion support. Supporting tests cover title tracking and standalone-chat rendering.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Workbench
  participant Executor
  participant Codex
  participant TaskTracking
  User->>Workbench: Create runtime task
  Workbench->>Executor: Send friendly title request
  Executor->>Codex: Run ephemeral title turn
  Codex-->>Executor: Return generated title
  Executor-->>Workbench: Emit title update
  Workbench->>TaskTracking: Synchronize linked task title
  Workbench-->>User: Render updated title shimmer
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: generating friendly task titles in Wework.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/wework-friendly-task-titles

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qdaxb
qdaxb marked this pull request as ready for review August 6, 2026 14:51
@qdaxb
qdaxb enabled auto-merge August 6, 2026 14:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wework/src/api/local/localServices.ts (1)

2521-2524: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prepare the selected title model before task creation.

Line 2523 prepares only data.modelId. The title request uses the separate data.friendlyTitle.modelId at Lines 1610-1612. If that model requires catalog synchronization, the primary task can start while title generation fails with an unavailable model.

Prepare each distinct configured model ID before sending runtime.tasks.create.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/api/local/localServices.ts` around lines 2521 - 2524, Update
createRuntimeTask to prepare every distinct configured model before task
creation, including data.friendlyTitle.modelId in addition to data.modelId when
present. Reuse prepareRuntimeModel for each unique ID and throw
modelCatalogSyncCancelled if any preparation fails, before runtime.tasks.create
is invoked.
🧹 Nitpick comments (7)
wework/src/components/layout/DesktopSidebar.tsx (2)

1645-1658: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated shimmer title markup in both RuntimeTaskRow layout branches. Both branches write the same title element, the same runtime-task-title / is-updated class toggle, the same aria-hidden shimmer overlay, and the same two data-testid values. Only the layout utility classes differ.

  • wework/src/components/layout/DesktopSidebar.tsx#L1645-L1658: extract a local renderTaskTitle(extraClassName: string) helper in RuntimeTaskRow and call it here with 'truncate'.
  • wework/src/components/layout/DesktopSidebar.tsx#L1681-L1694: call the same helper here with 'min-w-0 flex-1 truncate' and delete the duplicated markup.

As per coding guidelines: "extract shared logic instead of duplicating it."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/layout/DesktopSidebar.tsx` around lines 1645 - 1658, In
RuntimeTaskRow, extract the duplicated task-title and shimmer markup into a
local renderTaskTitle(extraClassName: string) helper. Update
wework/src/components/layout/DesktopSidebar.tsx lines 1645-1658 to call it with
'truncate', and lines 1681-1694 to call it with 'min-w-0 flex-1 truncate',
removing both duplicated markup blocks while preserving their shared classes,
accessibility attribute, and test IDs.

Source: Coding guidelines


1502-1513: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the runtime task title shimmer duration in a shared constant.

The timer currently uses a hardcoded 760 ms, while .runtime-task-title-shimmer uses runtime-task-title-sheen 760ms. When animation timing changes, update both sources from a single constant, such as RUNTIME_TASK_TITLE_SHIMMER_MS, to keep the overlay lifetime aligned with the CSS animation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/layout/DesktopSidebar.tsx` around lines 1502 - 1513,
The runtime task title shimmer duration is duplicated between the JavaScript
timeout in the task-title useEffect and the `.runtime-task-title-shimmer` CSS
animation. Define a shared RUNTIME_TASK_TITLE_SHIMMER_MS constant and use it for
both the setTimeout delay and the runtime-task-title-sheen animation duration,
preserving their synchronized timing.
wework/src/tauri/appPreferences.test.ts (1)

39-40: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for normalizeFriendlyTaskTitleModel.

The fixture only records the new defaults. The normalizer added in wework/src/tauri/appPreferences.ts contains branch logic: it rejects arrays and non-objects, requires non-empty modelName and executionModelId, restricts model types, and drops non-string options entries. Add cases for an incomplete config, an unsupported modelType, and mixed-type options so regressions in persisted preferences are caught.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/tauri/appPreferences.test.ts` around lines 39 - 40, Add test cases
for normalizeFriendlyTaskTitleModel covering incomplete configurations,
unsupported modelType values, and options arrays containing non-string entries.
Assert invalid configurations are rejected and mixed options are filtered to
strings, while preserving valid modelName and executionModelId requirements.
executor/src/runtime_work/handler/tasks.rs (3)

1121-1133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a truncation boundary test.

The two tests cover quoted multilingual input and whitespace-only input. normalize_friendly_title also truncates to 48 characters and then strips trailing punctuation. Add a case with input longer than 48 characters, including multi-byte characters, so the character-based limit stays verified.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/runtime_work/handler/tasks.rs` around lines 1121 - 1133, Add a
unit test alongside normalizes_model_title_to_one_short_line and
rejects_empty_model_title that passes normalize_friendly_title an input
exceeding 48 Unicode characters, including multi-byte characters, and asserts
the result is truncated to 48 characters before trailing punctuation is removed.
Verify the expected output uses character count rather than byte length.

335-374: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract and alias the friendly-title queueing path.

Move the payload parsing, logging, and task spawning at executor/src/runtime_work/handler/tasks.rs:335-374 into a private helper such as spawn_friendly_title_generation. Also read friendlyTitleExecutionRequest or friendly_title_execution_request so the existing snake_case convention does not silently skip a valid producer payload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/runtime_work/handler/tasks.rs` around lines 335 - 374, Extract
the friendly-title payload parsing, event logging, and tokio task spawning from
the current handler into a private helper such as
spawn_friendly_title_generation, then invoke that helper from the existing flow.
Have the helper accept either friendlyTitleExecutionRequest or
friendly_title_execution_request, while preserving the current success,
rejection, and skipped logging behavior.

30-33: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No change needed for duration handling.

run_codex_app_server_turn_on_shared_client bounds turn startup and notification polling with startup_deadline, and it cancels the turn via cancellation; CodexAppServerTurnOptions does not expose an additional user-supplied timeout field. The main work remains to name the 25/200 polling constants.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/runtime_work/handler/tasks.rs` around lines 30 - 33, Keep
run_codex_app_server_turn_on_shared_client’s existing startup_deadline and
cancellation-based duration handling unchanged; do not add a timeout to
CodexAppServerTurnOptions. Focus the requested change on replacing the nearby
polling constants 25 and 200 with clearly named constants.
wework/src/components/layout/DesktopSidebar.test.tsx (1)

1784-1844: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the shimmer is removed, and align the test name with the implementation term.

The test verifies that is-updated and the shimmer element appear. It does not verify that both are removed after the 760 ms timer in RuntimeTaskRow. A regression that leaves the shimmer permanently visible would pass. Use vi.useFakeTimers(), advance past the duration inside act, and assert removal.

The test name uses "sweeps" while the implementation and test IDs use "shimmer". Use "shimmer" so the same term names the same concept.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/layout/DesktopSidebar.test.tsx` around lines 1784 -
1844, Update the test name for the runtime task title change to use “shimmer”
instead of “sweeps”. In the test around RuntimeTaskLifecycleStore and
RuntimeTaskLifecycleProvider, enable fake timers, advance them past
RuntimeTaskRow’s 760 ms shimmer duration inside act after asserting the updated
state, then assert both the is-updated class and shimmer test element are
removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@executor/src/runtime_work/handler/tasks.rs`:
- Around line 83-132: Replace the final latest_link mutation and
upsert_local_task call in the friendly-title generation flow with an in-place
self.store.update_task mutation. Inside its closure, re-read and verify the
stored task title still equals source_title, then update only title and
updated_at; otherwise return the existing skipped result. Preserve concurrent
fields such as thread_id, runtime_handle mappings, and completed_at, and
propagate or map update errors consistently with the surrounding handler.

In `@wework/src/api/local/localDelivery.ts`:
- Around line 577-586: Update updateTaskTrackingTitle so the
runtime_tasks.context request returns null only when the failure confirms that
the task binding is missing; propagate transient IPC, authorization, and other
unexpected errors instead of catching them as successful no-ops. Preserve the
existing missing-binding behavior while allowing the provider to clear its
signature and retry after non-missing failures.

In `@wework/src/api/local/localServices.ts`:
- Around line 1597-1621: Update the friendlyTitleExecutionRequest construction
in buildLocalRuntimeExecutionRequest to create a text-only request: disable tool
access and omit workspace-related context such as workspacePath,
workspaceSource, and branch. Preserve the existing title prompt and model
configuration while ensuring the user-controlled message cannot operate on the
task workspace.

In `@wework/src/components/settings/GeneralSettingsPage.tsx`:
- Line 303: Update the friendlyTitleModels assignment in GeneralSettingsPage to
filter projectChat.models using the same active and compatibility checks as
supervisorModels: retain only models where isActive is not false and
compatibilityDisabled is not true. Apply the identical filtering to the
additional friendly-title model list referenced by the same component.
- Around line 453-466: Update the select element in the friendly task title
model control to enforce a minimum 44px height on mobile, while preserving its
existing desktop sizing and other classes. Use the responsive min-height utility
pattern already applied to the nearby popout shortcut button.
- Around line 270-285: Update saveFriendlyTaskTitles around selectedModel
resolution to detect when the persisted friendlyTaskTitleModel does not match
any workbench.projectChat.models entry, and report that missing model to the
user instead of silently disabling the setting and clearing it. Preserve the
existing patch behavior for valid models, while using the component’s
established error-message or unavailable-model selector mechanism.

---

Outside diff comments:
In `@wework/src/api/local/localServices.ts`:
- Around line 2521-2524: Update createRuntimeTask to prepare every distinct
configured model before task creation, including data.friendlyTitle.modelId in
addition to data.modelId when present. Reuse prepareRuntimeModel for each unique
ID and throw modelCatalogSyncCancelled if any preparation fails, before
runtime.tasks.create is invoked.

---

Nitpick comments:
In `@executor/src/runtime_work/handler/tasks.rs`:
- Around line 1121-1133: Add a unit test alongside
normalizes_model_title_to_one_short_line and rejects_empty_model_title that
passes normalize_friendly_title an input exceeding 48 Unicode characters,
including multi-byte characters, and asserts the result is truncated to 48
characters before trailing punctuation is removed. Verify the expected output
uses character count rather than byte length.
- Around line 335-374: Extract the friendly-title payload parsing, event
logging, and tokio task spawning from the current handler into a private helper
such as spawn_friendly_title_generation, then invoke that helper from the
existing flow. Have the helper accept either friendlyTitleExecutionRequest or
friendly_title_execution_request, while preserving the current success,
rejection, and skipped logging behavior.
- Around line 30-33: Keep run_codex_app_server_turn_on_shared_client’s existing
startup_deadline and cancellation-based duration handling unchanged; do not add
a timeout to CodexAppServerTurnOptions. Focus the requested change on replacing
the nearby polling constants 25 and 200 with clearly named constants.

In `@wework/src/components/layout/DesktopSidebar.test.tsx`:
- Around line 1784-1844: Update the test name for the runtime task title change
to use “shimmer” instead of “sweeps”. In the test around
RuntimeTaskLifecycleStore and RuntimeTaskLifecycleProvider, enable fake timers,
advance them past RuntimeTaskRow’s 760 ms shimmer duration inside act after
asserting the updated state, then assert both the is-updated class and shimmer
test element are removed.

In `@wework/src/components/layout/DesktopSidebar.tsx`:
- Around line 1645-1658: In RuntimeTaskRow, extract the duplicated task-title
and shimmer markup into a local renderTaskTitle(extraClassName: string) helper.
Update wework/src/components/layout/DesktopSidebar.tsx lines 1645-1658 to call
it with 'truncate', and lines 1681-1694 to call it with 'min-w-0 flex-1
truncate', removing both duplicated markup blocks while preserving their shared
classes, accessibility attribute, and test IDs.
- Around line 1502-1513: The runtime task title shimmer duration is duplicated
between the JavaScript timeout in the task-title useEffect and the
`.runtime-task-title-shimmer` CSS animation. Define a shared
RUNTIME_TASK_TITLE_SHIMMER_MS constant and use it for both the setTimeout delay
and the runtime-task-title-sheen animation duration, preserving their
synchronized timing.

In `@wework/src/tauri/appPreferences.test.ts`:
- Around line 39-40: Add test cases for normalizeFriendlyTaskTitleModel covering
incomplete configurations, unsupported modelType values, and options arrays
containing non-string entries. Assert invalid configurations are rejected and
mixed options are filtered to strings, while preserving valid modelName and
executionModelId requirements.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d688d48-31a9-4221-8223-07a26f9582b0

📥 Commits

Reviewing files that changed from the base of the PR and between 8ede6c8 and 2e60333.

📒 Files selected for processing (36)
  • docs/en/wework/settings.md
  • docs/zh/wework/settings.md
  • executor/src/runtime_work/handler/tasks.rs
  • wework/e2e/desktop/task-flow.e2e.mjs
  • wework/src-tauri/src/lib.rs
  • wework/src/App.plugins.test.tsx
  • wework/src/api/deliveries.test.ts
  • wework/src/api/deliveries.ts
  • wework/src/api/hybrid/hybridServices.ts
  • wework/src/api/local/codexOfficialModels.ts
  • wework/src/api/local/localDelivery.test.ts
  • wework/src/api/local/localDelivery.ts
  • wework/src/api/local/localServices.test.ts
  • wework/src/api/local/localServices.ts
  • wework/src/api/runtime/runtimeChatStream.ts
  • wework/src/components/layout/DesktopSidebar.test.tsx
  • wework/src/components/layout/DesktopSidebar.tsx
  • wework/src/components/layout/DesktopWorkbenchMain.tsx
  • wework/src/components/settings/GeneralSettingsPage.tsx
  • wework/src/components/settings/ModelSettingsPage.tsx
  • wework/src/features/model-settings/codexOfficialModels.test.ts
  • wework/src/features/model-settings/codexOfficialModels.ts
  • wework/src/features/workbench/WorkbenchProvider.test.tsx
  • wework/src/features/workbench/WorkbenchProvider.tsx
  • wework/src/features/workbench/runtimePaneMessages.test.ts
  • wework/src/features/workbench/runtimePaneMessages.ts
  • wework/src/features/workbench/useWorkbenchRuntimeMessaging.ts
  • wework/src/features/workbench/workbenchReducer.test.ts
  • wework/src/features/workbench/workbenchReducer.ts
  • wework/src/stream/chatStream.ts
  • wework/src/stream/responseApiStream.test.ts
  • wework/src/stream/responseApiStream.ts
  • wework/src/styles/globals.css
  • wework/src/tauri/appPreferences.test.ts
  • wework/src/tauri/appPreferences.ts
  • wework/src/types/api.ts
💤 Files with no reviewable changes (2)
  • wework/src/components/settings/ModelSettingsPage.tsx
  • wework/e2e/desktop/task-flow.e2e.mjs

Comment on lines +83 to +132
let mut latest_link = self.task_link_from_payload(&payload, false).await?;
for _ in 0..25 {
if latest_link.title != source_title {
log_executor_event(
"friendly task title generation skipped",
&[
("local_task_id", local_task_id.clone()),
("reason", "title_changed_during_generation".to_owned()),
],
);
return Ok(json!({"success": true, "skipped": true}));
}
if latest_link.thread_id.is_some() {
break;
}
sleep(Duration::from_millis(200)).await;
latest_link = self.task_link_from_payload(&payload, false).await?;
}
if latest_link.title != source_title {
log_executor_event(
"friendly task title generation skipped",
&[
("local_task_id", local_task_id.clone()),
("reason", "title_changed_before_update".to_owned()),
],
);
return Ok(json!({"success": true, "skipped": true}));
}
if let Some(thread_id) = latest_link.thread_id.as_deref() {
if let Err(error) = self
.call_codex_thread_method(
"thread/name/set",
json!({"threadId": thread_id, "name": title}),
)
.await
{
log_executor_event(
"friendly task title generation failed",
&[
("local_task_id", local_task_id.clone()),
("reason", "thread_name_update_failed".to_owned()),
("error", error.clone()),
],
);
return Ok(json!({"success": false, "error": error}));
}
}
latest_link.title = title.clone();
latest_link.updated_at = now_ms();
self.upsert_local_task(latest_link);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Replace the whole-link upsert with an in-place update_task mutation.

latest_link is read at Line 99 (or Line 83) and written back in full at Line 132. Friendly-title generation runs concurrently with the main turn spawned by create_task, and that turn mutates the same stored link through mark_task_running_for_send and record_runtime_turn_id. Any field those paths write between the read and upsert_local_task is reverted by this stale snapshot, including thread_id, runtime_handle turn mappings, updated_at, and completed_at. The title check at Line 101 is also a time-of-check/time-of-use gap: a rename that lands after the check is overwritten.

Mutate only the title through self.store.update_task, and re-verify the source title inside the closure so the guard and the write are atomic.

🐛 Proposed fix
-        latest_link.title = title.clone();
-        latest_link.updated_at = now_ms();
-        self.upsert_local_task(latest_link);
+        let applied = self
+            .store
+            .update_task(&local_task_id, |link| {
+                if link.title != source_title {
+                    return;
+                }
+                link.title = title.clone();
+                link.updated_at = now_ms();
+            })
+            .is_some();
+        if !applied {
+            log_executor_event(
+                "friendly task title generation skipped",
+                &[
+                    ("local_task_id", local_task_id.clone()),
+                    ("reason", "task_missing_before_update".to_owned()),
+                ],
+            );
+            return Ok(json!({"success": true, "skipped": true}));
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let mut latest_link = self.task_link_from_payload(&payload, false).await?;
for _ in 0..25 {
if latest_link.title != source_title {
log_executor_event(
"friendly task title generation skipped",
&[
("local_task_id", local_task_id.clone()),
("reason", "title_changed_during_generation".to_owned()),
],
);
return Ok(json!({"success": true, "skipped": true}));
}
if latest_link.thread_id.is_some() {
break;
}
sleep(Duration::from_millis(200)).await;
latest_link = self.task_link_from_payload(&payload, false).await?;
}
if latest_link.title != source_title {
log_executor_event(
"friendly task title generation skipped",
&[
("local_task_id", local_task_id.clone()),
("reason", "title_changed_before_update".to_owned()),
],
);
return Ok(json!({"success": true, "skipped": true}));
}
if let Some(thread_id) = latest_link.thread_id.as_deref() {
if let Err(error) = self
.call_codex_thread_method(
"thread/name/set",
json!({"threadId": thread_id, "name": title}),
)
.await
{
log_executor_event(
"friendly task title generation failed",
&[
("local_task_id", local_task_id.clone()),
("reason", "thread_name_update_failed".to_owned()),
("error", error.clone()),
],
);
return Ok(json!({"success": false, "error": error}));
}
}
latest_link.title = title.clone();
latest_link.updated_at = now_ms();
self.upsert_local_task(latest_link);
let mut latest_link = self.task_link_from_payload(&payload, false).await?;
for _ in 0..25 {
if latest_link.title != source_title {
log_executor_event(
"friendly task title generation skipped",
&[
("local_task_id", local_task_id.clone()),
("reason", "title_changed_during_generation".to_owned()),
],
);
return Ok(json!({"success": true, "skipped": true}));
}
if latest_link.thread_id.is_some() {
break;
}
sleep(Duration::from_millis(200)).await;
latest_link = self.task_link_from_payload(&payload, false).await?;
}
if latest_link.title != source_title {
log_executor_event(
"friendly task title generation skipped",
&[
("local_task_id", local_task_id.clone()),
("reason", "title_changed_before_update".to_owned()),
],
);
return Ok(json!({"success": true, "skipped": true}));
}
if let Some(thread_id) = latest_link.thread_id.as_deref() {
if let Err(error) = self
.call_codex_thread_method(
"thread/name/set",
json!({"threadId": thread_id, "name": title}),
)
.await
{
log_executor_event(
"friendly task title generation failed",
&[
("local_task_id", local_task_id.clone()),
("reason", "thread_name_update_failed".to_owned()),
("error", error.clone()),
],
);
return Ok(json!({"success": false, "error": error}));
}
}
let applied = self
.store
.update_task(&local_task_id, |link| {
if link.title != source_title {
return;
}
link.title = title.clone();
link.updated_at = now_ms();
})
.is_some();
if !applied {
log_executor_event(
"friendly task title generation skipped",
&[
("local_task_id", local_task_id.clone()),
("reason", "task_missing_before_update".to_owned()),
],
);
return Ok(json!({"success": true, "skipped": true}));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/src/runtime_work/handler/tasks.rs` around lines 83 - 132, Replace
the final latest_link mutation and upsert_local_task call in the friendly-title
generation flow with an in-place self.store.update_task mutation. Inside its
closure, re-read and verify the stored task title still equals source_title,
then update only title and updated_at; otherwise return the existing skipped
result. Preserve concurrent fields such as thread_id, runtime_handle mappings,
and completed_at, and propagate or map update errors consistently with the
surrounding handler.

Comment on lines +577 to +586
async updateTaskTrackingTitle(task: RuntimeTaskAddress, title: string) {
let binding: LocalTaskBindingRecord
try {
binding = await request<LocalTaskBindingRecord>('runtime_tasks.context', {
device_id: task.deviceId,
task_id: task.taskId,
})
} catch {
return null
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not convert IPC failures into successful no-ops.

Line 584 returns null for every runtime_tasks.context failure. The provider treats that result as fulfilled and retains the title signature. A transient IPC or authorization failure then prevents retrying the same title.

Return null only for a confirmed missing binding. Propagate other failures so the provider clears its signature and retries on a later event.

As per coding guidelines, “correct the primary path” and do not hide defects behind fallbacks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/api/local/localDelivery.ts` around lines 577 - 586, Update
updateTaskTrackingTitle so the runtime_tasks.context request returns null only
when the failure confirms that the task binding is missing; propagate transient
IPC, authorization, and other unexpected errors instead of catching them as
successful no-ops. Preserve the existing missing-binding behavior while allowing
the provider to clear its signature and retry after non-missing failures.

Source: Coding guidelines

Comment on lines +1597 to +1621
const friendlyTitleExecutionRequest = normalizedData.friendlyTitle
? buildLocalRuntimeExecutionRequest({
taskId: `friendly-title-${normalizedData.taskId ?? turnSeed}-${createRuntimeTurnSeed()}`,
runtime: 'codex',
teamId: normalizedData.teamId,
title: 'Generate friendly task title',
message: [
'为下面的用户请求生成一个简洁、具体、适合作为任务标题的中文标题。',
'只输出标题本身,不要引号、标点、解释或换行;最多 24 个汉字。',
'',
`用户请求:${normalizedData.message}`,
].join('\n'),
turnSeed: createRuntimeTurnSeed(),
modelId: normalizedData.friendlyTitle.modelId,
modelType: normalizedData.friendlyTitle.modelType,
modelOptions: normalizedData.friendlyTitle.modelOptions,
cloudModelGateway,
localDeviceId,
workspacePath: runtimeWorkspace.workspacePath,
workspaceSource: runtimeWorkspace.workspaceSource,
branch: runtimeWorkspace.branch,
newSession: true,
ephemeral: true,
user,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Disable tools for the friendly-title request.

Line 1607 inserts user-controlled text into a second model turn. buildLocalRuntimeExecutionRequest enables tools at Line 1457 and attaches the task workspace at Lines 1462-1470. A prompt-injected title request can therefore operate on the user workspace.

Create a text-only title request. Disable tools and omit workspace access for this request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/api/local/localServices.ts` around lines 1597 - 1621, Update the
friendlyTitleExecutionRequest construction in buildLocalRuntimeExecutionRequest
to create a text-only request: disable tool access and omit workspace-related
context such as workspacePath, workspaceSource, and branch. Preserve the
existing title prompt and model configuration while ensuring the user-controlled
message cannot operate on the task workspace.

Comment on lines +270 to +285
const selectedModel = workbench?.projectChat.models.find(
model => `${model.type}:${model.name}` === `${modelType ?? ''}:${modelName}`
)
const execution = selectedModel ? selectedModelExecutionFields(selectedModel, {}) : null
const patch: AppPreferencesPatch = {
friendlyTaskTitlesEnabled: enabled && Boolean(selectedModel),
friendlyTaskTitleModel: selectedModel
? {
modelName: selectedModel.name,
modelType: selectedModel.type,
executionModelId: execution?.modelId ?? '',
executionModelType: execution?.modelType ?? null,
options: execution?.modelOptions,
}
: null,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Report a message when the stored title model no longer exists.

saveFriendlyTaskTitles resolves selectedModel from workbench?.projectChat.models. If the persisted friendlyTaskTitleModel refers to a model that is no longer listed, the patch writes friendlyTaskTitlesEnabled: false and friendlyTaskTitleModel: null. The switch then returns to the off position and the user receives no explanation. Set an error message in that case, or keep the selector open with the missing model shown as unavailable.

🛠️ Proposed fix
     const execution = selectedModel ? selectedModelExecutionFields(selectedModel, {}) : null
+    if (enabled && !selectedModel) {
+      setError(t('workbench.friendly_task_titles_model_missing', '标题模型不可用,请重新选择模型。'))
+    }
     const patch: AppPreferencesPatch = {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const selectedModel = workbench?.projectChat.models.find(
model => `${model.type}:${model.name}` === `${modelType ?? ''}:${modelName}`
)
const execution = selectedModel ? selectedModelExecutionFields(selectedModel, {}) : null
const patch: AppPreferencesPatch = {
friendlyTaskTitlesEnabled: enabled && Boolean(selectedModel),
friendlyTaskTitleModel: selectedModel
? {
modelName: selectedModel.name,
modelType: selectedModel.type,
executionModelId: execution?.modelId ?? '',
executionModelType: execution?.modelType ?? null,
options: execution?.modelOptions,
}
: null,
}
const selectedModel = workbench?.projectChat.models.find(
model => `${model.type}:${model.name}` === `${modelType ?? ''}:${modelName}`
)
const execution = selectedModel ? selectedModelExecutionFields(selectedModel, {}) : null
if (enabled && !selectedModel) {
setError(t('workbench.friendly_task_titles_model_missing', '标题模型不可用,请重新选择模型。'))
}
const patch: AppPreferencesPatch = {
friendlyTaskTitlesEnabled: enabled && Boolean(selectedModel),
friendlyTaskTitleModel: selectedModel
? {
modelName: selectedModel.name,
modelType: selectedModel.type,
executionModelId: execution?.modelId ?? '',
executionModelType: execution?.modelType ?? null,
options: execution?.modelOptions,
}
: null,
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/settings/GeneralSettingsPage.tsx` around lines 270 -
285, Update saveFriendlyTaskTitles around selectedModel resolution to detect
when the persisted friendlyTaskTitleModel does not match any
workbench.projectChat.models entry, and report that missing model to the user
instead of silently disabling the setting and clearing it. Preserve the existing
patch behavior for valid models, while using the component’s established
error-message or unavailable-model selector mechanism.


const friendlyTitleModel = preferences.friendlyTaskTitleModel
const friendlyTitleModelKey = `${friendlyTitleModel?.modelType ?? ''}:${friendlyTitleModel?.modelName ?? ''}`
const friendlyTitleModels = workbench?.projectChat.models ?? []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Filter unusable models out of the title model list.

friendlyTitleModels passes every entry of workbench.projectChat.models to the selector. DesktopWorkbenchMain.tsx builds supervisorModels with model.isActive !== false && !model.compatibilityDisabled for the same catalog. Without that filter, the user can select an inactive or compatibility-disabled model, and every asynchronous title turn then fails at the executor.

♻️ Proposed fix
-  const friendlyTitleModels = workbench?.projectChat.models ?? []
+  const friendlyTitleModels = (workbench?.projectChat.models ?? []).filter(
+    model => model.isActive !== false && !model.compatibilityDisabled
+  )

Also applies to: 471-478

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/settings/GeneralSettingsPage.tsx` at line 303, Update
the friendlyTitleModels assignment in GeneralSettingsPage to filter
projectChat.models using the same active and compatibility checks as
supervisorModels: retain only models where isActive is not false and
compatibilityDisabled is not true. Apply the identical filtering to the
additional friendly-title model list referenced by the same component.

Comment on lines +453 to +466
<select
data-testid="friendly-task-title-model-select"
value={friendlyTitleModelKey}
disabled={loading || saving}
onChange={event => {
const [modelType, ...nameParts] = event.target.value.split(':')
const modelName = nameParts.join(':')
void saveFriendlyTaskTitles(
preferences.friendlyTaskTitlesEnabled && Boolean(modelName),
modelName,
modelName ? (modelType as UnifiedModel['type']) : null
)
}}
className="h-8 w-full rounded-md border border-border bg-background px-2 text-sm text-text-primary md:w-[220px]"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Raise the select height on mobile to at least 44px.

The <select> uses h-8 (32px). On mobile the row switches to max-sm:flex-col max-sm:items-stretch, so this control becomes the primary touch target at 32px height. The popout shortcut button in this same file applies max-md:min-h-11 for this reason.

🛠️ Proposed fix
-                  className="h-8 w-full rounded-md border border-border bg-background px-2 text-sm text-text-primary md:w-[220px]"
+                  className="h-8 w-full rounded-md border border-border bg-background px-2 text-sm text-text-primary max-md:h-11 md:w-[220px]"

As per coding guidelines: "Mobile controls must be at least 44px × 44px."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<select
data-testid="friendly-task-title-model-select"
value={friendlyTitleModelKey}
disabled={loading || saving}
onChange={event => {
const [modelType, ...nameParts] = event.target.value.split(':')
const modelName = nameParts.join(':')
void saveFriendlyTaskTitles(
preferences.friendlyTaskTitlesEnabled && Boolean(modelName),
modelName,
modelName ? (modelType as UnifiedModel['type']) : null
)
}}
className="h-8 w-full rounded-md border border-border bg-background px-2 text-sm text-text-primary md:w-[220px]"
<select
data-testid="friendly-task-title-model-select"
value={friendlyTitleModelKey}
disabled={loading || saving}
onChange={event => {
const [modelType, ...nameParts] = event.target.value.split(':')
const modelName = nameParts.join(':')
void saveFriendlyTaskTitles(
preferences.friendlyTaskTitlesEnabled && Boolean(modelName),
modelName,
modelName ? (modelType as UnifiedModel['type']) : null
)
}}
className="h-8 w-full rounded-md border border-border bg-background px-2 text-sm text-text-primary max-md:h-11 md:w-[220px]"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/settings/GeneralSettingsPage.tsx` around lines 453 -
466, Update the select element in the friendly task title model control to
enforce a minimum 44px height on mobile, while preserving its existing desktop
sizing and other classes. Use the responsive min-height utility pattern already
applied to the nearby popout shortcut button.

Source: Coding guidelines

@qdaxb
qdaxb added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit b30058e Aug 7, 2026
50 checks passed
@qdaxb
qdaxb deleted the feature/wework-friendly-task-titles branch August 7, 2026 04:33
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