You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two related defects in how the Web UI handles Content shapes and exposes per-message capabilities:
Categorization gap: categorizeMessage in packages/web/src/lib/utils/message.ts originally assumed Content was always ContentItem[], missing the string and single-ContentItem shapes. This part was resolved by PR fix(web): normalize Content shapes before message categorization #166 (normalizeContent helper).
Capability gap (load-bearing concern): per-message capability checks in MessageItem.svelte are ad-hoc derived (isToolResult, hasNonTextBlocks, isEditable) and conflate "which ContentItem.type is this" with "which capability does it support". Result: editable types like tool_result and thinking are blanket-blocked from editing because the assistant-side tool_use pairing concern bled into all non-text shapes. The intent is a type-aware capability API that lets each ContentItem.type opt into edit/delete/copy/export/convert/extract independently.
Origin
CodeRabbit review on PR #97 (6d447a6) flagged scope (1). User testing surfaced scope (2) as the load-bearing intent — categorization alone is "tidy refactoring," but the actual user-facing block is "I cannot edit my tool_result messages."
Scope (a) — Categorize all Content shapes (resolved)
Resolved by PR #166 (merged 2026-06-06 → beta, squash commit bb7f3d6). Not yet synced to main.
Scope (b) — Type-aware capability API for editable content (open)
Replace the ad-hoc isEditable derived in MessageItem.svelte with a getCapabilities(content: Content): Capabilities function in packages/web/src/lib/utils/message.ts (or a sibling module). The function returns per-message capability booleans.
Target types (editable side)
ContentItem.type is open (api.tsContentItem.type: string). The capability matrix covers the types currently observed in session JSONL:
Type
Edit
Delete
Copy
Export
Convert
Extract
text (human/user/assistant)
✅
✅
✅
✅
—
—
tool_result (user side)
✅ (with tool_use_id preservation)
✅
✅
✅
✅ (e.g. tool_result → text)
✅ (extract stdout/stderr/content)
thinking (assistant side)
✅
✅
✅
✅
✅ (thinking → text)
—
Explicitly excluded:
tool_use (assistant output) — editing breaks tool_use ↔ tool_result pairing invariant. Delete + copy may still apply.
image, document — not currently observed in session JSONL; revisit when they appear.
Capability API shape (proposed)
exportinterfaceCapabilities{canEdit: booleancanDelete: booleancanCopy: booleancanExport: booleancanConvert: booleancanExtract: boolean}exportconstgetCapabilities=(msg: Message): Capabilities=>{// Use normalizeContent(msg.message.content) to get ContentItem[].// Inspect items[0].type (and items.some(...)) to drive each boolean.// tool_use pairing constraint stays inside this function, not bled into UI.}
MessageItem.svelte consumes getCapabilities(msg) and renders edit/delete/copy/export/convert/extract affordances accordingly. No more ad-hoc isEditable, isToolResult, hasNonTextBlocks in the component.
Problem
Two related defects in how the Web UI handles
Contentshapes and exposes per-message capabilities:categorizeMessageinpackages/web/src/lib/utils/message.tsoriginally assumedContentwas alwaysContentItem[], missing thestringand single-ContentItemshapes. This part was resolved by PR fix(web): normalize Content shapes before message categorization #166 (normalizeContenthelper).MessageItem.svelteare ad-hoc derived (isToolResult,hasNonTextBlocks,isEditable) and conflate "whichContentItem.typeis this" with "which capability does it support". Result: editable types liketool_resultandthinkingare blanket-blocked from editing because the assistant-sidetool_usepairing concern bled into all non-text shapes. The intent is a type-aware capability API that lets eachContentItem.typeopt into edit/delete/copy/export/convert/extract independently.Origin
CodeRabbit review on PR #97 (
6d447a6) flagged scope (1). User testing surfaced scope (2) as the load-bearing intent — categorization alone is "tidy refactoring," but the actual user-facing block is "I cannot edit my tool_result messages."Scope (a) — Categorize all Content shapes (resolved)
Resolved by PR #166 (merged 2026-06-06 →
beta, squash commitbb7f3d6). Not yet synced tomain.Acceptance Criteria for (a)
msg.message.content) correctly categorized — PR fix(web): normalize Content shapes before message categorization #166Scope (b) — Type-aware capability API for editable content (open)
Replace the ad-hoc
isEditablederived inMessageItem.sveltewith agetCapabilities(content: Content): Capabilitiesfunction inpackages/web/src/lib/utils/message.ts(or a sibling module). The function returns per-message capability booleans.Target types (editable side)
ContentItem.typeis open (api.tsContentItem.type: string). The capability matrix covers the types currently observed in session JSONL:text(human/user/assistant)tool_result(user side)tool_use_idpreservation)stdout/stderr/content)thinking(assistant side)Explicitly excluded:
tool_use(assistant output) — editing breakstool_use ↔ tool_resultpairing invariant. Delete + copy may still apply.image,document— not currently observed in session JSONL; revisit when they appear.Capability API shape (proposed)
MessageItem.svelteconsumesgetCapabilities(msg)and renders edit/delete/copy/export/convert/extract affordances accordingly. No more ad-hocisEditable,isToolResult,hasNonTextBlocksin the component.Acceptance Criteria for (b)
getCapabilities(msg)implemented + unit-tested for each (type × capability) combination from the matrix above. (PR fix(core): type-aware message editing — unlock tool_result/thinking editing (issue #123) #200)MessageItem.svelteisEditable/isToolResult/hasNonTextBlocksderived removed; UI consumesgetCapabilities(msg)instead. (PR fix(core): type-aware message editing — unlock tool_result/thinking editing (issue #123) #200)tool_resultmessages render an edit affordance, preservingtool_use_idandis_errorfields on save. (PR fix(core): type-aware message editing — unlock tool_result/thinking editing (issue #123) #200)thinkingmessages render an edit affordance. (PR fix(core): type-aware message editing — unlock tool_result/thinking editing (issue #123) #200)packages/core/src/session/crud.ts editMessageContent) is tool_result-aware and thinking-aware (no silent structured-data drop). (PR fix(core): type-aware message editing — unlock tool_result/thinking editing (issue #123) #200)tool_result → text,thinking → text) is wired to a UI action + backend path.stdout/stderr/contentfromtool_resultinto a copyable text block.tool_use ↔ tool_resultverifies the pairing invariant is preserved across edit, delete, convert.~/ghq/github.com/es6kr/.ralph/docs/generated/plan-issue-123-content-handling-generalization.md).Out of scope
tool_useediting (intentional — pairing invariant).image/documentcontent (revisit when they appear in session JSONL).