Files
ironclaw/crates
Illia Polosukhin 22fa85b670 feat(engine): add short title field to v2 threads for sidebar labels (#2776)
* feat(engine): add short title field to v2 threads for sidebar labels

`Thread.goal` is the execution prompt — a multi-paragraph meta-prompt
for missions, or the full first user message for gateway chats. Reusing
it as the sidebar label makes the conversation list expand to fit the
longest prompt in view.

Split the two concerns:

- `Thread.title: Option<String>` (with `#[serde(default)]`) for the
  compact human label; legacy rows without the field rehydrate cleanly
  as None.
- Threaded a `title` parameter through `ThreadManager::spawn_thread_with_history`
  and added a `spawn_thread_with_title` wrapper; title is applied before
  `save_thread` so the executor's in-memory copy observes it atomically.
- Mission-spawned threads pass `Some(mission.name)`; gateway conversation
  threads pass `Thread::derive_title_from_message(content)` (first
  non-empty line, trimmed, char-safe truncated to 60 with an ellipsis).
- `EngineThreadInfo` carries the new field; `chat_threads_handler` prefers
  it and falls back to a derived short label for pre-existing threads.
- Belt-and-braces CSS truncation on `.thread-label` so the sidebar can
  never bleed across the page even if a title somehow slips through long.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(engine): streaming title truncation + docstring/test-name fix

Addresses review feedback on PR #2776:

- Drop the unreachable `trimmed.is_empty()` guard — the `find`
  predicate already guarantees the line is non-empty after trim.
- Replace `trimmed.chars().count()` + re-iterate with a single
  streaming pass: take up to MAX_CHARS-1 chars, peek the rest,
  and append either the final char (no ellipsis, result is
  MAX_CHARS) or '…'. Avoids an O(n) scan on pathological
  single-line input.
- Correct the docstring to say "leading and trailing whitespace"
  so it matches `trim()`, which is the right behavior for a
  sidebar label.
- Rename `derive_title_trims_trailing_whitespace` to
  `derive_title_trims_whitespace` since the test input has
  whitespace on both ends.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(bridge): assert thread_to_info carries title and goal separately

Addresses self-review finding on PR #2776: the `EngineThreadInfo` wire
contract gained a `title` field but nothing in the Rust tree directly
exercised the DTO populator after upstream dropped the sidebar
engine-thread merging. Adds two small tests that build a `Thread` with
and without a title and assert `thread_to_info` passes both `title` and
`goal` through independently — so mission DTOs can render the short
label without reading the multi-paragraph meta-prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(bridge): propagate title through EngineThreadInfo and archive roundtrip

Addresses two medium-severity review findings on PR #2776 (same class of
bug — a new `Thread.title` field was added without propagation to
satellite types, per `.claude/rules/review-discipline.md`):

1. `thread_to_info` now falls back to deriving a short label from
   `goal` when `title` is `None`. Without this, legacy engine threads
   persisted before the `title` field existed flow through to frontends
   (TUI, mission detail views) as `title = None`, and the frontend
   `threadTitle()` fallback chain in `history.js` renders a UUID
   prefix because `EngineThreadInfo` has no `turn_count`.

2. `ThreadArchiveSummary` now carries `title` (with `#[serde(default)]`
   for backwards compatibility, mirroring the `total_cost_usd`
   precedent at #2562). `compact_thread_summary` persists it,
   `thread_from_archive` reads it. `backfill_archived_threads` is a
   live consumer — without this, workspaces that only have archived
   summaries still rehydrate threads with no title.

Tests:
- `thread_to_info_derives_title_from_goal_when_absent`
- `thread_to_info_derives_from_first_line_of_long_goal`
- `archive_summary_preserves_title_through_round_trip`
- `archive_summary_handles_legacy_json_without_title_field`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 02:06:29 +09:00
..