Skip to content

Commit aac8d58

Browse files
grypezclaude
andcommitted
refactor(kernel-tui): split session-detail-view into focused files
The session-detail-view component file was 1235 lines, mixing the top-level component, three subcomponents, pure formatting helpers, layout math, and provision derivation. Split into a directory: session-detail-view/ index.tsx (barrel) session-detail-view.tsx (top-level component) format.ts (pure formatting helpers) layout.ts (entryRowCount, windowEndIdx, clampScroll) provisions.ts (buildProvisions, provisionKey, etc.) provision-editor.tsx provisions-panel.tsx revoke-confirm.tsx Layout-math and buildProvisions switched to options-bag signatures along the way (CLAUDE.md compliance past two args). Adds 62 vitest cases for the pure helpers (parseDescription, splitShellCommand, formatExpandedContent, layout math, provision derivation). Adds @ocap/repo-tools/test-utils/mock-endoify to the package's vitest setupFiles so tests can import from @metamask/kernel-utils/session, matching kernel-utils's own setup. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d32300a commit aac8d58

14 files changed

Lines changed: 2113 additions & 1236 deletions

packages/kernel-tui/src/components/session-detail-view.tsx

Lines changed: 0 additions & 1235 deletions
This file was deleted.
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import type { Provision } from '@metamask/kernel-utils/session';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import {
5+
escapeControlCharsInStrings,
6+
extractStringFields,
7+
formatExpandedContent,
8+
formatProvisionCompact,
9+
formatTime,
10+
parseDescription,
11+
splitShellCommand,
12+
tryParseJsonObject,
13+
} from './format.ts';
14+
15+
describe('formatTime', () => {
16+
it.each([
17+
['2026-01-02T03:04:05.000Z', /^\d{2}:\d{2}:\d{2}$/u],
18+
['2026-12-31T23:59:59.000Z', /^\d{2}:\d{2}:\d{2}$/u],
19+
])('renders ISO %s as HH:mm:ss', (iso, pattern) => {
20+
expect(formatTime(iso)).toMatch(pattern);
21+
});
22+
23+
it('pads single-digit fields with zero', () => {
24+
// Construct a Date in the local zone so we can predict the output.
25+
const date = new Date(2026, 0, 1, 3, 4, 5);
26+
expect(formatTime(date.toISOString())).toBe('03:04:05');
27+
});
28+
});
29+
30+
describe('splitShellCommand', () => {
31+
it.each<[string, string[]]>([
32+
['ls -la', ['ls -la']],
33+
['ls && cd /tmp', ['ls', '&& cd /tmp']],
34+
['git log | head', ['git log', '| head']],
35+
['echo a ; echo b', ['echo a', '; echo b']],
36+
[
37+
'git log --oneline | head -10 && echo done',
38+
['git log --oneline', '| head -10', '&& echo done'],
39+
],
40+
['true || false', ['true || false']],
41+
])('splits %s into segments', (command, expected) => {
42+
expect(splitShellCommand(command)).toStrictEqual(expected);
43+
});
44+
45+
it('returns empty array for empty input', () => {
46+
expect(splitShellCommand('')).toStrictEqual([]);
47+
});
48+
});
49+
50+
describe('tryParseJsonObject', () => {
51+
it('parses a valid object literal', () => {
52+
expect(tryParseJsonObject('{"a":1,"b":"x"}')).toStrictEqual({
53+
a: 1,
54+
b: 'x',
55+
});
56+
});
57+
58+
it.each<[string]>([['[1,2,3]'], ['"hello"'], ['42'], ['null'], ['not json']])(
59+
'returns null for non-object input %s',
60+
(input) => {
61+
expect(tryParseJsonObject(input)).toBeNull();
62+
},
63+
);
64+
});
65+
66+
describe('escapeControlCharsInStrings', () => {
67+
it('escapes newlines inside string values', () => {
68+
expect(escapeControlCharsInStrings('{"a":"line1\nline2"}')).toBe(
69+
'{"a":"line1\\nline2"}',
70+
);
71+
});
72+
73+
it('escapes tabs and CR inside string values', () => {
74+
expect(escapeControlCharsInStrings('{"a":"x\ty\rz"}')).toBe(
75+
'{"a":"x\\ty\\rz"}',
76+
);
77+
});
78+
79+
it('preserves structural whitespace outside strings', () => {
80+
const input = '{\n "a": "ok"\n}';
81+
expect(escapeControlCharsInStrings(input)).toBe(input);
82+
});
83+
84+
it('leaves existing backslash escapes alone', () => {
85+
expect(escapeControlCharsInStrings('{"a":"\\nliteral"}')).toBe(
86+
'{"a":"\\nliteral"}',
87+
);
88+
});
89+
});
90+
91+
describe('parseDescription', () => {
92+
it('parses a Bash invocation with a JSON body', () => {
93+
const result = parseDescription(
94+
'Allow Bash({"command":"ls -la","description":"list"})',
95+
);
96+
expect(result).toStrictEqual({
97+
label: 'Allow Bash',
98+
params: { command: 'ls -la', description: 'list' },
99+
});
100+
});
101+
102+
it('parses a Read invocation', () => {
103+
const result = parseDescription('Allow Read({"file_path":"/tmp/foo.txt"})');
104+
expect(result).toStrictEqual({
105+
label: 'Allow Read',
106+
params: { file_path: '/tmp/foo.txt' },
107+
});
108+
});
109+
110+
it('parses an Edit invocation', () => {
111+
const result = parseDescription(
112+
'Allow Edit({"file_path":"/x","old_string":"a","new_string":"b"})',
113+
);
114+
expect(result).toStrictEqual({
115+
label: 'Allow Edit',
116+
params: { file_path: '/x', old_string: 'a', new_string: 'b' },
117+
});
118+
});
119+
120+
it('returns null params when no parens are present', () => {
121+
expect(parseDescription('Just a label')).toStrictEqual({
122+
label: 'Just a label',
123+
params: null,
124+
});
125+
});
126+
127+
it('preserves the label even when JSON parse fails', () => {
128+
expect(parseDescription('Allow Bash({not json})')).toStrictEqual({
129+
label: 'Allow Bash',
130+
params: null,
131+
});
132+
});
133+
134+
it('retries via control-char escaping when JSON contains literal newlines', () => {
135+
const result = parseDescription('Allow Bash({"command":"a\nb"})');
136+
expect(result).toStrictEqual({
137+
label: 'Allow Bash',
138+
params: { command: 'a\nb' },
139+
});
140+
});
141+
142+
it('returns null params when the closing paren is missing', () => {
143+
expect(parseDescription('Allow Bash({"command":"x"}')).toStrictEqual({
144+
label: 'Allow Bash',
145+
params: null,
146+
});
147+
});
148+
});
149+
150+
describe('extractStringFields', () => {
151+
it('extracts top-level string fields', () => {
152+
expect(
153+
extractStringFields('{"command":"ls -la","description":"list"}'),
154+
).toStrictEqual([
155+
['command', 'ls -la'],
156+
['description', 'list'],
157+
]);
158+
});
159+
160+
it('decodes common escape sequences in values', () => {
161+
expect(extractStringFields('{"x":"a\\nb\\tc\\rd"}')).toStrictEqual([
162+
['x', 'a\nb\tc\rd'],
163+
]);
164+
});
165+
166+
it('skips non-string fields', () => {
167+
expect(extractStringFields('{"n":42,"s":"hi"}')).toStrictEqual([
168+
['s', 'hi'],
169+
]);
170+
});
171+
172+
it('returns null when input is not object-shaped', () => {
173+
expect(extractStringFields('[1,2,3]')).toBeNull();
174+
});
175+
176+
it('returns null when no string fields are found', () => {
177+
expect(extractStringFields('{"n":42}')).toBeNull();
178+
});
179+
});
180+
181+
describe('formatProvisionCompact', () => {
182+
it('formats a single-invocation provision', () => {
183+
const provision: Provision = {
184+
tool: 'Bash',
185+
patterns: [
186+
{
187+
name: 'ls',
188+
argPatterns: [{ kind: 'exact', value: '-la' }],
189+
},
190+
],
191+
};
192+
expect(formatProvisionCompact(provision)).toBe('ls -la');
193+
});
194+
195+
it('joins a pipeline with ` | `', () => {
196+
const provision: Provision = {
197+
tool: 'Bash',
198+
patterns: [
199+
{
200+
name: 'git',
201+
argPatterns: [{ kind: 'exact', value: 'log' }, { kind: 'wildcard' }],
202+
},
203+
{
204+
name: 'head',
205+
argPatterns: [{ kind: 'wildcard' }],
206+
},
207+
],
208+
};
209+
expect(formatProvisionCompact(provision)).toBe('git log * | head *');
210+
});
211+
212+
it('renders prefix patterns with a trailing star', () => {
213+
const provision: Provision = {
214+
tool: 'Read',
215+
patterns: [
216+
{
217+
name: 'Read',
218+
argPatterns: [{ kind: 'prefix', prefix: '/tmp/' }],
219+
},
220+
],
221+
};
222+
expect(formatProvisionCompact(provision)).toBe('Read /tmp/*');
223+
});
224+
});
225+
226+
describe('formatExpandedContent', () => {
227+
it('splits a Bash command on shell operators', () => {
228+
const description = 'Allow Bash({"command":"ls && cd /tmp"})';
229+
expect(formatExpandedContent(description)).toBe('ls\n&& cd /tmp');
230+
});
231+
232+
it('splits a Bash command on newlines when present', () => {
233+
const description = 'Allow Bash({"command":"line1\\nline2"})';
234+
expect(formatExpandedContent(description)).toBe('line1\nline2');
235+
});
236+
237+
it('renders generic tools as key: value lines', () => {
238+
const description = 'Allow Read({"file_path":"/x","limit":50})';
239+
expect(formatExpandedContent(description)).toBe('file_path: /x\nlimit: 50');
240+
});
241+
242+
it('truncates very long string values with an ellipsis', () => {
243+
const longValue = 'a'.repeat(250);
244+
const description = `Allow Read({"file_path":"${longValue}"})`;
245+
const output = formatExpandedContent(description);
246+
expect(output.startsWith('file_path: ')).toBe(true);
247+
expect(output.endsWith('…')).toBe(true);
248+
expect(output.length).toBeLessThan(longValue.length + 'file_path: '.length);
249+
});
250+
251+
it('truncates long Bash segments independently', () => {
252+
const longSegment = 'a'.repeat(250);
253+
const description = `Allow Bash({"command":"${longSegment} && ls"})`;
254+
const output = formatExpandedContent(description);
255+
const [first, second] = output.split('\n');
256+
expect(first?.endsWith('…')).toBe(true);
257+
expect(second).toBe('&& ls');
258+
});
259+
260+
it('falls back to extractStringFields when JSON parsing fails', () => {
261+
// Description with an unescaped quote inside the value — outer JSON fails.
262+
const description = 'Allow Bash({"command":"echo "hi""})';
263+
const output = formatExpandedContent(description);
264+
expect(output).toContain('echo');
265+
});
266+
267+
it('returns the raw description when no parens are present', () => {
268+
expect(formatExpandedContent('hello world')).toBe('hello world');
269+
});
270+
});

0 commit comments

Comments
 (0)