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:
@@ -26,15 +26,21 @@ class ConversationView extends StatefulWidget {
|
||||
this.wrapInSelectionArea = true,
|
||||
this.emptyState,
|
||||
this.hiddenToolUseIds = const <String>{},
|
||||
this.toolUseOutcomes = const <String, bool>{},
|
||||
});
|
||||
|
||||
final ConversationController controller;
|
||||
|
||||
/// tool_use_ids whose raw tool-use card should be hidden because the call
|
||||
/// surfaced as a prompt (permission / AskUserQuestion) — D-78. The result is
|
||||
/// still shown (it's the useful answer); only the request payload is hidden.
|
||||
/// tool_use_ids that surfaced as a prompt (permission / AskUserQuestion) —
|
||||
/// D-78. While pending (not in [toolUseOutcomes]) the raw tool-use card is
|
||||
/// hidden (it shows as a prompt). The result is always kept.
|
||||
final Set<String> hiddenToolUseIds;
|
||||
|
||||
/// Resolved outcome per prompted tool_use_id (true = allowed, false = denied)
|
||||
/// — a resolved permission tool-use renders collapsed with a green/red
|
||||
/// border instead of being hidden (D-78).
|
||||
final Map<String, bool> toolUseOutcomes;
|
||||
|
||||
/// Whether to wrap the list in its own [ClideSelectionArea]. The team
|
||||
/// grid sets this false and wraps all tiles in one shared area so
|
||||
/// selection spans tiles — nesting SelectionAreas is illegal (T-140).
|
||||
@@ -83,8 +89,14 @@ class _ConversationViewState extends State<ConversationView> {
|
||||
for (final it in items)
|
||||
if (it is AssistantToolUse && it.name == 'AskUserQuestion') it.toolUseId,
|
||||
};
|
||||
final outcomes = widget.toolUseOutcomes;
|
||||
bool drop(ConversationItem it) {
|
||||
if (it is AssistantToolUse) return it.name == 'AskUserQuestion' || hidden.contains(it.toolUseId);
|
||||
if (it is AssistantToolUse) {
|
||||
if (it.name == 'AskUserQuestion') return true;
|
||||
// Permission-prompted: hide only while pending; once resolved it shows
|
||||
// collapsed with a green/red border.
|
||||
return hidden.contains(it.toolUseId) && !outcomes.containsKey(it.toolUseId);
|
||||
}
|
||||
if (it is ToolResultMessage) return auqIds.contains(it.toolUseId); // AUQ result only; keep permission results
|
||||
return false;
|
||||
}
|
||||
@@ -124,7 +136,7 @@ class _ConversationViewState extends State<ConversationView> {
|
||||
controller: _scroll,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, i) => _ConversationTurn(item: items[i], tokens: tokens),
|
||||
itemBuilder: (context, i) => _ConversationTurn(item: items[i], tokens: tokens, toolUseOutcomes: widget.toolUseOutcomes),
|
||||
),
|
||||
);
|
||||
return ColoredBox(
|
||||
@@ -140,21 +152,36 @@ const claudeAccent = Color(0xFFD97757);
|
||||
|
||||
/// One conversation item, rendered by kind.
|
||||
class _ConversationTurn extends StatelessWidget {
|
||||
const _ConversationTurn({required this.item, required this.tokens});
|
||||
const _ConversationTurn({required this.item, required this.tokens, this.toolUseOutcomes = const <String, bool>{}});
|
||||
|
||||
final ConversationItem item;
|
||||
final SurfaceTokens tokens;
|
||||
final Map<String, bool> toolUseOutcomes;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i = item;
|
||||
return switch (i) {
|
||||
UserMessage() => ConversationCard(
|
||||
accent: tokens.globalFocus,
|
||||
label: 'you',
|
||||
copyText: i.text,
|
||||
body: ClideMarkdown(i.text),
|
||||
),
|
||||
UserMessage() => i.injected
|
||||
// Harness-injected (skill load / command expansion / system
|
||||
// reminder) — not typed by the user, so de-emphasise: a muted,
|
||||
// collapsed "context" card rather than the "you" accent (D-78).
|
||||
? ConversationCard(
|
||||
variant: ConversationCardVariant.bare,
|
||||
accent: tokens.globalTextMuted,
|
||||
label: 'context',
|
||||
copyText: i.text,
|
||||
collapsible: true,
|
||||
collapsedByDefault: true,
|
||||
collapsedSummary: _firstLine(i.text),
|
||||
body: ClideText(i.text, muted: true, fontSize: clideFontMeta),
|
||||
)
|
||||
: ConversationCard(
|
||||
accent: tokens.globalFocus,
|
||||
label: 'you',
|
||||
copyText: i.text,
|
||||
body: ClideMarkdown(i.text),
|
||||
),
|
||||
AssistantTextMessage() => ConversationCard(
|
||||
accent: claudeAccent,
|
||||
label: 'claude',
|
||||
@@ -177,6 +204,23 @@ class _ConversationTurn extends StatelessWidget {
|
||||
|
||||
Widget _toolUse(AssistantToolUse t) {
|
||||
final pretty = const JsonEncoder.withIndent(' ').convert(t.input);
|
||||
// A resolved permission-prompted call: collapsed, green if approved / red
|
||||
// if denied — a quiet record of what was permitted (D-78).
|
||||
final outcome = toolUseOutcomes[t.toolUseId];
|
||||
if (outcome != null) {
|
||||
final color = outcome ? tokens.statusSuccess : tokens.statusError;
|
||||
return ConversationCard(
|
||||
variant: ConversationCardVariant.bordered,
|
||||
accent: color,
|
||||
borderColor: color,
|
||||
label: t.name,
|
||||
copyText: pretty,
|
||||
collapsible: true,
|
||||
collapsedByDefault: true,
|
||||
collapsedSummary: _toolUseSummary(t),
|
||||
body: ClideCodeBlock(source: pretty, language: 'json'),
|
||||
);
|
||||
}
|
||||
// Collapse only the bulky multi-line form; a trivial one-liner just shows.
|
||||
final multiline = pretty.contains('\n');
|
||||
return ConversationCard(
|
||||
|
||||
Reference in New Issue
Block a user