refine the prompt/log UX: show commands, de-emphasize injects

Two spot-check fixes (T-178, T-179), both grounded in a boundary test of
the stream-json wire (findings folded into the spike doc):

- Harness-injected user messages (skill loads, slash-command expansions,
  system reminders) carry isSynthetic on the wire (isMeta in the
  transcript). They were rendering as blue "you" cards though the user
  never typed them; now UserMessage.injected flags them and the view
  shows a muted, collapsed "context" card instead.
- Permission prompts now show the command/input being permitted (a
  capped, scrollable code block) so you can see what you approve. Instead
  of fully hiding a prompted tool-use, once resolved it collapses to a
  one-line summary with a green (approved) or red (denied) border; the
  session tracks per-tool_use_id outcome and the view colours it. The
  result is kept.

Corrects an earlier wrong assumption: the Skill tool is auto-allowed
(no permission prompt); the inject only appears once the Skill tool is
actually invoked, which is why deny-captures missed it.

T-178, T-179, D-78.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 10:26:33 +02:00
co-authored by Claude Opus 4.7
parent c30a707b41
commit 2243237b13
12 changed files with 189 additions and 32 deletions
+14 -3
View File
@@ -53,13 +53,19 @@ final class UserMessage extends ConversationItem {
required super.timestamp,
required super.isSidechain,
required this.text,
this.injected = false,
});
/// The concatenated text of all `text` parts in the content array.
final String text;
/// True when this "user" message was injected by the harness (a skill load,
/// a slash-command expansion, a system reminder) rather than typed by the
/// user — the view de-emphasises these (D-78).
final bool injected;
@override
String toString() => 'UserMessage(${_shortId(uuid)}, ${text.length} chars)';
String toString() => 'UserMessage(${_shortId(uuid)}, ${text.length} chars${injected ? ', injected' : ''})';
}
/// A tool-result delivered from the host back to Claude as a user message.
@@ -520,9 +526,14 @@ void _parseUserInto(
if (message == null) return;
final content = message['content'];
// Harness-injected user messages (skill loads, slash-command expansions,
// system reminders) — `isSynthetic` on the stream-json wire, `isMeta` in the
// transcript. The view de-emphasises these (D-78).
final injected = envelope['isSynthetic'] == true || envelope['isMeta'] == true;
if (content is String) {
if (content.isNotEmpty) {
out.add(UserMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, text: content));
out.add(UserMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, text: content, injected: injected));
}
return;
}
@@ -550,7 +561,7 @@ void _parseUserInto(
}
}
if (textParts.isNotEmpty) {
out.add(UserMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, text: textParts.join('\n')));
out.add(UserMessage(uuid: uuid, timestamp: timestamp, isSidechain: isSidechain, text: textParts.join('\n'), injected: injected));
}
}