@assistkick/create 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistkick/create",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "Scaffold assistkick-product-system into any project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,273 @@
1
+ /**
2
+ * ToolDetailView — humanized views for Agent, Read, and Bash tool calls.
3
+ * Renders both input and result in a formatted layout, replacing the raw
4
+ * JSON + plain text display with structured, readable panels.
5
+ */
6
+
7
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
8
+ import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
9
+ import ReactMarkdown from 'react-markdown';
10
+ import type { Components } from 'react-markdown';
11
+ import remarkGfm from 'remark-gfm';
12
+ import type { ToolResultBlock } from '../hooks/use_chat_stream';
13
+
14
+ /** Map common file extensions to Prism language identifiers. */
15
+ const extToLang: Record<string, string> = {
16
+ ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx',
17
+ py: 'python', rb: 'ruby', rs: 'rust', go: 'go',
18
+ java: 'java', kt: 'kotlin', cs: 'csharp', cpp: 'cpp', c: 'c',
19
+ css: 'css', scss: 'scss', html: 'html', vue: 'html',
20
+ json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
21
+ md: 'markdown', sql: 'sql', sh: 'bash', bash: 'bash', zsh: 'bash',
22
+ xml: 'xml', svg: 'xml', graphql: 'graphql', prisma: 'prisma',
23
+ dockerfile: 'docker',
24
+ };
25
+
26
+ const inferLang = (filePath: string): string => {
27
+ const name = filePath.split('/').pop() ?? '';
28
+ if (name.toLowerCase() === 'dockerfile') return 'docker';
29
+ const ext = name.split('.').pop()?.toLowerCase() ?? '';
30
+ return extToLang[ext] ?? 'text';
31
+ };
32
+
33
+ const codeStyle: React.CSSProperties = {
34
+ margin: 0,
35
+ padding: '0.75rem',
36
+ fontSize: '12px',
37
+ lineHeight: '1.5',
38
+ background: 'transparent',
39
+ borderRadius: 0,
40
+ };
41
+
42
+ /** Normalize result content to a string. */
43
+ const contentToString = (content: ToolResultBlock['content']): string => {
44
+ if (!content) return '';
45
+ if (typeof content === 'string') return content;
46
+ if (Array.isArray(content)) {
47
+ return (content as Array<Record<string, unknown>>)
48
+ .filter(c => typeof c.text === 'string')
49
+ .map(c => c.text as string)
50
+ .join('\n');
51
+ }
52
+ return String(content);
53
+ };
54
+
55
+ /** Strip line number prefixes from Read tool output (e.g. " 1→" format). */
56
+ const stripLineNumbers = (text: string): { code: string; startLine: number } => {
57
+ const lines = text.split('\n');
58
+ let startLine = 1;
59
+ let foundLineNum = false;
60
+
61
+ const stripped = lines.map((line, i) => {
62
+ const match = line.match(/^\s*(\d+)[→\t]/);
63
+ if (match) {
64
+ if (i === 0) {
65
+ startLine = parseInt(match[1], 10);
66
+ foundLineNum = true;
67
+ }
68
+ return line.replace(/^\s*\d+[→\t]/, '');
69
+ }
70
+ return line;
71
+ });
72
+
73
+ return {
74
+ code: foundLineNum ? stripped.join('\n') : text,
75
+ startLine: foundLineNum ? startLine : 1,
76
+ };
77
+ };
78
+
79
+ /** Markdown components for Agent output — syntax-highlighted fenced code blocks. */
80
+ const mdComponents: Components = {
81
+ code({ className, children, ...props }) {
82
+ const match = /language-(\w+)/.exec(className || '');
83
+ const codeString = String(children).replace(/\n$/, '');
84
+
85
+ if (match) {
86
+ return (
87
+ <SyntaxHighlighter
88
+ style={oneDark}
89
+ language={match[1]}
90
+ PreTag="div"
91
+ customStyle={{ margin: 0, borderRadius: 6, fontSize: 12 }}
92
+ >
93
+ {codeString}
94
+ </SyntaxHighlighter>
95
+ );
96
+ }
97
+
98
+ return (
99
+ <code className={className} {...props}>
100
+ {children}
101
+ </code>
102
+ );
103
+ },
104
+ };
105
+
106
+ /** Tailwind classes for markdown rendering inside tool detail panels. */
107
+ const mdClass = [
108
+ 'text-[12px] font-mono text-content-secondary break-words min-w-0',
109
+ '[&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
110
+ '[&_h1]:text-[14px] [&_h1]:font-bold [&_h1]:mt-3 [&_h1]:mb-1 [&_h1]:text-content-primary',
111
+ '[&_h2]:text-[13px] [&_h2]:font-bold [&_h2]:mt-2.5 [&_h2]:mb-1 [&_h2]:text-content-primary',
112
+ '[&_h3]:text-[12px] [&_h3]:font-semibold [&_h3]:mt-2 [&_h3]:mb-0.5 [&_h3]:text-content-primary',
113
+ '[&_p]:my-1 [&_p]:leading-[1.5]',
114
+ '[&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-4 [&_ol]:pl-4 [&_ul]:my-1 [&_ol]:my-1',
115
+ '[&_li]:my-[2px] [&_li]:leading-normal',
116
+ '[&_blockquote]:border-l-2 [&_blockquote]:border-edge [&_blockquote]:pl-2 [&_blockquote]:my-1.5 [&_blockquote]:text-content-muted',
117
+ '[&_pre]:bg-surface-raised [&_pre]:py-2 [&_pre]:px-2.5 [&_pre]:rounded-md [&_pre]:overflow-x-auto [&_pre]:my-1.5 [&_pre]:text-xs',
118
+ '[&_pre_code]:bg-transparent [&_pre_code]:p-0',
119
+ '[&_code]:bg-surface-raised [&_code]:px-1 [&_code]:py-px [&_code]:rounded-[3px] [&_code]:text-xs [&_code]:text-accent',
120
+ '[&_strong]:font-bold [&_strong]:text-content-primary',
121
+ '[&_hr]:my-2 [&_hr]:border-edge',
122
+ '[&_table]:my-1.5 [&_table]:border-collapse [&_table]:block [&_table]:overflow-x-auto',
123
+ '[&_th]:px-2 [&_th]:py-0.5 [&_th]:border [&_th]:border-edge [&_th]:text-xs [&_th]:font-semibold [&_th]:bg-surface-raised',
124
+ '[&_td]:px-2 [&_td]:py-0.5 [&_td]:border [&_td]:border-edge [&_td]:text-xs',
125
+ ].join(' ');
126
+
127
+ interface ToolDetailViewProps {
128
+ toolName: string;
129
+ input: Record<string, unknown>;
130
+ result?: ToolResultBlock;
131
+ }
132
+
133
+ /** Labeled panel wrapper. */
134
+ const Panel = ({ label, labelColor, children }: { label: string; labelColor?: string; children: React.ReactNode }) => (
135
+ <div className="border rounded-md border-edge bg-surface-alt overflow-hidden">
136
+ <div className={`text-[10px] font-mono uppercase tracking-wider px-3 py-1.5 border-b border-edge/50 ${labelColor ?? 'text-content-muted'}`}>
137
+ {label}
138
+ </div>
139
+ {children}
140
+ </div>
141
+ );
142
+
143
+ /* ── Read ──────────────────────────────────────────── */
144
+
145
+ const ReadDetailView = ({ input, result }: Omit<ToolDetailViewProps, 'toolName'>) => {
146
+ const filePath = (input.file_path as string) ?? '';
147
+ const language = inferLang(filePath);
148
+ const offset = input.offset as number | undefined;
149
+ const limit = input.limit as number | undefined;
150
+ const rawContent = result ? contentToString(result.content) : '';
151
+ const { code, startLine } = stripLineNumbers(rawContent);
152
+
153
+ return (
154
+ <div className="flex flex-col gap-2">
155
+ <div className="text-[10px] font-mono text-content-muted truncate px-1">
156
+ {filePath}
157
+ {offset != null && <span className="ml-2 opacity-70">from line {offset}</span>}
158
+ {limit != null && <span className="ml-1 opacity-70">({limit} lines)</span>}
159
+ </div>
160
+ {result && (
161
+ <Panel label={result.isError ? 'Error' : 'Content'} labelColor={result.isError ? 'text-red-400' : undefined}>
162
+ <div className="overflow-auto max-h-[400px]">
163
+ {result.isError ? (
164
+ <pre className="text-[12px] font-mono text-error whitespace-pre-wrap break-words px-3 py-2 m-0">
165
+ {rawContent || '(empty)'}
166
+ </pre>
167
+ ) : (
168
+ <SyntaxHighlighter
169
+ language={language}
170
+ style={oneDark}
171
+ customStyle={codeStyle}
172
+ showLineNumbers
173
+ startingLineNumber={startLine}
174
+ wrapLongLines
175
+ >
176
+ {code || '(empty)'}
177
+ </SyntaxHighlighter>
178
+ )}
179
+ </div>
180
+ </Panel>
181
+ )}
182
+ </div>
183
+ );
184
+ };
185
+
186
+ /* ── Bash ──────────────────────────────────────────── */
187
+
188
+ const BashDetailView = ({ input, result }: Omit<ToolDetailViewProps, 'toolName'>) => {
189
+ const command = (input.command as string) ?? '';
190
+ const description = (input.description as string) ?? '';
191
+ const content = result ? contentToString(result.content) : '';
192
+
193
+ return (
194
+ <div className="flex flex-col gap-2">
195
+ {description && (
196
+ <div className="text-[10px] font-mono text-content-muted px-1">{description}</div>
197
+ )}
198
+ <Panel label="Command">
199
+ <div className="overflow-auto max-h-[200px]">
200
+ <SyntaxHighlighter language="bash" style={oneDark} customStyle={codeStyle} wrapLongLines>
201
+ {command}
202
+ </SyntaxHighlighter>
203
+ </div>
204
+ </Panel>
205
+ {result && (
206
+ <Panel label={result.isError ? 'Error' : 'Output'} labelColor={result.isError ? 'text-red-400' : undefined}>
207
+ <pre
208
+ className={`text-[12px] font-mono whitespace-pre-wrap break-words px-3 py-2 m-0 max-h-[400px] overflow-y-auto ${
209
+ result.isError ? 'text-error' : 'text-content-secondary'
210
+ }`}
211
+ >
212
+ {content || '(empty)'}
213
+ </pre>
214
+ </Panel>
215
+ )}
216
+ </div>
217
+ );
218
+ };
219
+
220
+ /* ── Agent ─────────────────────────────────────────── */
221
+
222
+ const AgentDetailView = ({ input, result }: Omit<ToolDetailViewProps, 'toolName'>) => {
223
+ const description = (input.description as string) ?? '';
224
+ const subagentType = (input.subagent_type as string) ?? 'general-purpose';
225
+ const prompt = (input.prompt as string) ?? '';
226
+ const content = result ? contentToString(result.content) : '';
227
+
228
+ return (
229
+ <div className="flex flex-col gap-2">
230
+ <div className="flex items-center gap-2 px-1">
231
+ <span className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-accent/15 text-accent">
232
+ {subagentType}
233
+ </span>
234
+ {description && (
235
+ <span className="text-[11px] font-mono text-content-secondary">{description}</span>
236
+ )}
237
+ </div>
238
+ <Panel label="Prompt">
239
+ <div className="px-3 py-2 text-[12px] font-mono text-content-secondary whitespace-pre-wrap break-words max-h-[300px] overflow-y-auto">
240
+ {prompt || '(empty)'}
241
+ </div>
242
+ </Panel>
243
+ {result && (
244
+ <Panel label={result.isError ? 'Error' : 'Response'} labelColor={result.isError ? 'text-red-400' : undefined}>
245
+ <div className="px-3 py-2 max-h-[400px] overflow-y-auto">
246
+ {result.isError ? (
247
+ <pre className="text-[12px] font-mono text-error whitespace-pre-wrap break-words m-0">
248
+ {content || '(empty)'}
249
+ </pre>
250
+ ) : (
251
+ <div className={mdClass}>
252
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={mdComponents}>
253
+ {content || '(empty)'}
254
+ </ReactMarkdown>
255
+ </div>
256
+ )}
257
+ </div>
258
+ </Panel>
259
+ )}
260
+ </div>
261
+ );
262
+ };
263
+
264
+ /* ── Router ────────────────────────────────────────── */
265
+
266
+ export const ToolDetailView = ({ toolName, input, result }: ToolDetailViewProps) => {
267
+ switch (toolName) {
268
+ case 'Read': return <ReadDetailView input={input} result={result} />;
269
+ case 'Bash': return <BashDetailView input={input} result={result} />;
270
+ case 'Agent': return <AgentDetailView input={input} result={result} />;
271
+ default: return null;
272
+ }
273
+ };
@@ -11,6 +11,7 @@ import { useState } from 'react';
11
11
  import { ChevronDown, ChevronRight } from 'lucide-react';
