@assistkick/create 1.22.0 → 1.24.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 +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +5 -5
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +17 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolDetailView.tsx +119 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +10 -6
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +11 -1
package/package.json
CHANGED
package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts
CHANGED
|
@@ -33,8 +33,8 @@ export interface SpawnOptions {
|
|
|
33
33
|
allowedTools?: string[];
|
|
34
34
|
/** Compacted conversation context to prepend to the system prompt */
|
|
35
35
|
continuationContext?: string;
|
|
36
|
-
/**
|
|
37
|
-
|
|
36
|
+
/** Path to a temporary MCP config JSON file to pass via --mcp-config */
|
|
37
|
+
mcpConfigPath?: string;
|
|
38
38
|
onEvent: (event: Record<string, unknown>) => void;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -92,9 +92,9 @@ export class ChatCliBridge {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// MCP server configuration (
|
|
96
|
-
if (options.
|
|
97
|
-
args.push('--mcp-config', options.
|
|
95
|
+
// MCP server configuration (temp file path)
|
|
96
|
+
if (options.mcpConfigPath) {
|
|
97
|
+
args.push('--mcp-config', options.mcpConfigPath);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Prompt via stdin
|
package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts
CHANGED
|
@@ -30,6 +30,9 @@ import type { ChatMessageRepository } from './chat_message_repository.js';
|
|
|
30
30
|
import type { ChatSessionService } from './chat_session_service.js';
|
|
31
31
|
import type { TitleGeneratorService } from './title_generator_service.js';
|
|
32
32
|
import type { McpConfigService } from './mcp_config_service.js';
|
|
33
|
+
import { writeFileSync, unlinkSync } from 'node:fs';
|
|
34
|
+
import { tmpdir } from 'node:os';
|
|
35
|
+
import { join } from 'node:path';
|
|
33
36
|
|
|
34
37
|
export interface ChatWsHandlerDeps {
|
|
35
38
|
wss: WebSocketServer;
|
|
@@ -425,15 +428,18 @@ export class ChatWsHandler {
|
|
|
425
428
|
}
|
|
426
429
|
}
|
|
427
430
|
|
|
428
|
-
// Build
|
|
429
|
-
let
|
|
431
|
+
// Build MCP config temp file for this spawn (if user has MCP servers configured)
|
|
432
|
+
let mcpConfigPath: string | undefined;
|
|
433
|
+
let mcpConfigContent: string | undefined;
|
|
430
434
|
const userId = this.wsUserIds.get(ws);
|
|
431
435
|
if (userId && this.mcpConfigService) {
|
|
432
436
|
try {
|
|
433
437
|
const mergedServers = await this.mcpConfigService.buildMergedConfig(userId, projectId);
|
|
434
438
|
if (mergedServers) {
|
|
435
|
-
|
|
436
|
-
|
|
439
|
+
mcpConfigContent = JSON.stringify({ mcpServers: mergedServers }, null, 2);
|
|
440
|
+
mcpConfigPath = join(tmpdir(), `mcp-${claudeSessionId}.json`);
|
|
441
|
+
writeFileSync(mcpConfigPath, mcpConfigContent, 'utf-8');
|
|
442
|
+
this.log('CHAT_WS', `MCP config for user ${userId}: ${mcpConfigContent}`);
|
|
437
443
|
}
|
|
438
444
|
} catch (err: any) {
|
|
439
445
|
this.log('CHAT_WS', `Failed to build MCP config: ${err.message}`);
|
|
@@ -449,7 +455,7 @@ export class ChatWsHandler {
|
|
|
449
455
|
permissionMode,
|
|
450
456
|
allowedTools: msg.allowedTools,
|
|
451
457
|
continuationContext,
|
|
452
|
-
|
|
458
|
+
mcpConfigPath,
|
|
453
459
|
onEvent: (event) => {
|
|
454
460
|
// Forward to current WS (may have been re-attached after disconnect)
|
|
455
461
|
const currentWs = this.sessionToWs.get(claudeSessionId);
|
|
@@ -542,6 +548,11 @@ export class ChatWsHandler {
|
|
|
542
548
|
|
|
543
549
|
completion
|
|
544
550
|
.then((result) => {
|
|
551
|
+
// Clean up MCP config temp file
|
|
552
|
+
if (mcpConfigPath) {
|
|
553
|
+
try { unlinkSync(mcpConfigPath); } catch { /* already deleted */ }
|
|
554
|
+
}
|
|
555
|
+
|
|
545
556
|
// Look up the current WS — may differ from the original if client reconnected
|
|
546
557
|
const currentWs = this.sessionToWs.get(claudeSessionId);
|
|
547
558
|
if (currentWs) {
|
|
@@ -581,6 +592,7 @@ export class ChatWsHandler {
|
|
|
581
592
|
const parts: string[] = [];
|
|
582
593
|
if (result.stderr) parts.push(result.stderr);
|
|
583
594
|
if (result.stdoutNonJsonLines) parts.push(result.stdoutNonJsonLines);
|
|
595
|
+
if (mcpConfigContent) parts.push(`MCP config passed: ${mcpConfigContent}`);
|
|
584
596
|
errorDetail = parts.length > 0
|
|
585
597
|
? parts.join('\n')
|
|
586
598
|
: `CLI exited with code ${result.exitCode}`;
|
package/templates/assistkick-product-system/packages/frontend/src/components/ToolDetailView.tsx
CHANGED
|
@@ -261,6 +261,124 @@ const AgentDetailView = ({ input, result }: Omit<ToolDetailViewProps, 'toolName'
|
|
|
261
261
|
);
|
|
262
262
|
};
|
|
263
263
|
|
|
264
|
+
/* ── Generic (MCP & other tools) ──────────────────── */
|
|
265
|
+
|
|
266
|
+
/** Format an MCP-style tool name for display: mcp__jetbrains__list_directory_tree → jetbrains / list_directory_tree */
|
|
267
|
+
const formatToolName = (name: string): { provider: string | null; action: string } => {
|
|
268
|
+
if (name.startsWith('mcp__')) {
|
|
269
|
+
const parts = name.slice(5).split('__');
|
|
270
|
+
if (parts.length >= 2) {
|
|
271
|
+
return { provider: parts[0], action: parts.slice(1).join(' / ') };
|
|
272
|
+
}
|
|
273
|
+
return { provider: null, action: parts[0] };
|
|
274
|
+
}
|
|
275
|
+
return { provider: null, action: name };
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/** Try to pretty-print a value — if JSON, format it; otherwise return as string. */
|
|
279
|
+
const tryFormatValue = (value: unknown): { text: string; isJson: boolean } => {
|
|
280
|
+
if (value == null) return { text: '', isJson: false };
|
|
281
|
+
if (typeof value === 'string') {
|
|
282
|
+
// Try parsing as JSON
|
|
283
|
+
try {
|
|
284
|
+
const parsed = JSON.parse(value);
|
|
285
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
286
|
+
return { text: JSON.stringify(parsed, null, 2), isJson: true };
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
// Not JSON — return as plain text
|
|
290
|
+
}
|
|
291
|
+
return { text: value, isJson: false };
|
|
292
|
+
}
|
|
293
|
+
if (typeof value === 'object') {
|
|
294
|
+
return { text: JSON.stringify(value, null, 2), isJson: true };
|
|
295
|
+
}
|
|
296
|
+
return { text: String(value), isJson: false };
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const GenericDetailView = ({ toolName, input, result }: ToolDetailViewProps) => {
|
|
300
|
+
const { provider, action } = formatToolName(toolName);
|
|
301
|
+
const content = result ? contentToString(result.content) : '';
|
|
302
|
+
const inputEntries = Object.entries(input);
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<div className="flex flex-col gap-2">
|
|
306
|
+
{/* Tool name badge */}
|
|
307
|
+
<div className="flex items-center gap-2 px-1">
|
|
308
|
+
{provider && (
|
|
309
|
+
<span className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-accent/15 text-accent">
|
|
310
|
+
{provider}
|
|
311
|
+
</span>
|
|
312
|
+
)}
|
|
313
|
+
<span className="text-[11px] font-mono text-content-secondary">{action}</span>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
{/* Input parameters */}
|
|
317
|
+
{inputEntries.length > 0 && (
|
|
318
|
+
<Panel label="Input">
|
|
319
|
+
<div className="px-3 py-2 flex flex-col gap-1.5">
|
|
320
|
+
{inputEntries.map(([key, value]) => {
|
|
321
|
+
const { text, isJson } = tryFormatValue(value);
|
|
322
|
+
const isLong = text.length > 120;
|
|
323
|
+
return (
|
|
324
|
+
<div key={key} className="flex flex-col gap-0.5">
|
|
325
|
+
<span className="text-[10px] font-mono text-content-muted uppercase tracking-wider">{key}</span>
|
|
326
|
+
{isJson ? (
|
|
327
|
+
<pre className="text-[12px] font-mono text-content-secondary whitespace-pre-wrap break-words m-0 max-h-[200px] overflow-y-auto bg-surface-raised/50 rounded px-2 py-1">
|
|
328
|
+
{text}
|
|
329
|
+
</pre>
|
|
330
|
+
) : isLong ? (
|
|
331
|
+
<pre className="text-[12px] font-mono text-content-secondary whitespace-pre-wrap break-words m-0 max-h-[200px] overflow-y-auto">
|
|
332
|
+
{text}
|
|
333
|
+
</pre>
|
|
334
|
+
) : (
|
|
335
|
+
<span className="text-[12px] font-mono text-content-secondary">{text || '(empty)'}</span>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
})}
|
|
340
|
+
</div>
|
|
341
|
+
</Panel>
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
{/* Output */}
|
|
345
|
+
{result && (
|
|
346
|
+
<Panel label={result.isError ? 'Error' : 'Output'} labelColor={result.isError ? 'text-red-400' : undefined}>
|
|
347
|
+
<div className="max-h-[400px] overflow-y-auto">
|
|
348
|
+
{(() => {
|
|
349
|
+
const { text: formatted, isJson } = tryFormatValue(content);
|
|
350
|
+
if (result.isError) {
|
|
351
|
+
return (
|
|
352
|
+
<pre className="text-[12px] font-mono text-error whitespace-pre-wrap break-words px-3 py-2 m-0">
|
|
353
|
+
{content || '(empty)'}
|
|
354
|
+
</pre>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (isJson) {
|
|
358
|
+
return (
|
|
359
|
+
<SyntaxHighlighter
|
|
360
|
+
language="json"
|
|
361
|
+
style={oneDark}
|
|
362
|
+
customStyle={codeStyle}
|
|
363
|
+
wrapLongLines
|
|
364
|
+
>
|
|
365
|
+
{formatted}
|
|
366
|
+
</SyntaxHighlighter>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return (
|
|
370
|
+
<pre className="text-[12px] font-mono text-content-secondary whitespace-pre-wrap break-words px-3 py-2 m-0">
|
|
371
|
+
{content || '(empty)'}
|
|
372
|
+
</pre>
|
|
373
|
+
);
|
|
374
|
+
})()}
|
|
375
|
+
</div>
|
|
376
|
+
</Panel>
|
|
377
|
+
)}
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
};
|
|
381
|
+
|
|
264
382
|
/* ── Router ────────────────────────────────────────── */
|
|
265
383
|
|
|
266
384
|
export const ToolDetailView = ({ toolName, input, result }: ToolDetailViewProps) => {
|
|
@@ -268,6 +386,6 @@ export const ToolDetailView = ({ toolName, input, result }: ToolDetailViewProps)
|
|
|
268
386
|
case 'Read': return <ReadDetailView input={input} result={result} />;
|
|
269
387
|
case 'Bash': return <BashDetailView input={input} result={result} />;
|
|
270
388
|
case 'Agent': return <AgentDetailView input={input} result={result} />;
|
|
271
|
-
default: return
|
|
389
|
+
default: return <GenericDetailView toolName={toolName} input={input} result={result} />;
|
|
272
390
|
}
|
|
273
391
|
};
|
package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx
CHANGED
|
@@ -53,12 +53,16 @@ const contentToString = (content: ToolResultBlock['content']): string => {
|
|
|
53
53
|
|
|
54
54
|
type ViewTab = 'humanized' | 'raw';
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
/** All tools now support humanized + raw tabs. */
|
|
57
|
+
const hasHumanizedView = (_name: string): boolean => true;
|
|
58
58
|
|
|
59
59
|
/** Tools where the humanized view renders both input and result together. */
|
|
60
60
|
const hasIntegratedView = (name: string): boolean =>
|
|
61
|
-
name === 'Read' || name === 'Bash' || name === 'Agent';
|
|
61
|
+
name === 'Read' || name === 'Bash' || name === 'Agent' || hasGenericView(name);
|
|
62
|
+
|
|
63
|
+
/** Tools that use the generic humanized view (MCP tools and other non-built-in tools). */
|
|
64
|
+
const hasGenericView = (name: string): boolean =>
|
|
65
|
+
!['Edit', 'Write', 'Read', 'Bash', 'Agent'].includes(name);
|
|
62
66
|
|
|
63
67
|
export const ToolUseCard = ({ name, input, isStreaming, result }: ToolUseCardProps) => {
|
|
64
68
|
const [expanded, setExpanded] = useState(false);
|
|
@@ -120,10 +124,10 @@ export const ToolUseCard = ({ name, input, isStreaming, result }: ToolUseCardPro
|
|
|
120
124
|
{/* Tool input — humanized or raw */}
|
|
121
125
|
{hasHumanizedView(name) && viewTab === 'humanized' ? (
|
|
122
126
|
<div className="px-3 py-2">
|
|
123
|
-
{
|
|
124
|
-
<ToolDetailView toolName={name} input={input} result={result} />
|
|
125
|
-
) : (
|
|
127
|
+
{name === 'Edit' || name === 'Write' ? (
|
|
126
128
|
<ToolDiffView toolName={name as 'Edit' | 'Write'} input={input} />
|
|
129
|
+
) : (
|
|
130
|
+
<ToolDetailView toolName={name} input={input} result={result} />
|
|
127
131
|
)}
|
|
128
132
|
</div>
|
|
129
133
|
) : (
|
|
@@ -76,8 +76,18 @@ export const summarizeToolUse = (
|
|
|
76
76
|
return url ? truncate(`Fetch: ${url}`, MAX_SUMMARY_LEN) : 'Web fetch';
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
default:
|
|
79
|
+
default: {
|
|
80
|
+
// MCP tools: mcp__provider__action → "provider: action"
|
|
81
|
+
if (name.startsWith('mcp__')) {
|
|
82
|
+
const parts = name.slice(5).split('__');
|
|
83
|
+
if (parts.length >= 2) {
|
|
84
|
+
const provider = parts[0];
|
|
85
|
+
const action = parts.slice(1).join(' ');
|
|
86
|
+
return truncate(`${provider}: ${action}`, MAX_SUMMARY_LEN);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
80
89
|
return name;
|
|
90
|
+
}
|
|
81
91
|
}
|
|
82
92
|
};
|
|
83
93
|
|