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:
@@ -142,6 +142,36 @@ void main() {
|
||||
expect(echoed.single.text, 'do the thing');
|
||||
});
|
||||
|
||||
test('a synthetic user message (skill/command inject) is flagged injected', () async {
|
||||
proc.emit(jsonEncode({
|
||||
'type': 'user',
|
||||
'isSynthetic': true,
|
||||
'message': {
|
||||
'role': 'user',
|
||||
'content': [
|
||||
{'type': 'text', 'text': 'Base directory for this skill: /x'}
|
||||
],
|
||||
},
|
||||
}));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
final u = items.whereType<UserMessage>().single;
|
||||
expect(u.injected, isTrue);
|
||||
});
|
||||
|
||||
test('a plain user text event is not flagged injected', () async {
|
||||
proc.emit(jsonEncode({
|
||||
'type': 'user',
|
||||
'message': {
|
||||
'role': 'user',
|
||||
'content': [
|
||||
{'type': 'text', 'text': 'hello'}
|
||||
],
|
||||
},
|
||||
}));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(items.whereType<UserMessage>().single.injected, isFalse);
|
||||
});
|
||||
|
||||
test('a can_use_tool control_request becomes a pending prompt (not a conversation item)', () async {
|
||||
final emitted = <ToolPrompt?>[];
|
||||
session.pendingPromptStream.listen(emitted.add);
|
||||
@@ -223,6 +253,20 @@ void main() {
|
||||
expect((follow['message'] as Map)['content'], 'use docs/ instead');
|
||||
});
|
||||
|
||||
test('resolvePrompt records the tool outcome — allow', () async {
|
||||
proc.emit(canUseTool('o1'));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
session.resolvePrompt('o1', AllowTool(const {}));
|
||||
expect(session.toolUseOutcomes['toolu_1'], isTrue);
|
||||
});
|
||||
|
||||
test('resolvePrompt records the tool outcome — deny', () async {
|
||||
proc.emit(canUseTool('o2'));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
session.resolvePrompt('o2', const DenyTool('no'));
|
||||
expect(session.toolUseOutcomes['toolu_1'], isFalse);
|
||||
});
|
||||
|
||||
test('resolvePrompt(deny) writes a deny decision with a message', () async {
|
||||
proc.emit(canUseTool('req-3'));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
Reference in New Issue
Block a user