12
12
  import { summarizeToolUse, toolIcon } from '../lib/tool_use_summary';
13
13
  import { ToolDiffView } from './ToolDiffView';
14
+ import { ToolDetailView } from './ToolDetailView';
14
15
  import type { ToolResultBlock } from '../hooks/use_chat_stream';
15
16
 
16
17
  interface ToolUseCardProps {
@@ -52,7 +53,12 @@ const contentToString = (content: ToolResultBlock['content']): string => {
52
53
 
53
54
  type ViewTab = 'humanized' | 'raw';
54
55
 
55
- const hasHumanizedView = (name: string): boolean => name === 'Edit' || name === 'Write';
56
+ const hasHumanizedView = (name: string): boolean =>
57
+ name === 'Edit' || name === 'Write' || name === 'Read' || name === 'Bash' || name === 'Agent';
58
+
59
+ /** Tools where the humanized view renders both input and result together. */
60
+ const hasIntegratedView = (name: string): boolean =>
61
+ name === 'Read' || name === 'Bash' || name === 'Agent';
56
62
 
57
63
  export const ToolUseCard = ({ name, input, isStreaming, result }: ToolUseCardProps) => {
58
64
  const [expanded, setExpanded] = useState(false);
@@ -114,7 +120,11 @@ export const ToolUseCard = ({ name, input, isStreaming, result }: ToolUseCardPro
114
120
  {/* Tool input — humanized or raw */}
115
121
  {hasHumanizedView(name) && viewTab === 'humanized' ? (
116
122
  <div className="px-3 py-2">
117
- <ToolDiffView toolName={name as 'Edit' | 'Write'} input={input} />
123
+ {hasIntegratedView(name) ? (
124
+ <ToolDetailView toolName={name} input={input} result={result} />
125
+ ) : (
126
+ <ToolDiffView toolName={name as 'Edit' | 'Write'} input={input} />
127
+ )}
118
128
  </div>
119
129
  ) : (
120
130
  <div className="px-3 py-2">
@@ -127,8 +137,8 @@ export const ToolUseCard = ({ name, input, isStreaming, result }: ToolUseCardPro
127
137
  </div>
128
138
  )}
129
139
 
130
- {/* Tool output — shown inline when result is available */}
131
- {result && (
140
+ {/* Tool output — shown inline when result is available (skip for integrated views in humanized mode) */}
141
+ {result && !(hasIntegratedView(name) && viewTab === 'humanized') && (
132
142
  <div className="border-t border-edge px-3 py-2">
133
143
  <div className="text-[10px] font-mono text-content-muted uppercase tracking-wider mb-1">
134
144
  {result.isError ? 'Error' : 'Output'}