shraga 0.1.142 → 0.1.144
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/dist/client/assets/{index-BLAQpfJ9.css → index-BBkhe7pK.css} +2 -2
- package/dist/client/assets/index-D8WQMu83.js +2112 -0
- package/dist/client/index.html +2 -2
- package/package.json +5 -1
- package/src/client/bare-v3.d.ts +15 -0
- package/src/client/components/AssistantMarkdown.css +18 -0
- package/src/client/components/AssistantMarkdown.tsx +94 -0
- package/src/client/components/AuthedImage.tsx +10 -5
- package/src/client/components/ChatView.tsx +8 -76
- package/src/server/engine/claude-code.ts +5 -1
- package/dist/client/assets/index-DbQklb-I.js +0 -2009
package/dist/client/index.html
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
14
14
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
15
15
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
16
|
-
<script type="module" crossorigin src="/assets/index-
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-D8WQMu83.js"></script>
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BBkhe7pK.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
20
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shraga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.144",
|
|
4
4
|
"description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@anthropic-ai/claude-agent-sdk": "^0.3.280",
|
|
59
|
+
"@livx.cc/bare-v3": "0.1.0-alpha.57",
|
|
59
60
|
"@livx.cc/mcp-firebase": "^0.1.14",
|
|
60
61
|
"@radix-ui/react-accordion": "^1.2.2",
|
|
61
62
|
"@radix-ui/react-dialog": "^1.1.4",
|
|
@@ -67,10 +68,13 @@
|
|
|
67
68
|
"clsx": "^2.1.1",
|
|
68
69
|
"cron-parser": "^5.5.0",
|
|
69
70
|
"cronstrue": "^3.14.0",
|
|
71
|
+
"dompurify": "^3.4.16",
|
|
70
72
|
"edge.libx.js": "^0.6.1",
|
|
71
73
|
"express": "^4.21.1",
|
|
72
74
|
"firebase": "^11.0.0",
|
|
75
|
+
"highlight.js": "~11.11.0",
|
|
73
76
|
"lucide-react": "^0.468.0",
|
|
77
|
+
"marked": "^18.0.14",
|
|
74
78
|
"mcp-slack-use": "github:Livshitz/mcp-slack-use#38ff8b7",
|
|
75
79
|
"react": "^19.0.0",
|
|
76
80
|
"react-dom": "^19.0.0",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// bare-v3 ships plain JS without typings — just the surface AssistantMarkdown uses.
|
|
2
|
+
declare module '@livx.cc/bare-v3/elements/markdown-stream' {
|
|
3
|
+
export class MarkdownStream {
|
|
4
|
+
constructor(options: { Marked: unknown; sanitize: (html: string) => string; code?: (text: string, lang: string) => string });
|
|
5
|
+
buf: string;
|
|
6
|
+
attach(el: HTMLElement): this;
|
|
7
|
+
detach(): void;
|
|
8
|
+
push(chunk: string): void;
|
|
9
|
+
end(): void;
|
|
10
|
+
toHtml(src: string): string;
|
|
11
|
+
}
|
|
12
|
+
export function codeBlock(text: string, lang: string): string;
|
|
13
|
+
export function promptGuide(options?: object): string;
|
|
14
|
+
}
|
|
15
|
+
declare module '@livx.cc/bare-v3/css';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* bare-v3 ships its styles in @layer; Tailwind's preflight is unlayered, so it wins regardless of
|
|
2
|
+
specificity (zeroes borders, strips button chrome). Inside bare-v3 components, roll those
|
|
3
|
+
properties back to bare's layer. */
|
|
4
|
+
.assistant-md { font-family: inherit; }
|
|
5
|
+
.assistant-md :is(.b3-table-wrap, .b3-alert, .b3-stat, .b3-surface, .b3-badge, .b3-button, .b3-skeleton),
|
|
6
|
+
.assistant-md :is(.b3-table-wrap, .b3-alert) * {
|
|
7
|
+
border-width: revert-layer;
|
|
8
|
+
border-style: revert-layer;
|
|
9
|
+
border-color: revert-layer;
|
|
10
|
+
}
|
|
11
|
+
.assistant-md .b3-button {
|
|
12
|
+
padding: revert-layer;
|
|
13
|
+
background: revert-layer;
|
|
14
|
+
line-height: revert-layer;
|
|
15
|
+
}
|
|
16
|
+
.assistant-md .b3-alert :is(ul, ol) { list-style: revert-layer; padding-inline-start: 1.25em; }
|
|
17
|
+
.assistant-md .b3-alert p + * { margin-block-start: 0.5em; }
|
|
18
|
+
.assistant-md > .not-prose { margin-block: 1em; }
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { Marked } from 'marked';
|
|
3
|
+
import DOMPurify from 'dompurify';
|
|
4
|
+
import hljs from 'highlight.js/lib/common';
|
|
5
|
+
import { MarkdownStream, codeBlock } from '@livx.cc/bare-v3/elements/markdown-stream';
|
|
6
|
+
import '@livx.cc/bare-v3/css';
|
|
7
|
+
import 'highlight.js/styles/github.css';
|
|
8
|
+
import './AssistantMarkdown.css';
|
|
9
|
+
import { NEEDS_AUTH, authedBlobUrl } from './AuthedImage';
|
|
10
|
+
import { useWorkspace } from '@/lib/workspaceContext';
|
|
11
|
+
import { useDarkMode } from '@/hooks/useDarkMode';
|
|
12
|
+
import { logger } from '@/lib/debug';
|
|
13
|
+
|
|
14
|
+
const log = logger.forComponent('AssistantMarkdown');
|
|
15
|
+
|
|
16
|
+
// Assistant text renders through bare-v3's MarkdownStream: finished blocks freeze, only the tail
|
|
17
|
+
// re-renders, and half-written syntax never flashes raw. One instance per text block, so text
|
|
18
|
+
// segments split by tool calls each get their own stream.
|
|
19
|
+
|
|
20
|
+
const RTL_BLOCK_TAGS = new Set(['P', 'LI', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'TD', 'TH']);
|
|
21
|
+
// bare-v3 component roots — `not-prose` keeps Tailwind Typography off them (it would override their styles).
|
|
22
|
+
const B3_BLOCKS = ['b3-table-wrap', 'b3-alert', 'b3-md-code', 'b3-md-stats', 'b3-md-bars', 'b3-md-tasks'];
|
|
23
|
+
const IMG_CLASS = 'max-h-[80vh] max-w-full rounded-xl border object-contain cursor-pointer hover:opacity-80 transition-opacity';
|
|
24
|
+
|
|
25
|
+
const purify = DOMPurify(window);
|
|
26
|
+
purify.addHook('afterSanitizeAttributes', (node) => {
|
|
27
|
+
const el = node as Element;
|
|
28
|
+
if (RTL_BLOCK_TAGS.has(el.tagName)) el.setAttribute('dir', 'auto');
|
|
29
|
+
if (B3_BLOCKS.some((c) => el.classList?.contains(c))) el.classList.add('not-prose');
|
|
30
|
+
if (el.tagName === 'A' && el.hasAttribute('href')) { el.setAttribute('target', '_blank'); el.setAttribute('rel', 'noopener noreferrer'); }
|
|
31
|
+
if (el.tagName === 'IMG') {
|
|
32
|
+
el.setAttribute('class', IMG_CLASS);
|
|
33
|
+
const src = el.getAttribute('src') ?? '';
|
|
34
|
+
// `/uploads/*` 401s without a bearer header — park the src; resolveImages() swaps in a blob URL.
|
|
35
|
+
if (NEEDS_AUTH.test(src)) { el.setAttribute('data-authed-src', src); el.removeAttribute('src'); }
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const sanitize = (html: string) => purify.sanitize(html);
|
|
39
|
+
|
|
40
|
+
const code = (text: string, lang: string) => {
|
|
41
|
+
const base = codeBlock(text, lang);
|
|
42
|
+
if (!lang || !hljs.getLanguage(lang)) return base;
|
|
43
|
+
const html = hljs.highlight(text, { language: lang, ignoreIllegals: true }).value;
|
|
44
|
+
return base.replace(/<code>[\s\S]*<\/code>/, () => `<code class="hljs language-${lang}">${html}</code>`);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function AssistantMarkdown({ text, streaming = false, onImageClick }: { text: string; streaming?: boolean; onImageClick?: (src: string) => void }) {
|
|
48
|
+
const clean = text.replace(/\[Image #\d+\]\s*/g, '').trim();
|
|
49
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
50
|
+
const view = useMemo(() => new MarkdownStream({ Marked, sanitize, code }), []);
|
|
51
|
+
const shown = useRef<{ text: string; live: boolean } | null>(null);
|
|
52
|
+
const blobs = useRef(new Map<string, Promise<string>>());
|
|
53
|
+
const { getToken } = useWorkspace();
|
|
54
|
+
const { dark } = useDarkMode();
|
|
55
|
+
|
|
56
|
+
useLayoutEffect(() => {
|
|
57
|
+
const el = ref.current;
|
|
58
|
+
if (!el) return;
|
|
59
|
+
const s = shown.current;
|
|
60
|
+
const extends_ = !!s?.live && clean.startsWith(s.text);
|
|
61
|
+
if (streaming) {
|
|
62
|
+
if (!extends_) { el.replaceChildren(); view.attach(el); view.push(clean); }
|
|
63
|
+
else if (clean.length > s!.text.length) view.push(clean.slice(s!.text.length));
|
|
64
|
+
} else if (extends_) {
|
|
65
|
+
view.push(clean.slice(s!.text.length));
|
|
66
|
+
view.end();
|
|
67
|
+
} else if (!s || s.text !== clean) {
|
|
68
|
+
view.attach(el); // binds the Copy-button delegate; toHtml renders the settled message in one pass
|
|
69
|
+
el.innerHTML = view.toHtml(clean);
|
|
70
|
+
}
|
|
71
|
+
shown.current = { text: clean, live: streaming };
|
|
72
|
+
for (const img of el.querySelectorAll<HTMLImageElement>('img[data-authed-src]:not([src])')) {
|
|
73
|
+
const src = img.dataset.authedSrc!;
|
|
74
|
+
if (!blobs.current.has(src)) blobs.current.set(src, authedBlobUrl(src, getToken));
|
|
75
|
+
blobs.current.get(src)!.then((url) => { img.src = url; }, (err) => log.error('image load failed:', src, err));
|
|
76
|
+
}
|
|
77
|
+
}, [clean, streaming, view, getToken]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => () => {
|
|
80
|
+
view.detach();
|
|
81
|
+
shown.current = null;
|
|
82
|
+
for (const p of blobs.current.values()) p.then(URL.revokeObjectURL, () => {});
|
|
83
|
+
blobs.current.clear();
|
|
84
|
+
}, [view]);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
ref={ref}
|
|
89
|
+
data-b3-theme={dark ? 'dark' : 'light'}
|
|
90
|
+
onClick={(e) => { const t = e.target as HTMLElement; if (t instanceof HTMLImageElement && t.src) onImageClick?.(t.src); }}
|
|
91
|
+
className="b3-root assistant-md bg-transparent prose prose-sm max-w-none dark:prose-invert prose-code:before:content-none prose-code:after:content-none break-words min-w-0"
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -5,7 +5,15 @@ import { useWorkspace } from '@/lib/workspaceContext';
|
|
|
5
5
|
// `/uploads/*` is gated by requireAuth on the server, so a plain <img src>/<a href> GET 401s
|
|
6
6
|
// (no Authorization header). Fetch with a fresh bearer token and render/open a blob URL instead.
|
|
7
7
|
// Other srcs (data:, http(s):, blob:) need no auth.
|
|
8
|
-
const NEEDS_AUTH = /^\/uploads\//;
|
|
8
|
+
export const NEEDS_AUTH = /^\/uploads\//;
|
|
9
|
+
|
|
10
|
+
/** Fetch an auth-gated src with a bearer token → a blob URL the caller must revoke. */
|
|
11
|
+
export async function authedBlobUrl(src: string, getToken: () => Promise<string | null | undefined>): Promise<string> {
|
|
12
|
+
const token = await getToken();
|
|
13
|
+
const res = await fetch(src, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
|
|
14
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
15
|
+
return URL.createObjectURL(await res.blob());
|
|
16
|
+
}
|
|
9
17
|
|
|
10
18
|
interface ImageProps {
|
|
11
19
|
src: string;
|
|
@@ -29,10 +37,7 @@ export function AuthedImage({ src, alt, className, onClick }: ImageProps) {
|
|
|
29
37
|
setResolved(null);
|
|
30
38
|
(async () => {
|
|
31
39
|
try {
|
|
32
|
-
|
|
33
|
-
const res = await fetch(src, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
|
|
34
|
-
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
35
|
-
objectUrl = URL.createObjectURL(await res.blob());
|
|
40
|
+
objectUrl = await authedBlobUrl(src, getToken);
|
|
36
41
|
if (cancelled) URL.revokeObjectURL(objectUrl);
|
|
37
42
|
else setResolved(objectUrl);
|
|
38
43
|
} catch (err) {
|
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import
|
|
3
|
-
import remarkGfm from 'remark-gfm';
|
|
4
|
-
import rehypeHighlight from 'rehype-highlight';
|
|
5
|
-
import type { Root, Element } from 'hast';
|
|
6
|
-
import type { Plugin } from 'unified';
|
|
7
|
-
|
|
8
|
-
const RTL_BLOCK_TAGS = new Set(['p', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'td', 'th']);
|
|
9
|
-
const rehypeBidi: Plugin<[], Root> = () => (tree) => {
|
|
10
|
-
const visit = (node: Root | Element) => {
|
|
11
|
-
for (const child of (node.children ?? [])) {
|
|
12
|
-
if (child.type === 'element') {
|
|
13
|
-
if (RTL_BLOCK_TAGS.has(child.tagName)) {
|
|
14
|
-
child.properties ??= {};
|
|
15
|
-
child.properties.dir = 'auto';
|
|
16
|
-
}
|
|
17
|
-
visit(child);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
visit(tree);
|
|
22
|
-
};
|
|
23
|
-
import 'highlight.js/styles/github.css';
|
|
24
|
-
import { ChevronRight, Wrench, User, Bot, Copy, Check, RotateCcw, Pencil, X, SendHorizontal, ShieldQuestion, CheckCircle2, XCircle, Eye, EyeOff, Info, Loader2, BrainCircuit, GitFork, Minimize2 } from 'lucide-react';
|
|
2
|
+
import { ChevronRight, Wrench, User, Bot, Check, RotateCcw, Pencil, X, SendHorizontal, ShieldQuestion, CheckCircle2, XCircle, Eye, EyeOff, Info, Loader2, BrainCircuit, GitFork, Minimize2 } from 'lucide-react';
|
|
25
3
|
import { cn } from '@/lib/utils';
|
|
26
4
|
import { toolPreview } from '@/lib/tool-preview';
|
|
27
5
|
import { annotateLimitReset } from '@/lib/limit-reset';
|
|
@@ -32,33 +10,15 @@ import { AuthedImage, AuthedFileLink } from './AuthedImage';
|
|
|
32
10
|
import { useSlots } from '@/lib/slots';
|
|
33
11
|
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
|
34
12
|
import { ZoomableImage } from './ZoomableImage';
|
|
13
|
+
import { AssistantMarkdown } from './AssistantMarkdown';
|
|
35
14
|
|
|
36
15
|
// The appwrap iOS WKWebView wedges its GPU compositor when rendering react-markdown's DOM for a chat with
|
|
37
16
|
// history — JS keeps running but paint + hit-testing freeze for ~1-2min (screen stuck/touch-dead) before
|
|
38
17
|
// WebKit lazily restores. The SAME content in mobile Safari and on desktop is fine, so this is specific to
|
|
39
|
-
// the native WKWebView shell, not the web app. Until the native shell issue is fixed, render assistant
|
|
18
|
+
// the native WKWebView shell, not the web app. (Observed with react-markdown; MarkdownStream is unverified there.) Until the native shell issue is fixed, render assistant
|
|
40
19
|
// messages as PLAIN TEXT inside the native app only; full markdown stays on Safari/desktop/web.
|
|
41
20
|
const IS_APPWRAP_NATIVE = typeof window !== 'undefined' && !!(window as { webkit?: { messageHandlers?: { appwrap?: unknown } } }).webkit?.messageHandlers?.appwrap;
|
|
42
21
|
|
|
43
|
-
function AssistantMarkdown({ text, onImageClick }: { text: string; onImageClick?: (src: string) => void }) {
|
|
44
|
-
const clean = text.replace(/\[Image #\d+\]\s*/g, '').trim();
|
|
45
|
-
if (IS_APPWRAP_NATIVE) {
|
|
46
|
-
return <div className="prose prose-sm max-w-none dark:prose-invert break-words min-w-0 whitespace-pre-wrap">{clean}</div>;
|
|
47
|
-
}
|
|
48
|
-
return (
|
|
49
|
-
<ReactMarkdown
|
|
50
|
-
remarkPlugins={[remarkGfm]}
|
|
51
|
-
rehypePlugins={[rehypeHighlight, rehypeBidi]}
|
|
52
|
-
className="prose prose-sm max-w-none dark:prose-invert prose-pre:bg-muted prose-pre:border prose-code:before:content-none prose-code:after:content-none break-words min-w-0"
|
|
53
|
-
components={{
|
|
54
|
-
pre: ({ children, ...props }) => <CodeBlock {...props}>{children}</CodeBlock>,
|
|
55
|
-
a: ({ href, children }) => <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>,
|
|
56
|
-
img: ({ src, alt }) => <AuthedImage src={src ?? ''} alt={alt ?? ''} className="max-h-[80vh] max-w-full rounded-xl border object-contain cursor-pointer hover:opacity-80 transition-opacity" onClick={(s) => onImageClick?.(s)} />,
|
|
57
|
-
}}
|
|
58
|
-
>{clean}</ReactMarkdown>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
22
|
function formatTs(ts: number): string {
|
|
63
23
|
const d = new Date(ts);
|
|
64
24
|
const now = new Date();
|
|
@@ -397,7 +357,7 @@ const MessageRow = memo(function MessageRow({
|
|
|
397
357
|
const result = block.type === 'tool_use'
|
|
398
358
|
? message.blocks.find((b): b is MessageBlock & { type: 'tool_result' } => b.type === 'tool_result' && b.toolUseId === block.toolUseId)
|
|
399
359
|
: undefined;
|
|
400
|
-
return <BlockRenderer key={i} block={block} isUser={isUser} ts={message.ts} onPermissionRespond={onPermissionRespond} onQuestionRespond={onQuestionRespond} pairedResult={result} onImageClick={onImageClick} busy={busy} screenMap={screenMap} />;
|
|
360
|
+
return <BlockRenderer key={i} block={block} isUser={isUser} ts={message.ts} onPermissionRespond={onPermissionRespond} onQuestionRespond={onQuestionRespond} pairedResult={result} onImageClick={onImageClick} busy={busy} live={!!busy && block === message.blocks[message.blocks.length - 1]} screenMap={screenMap} />;
|
|
401
361
|
})}
|
|
402
362
|
{isUser && userText && (
|
|
403
363
|
<div className={cn('flex gap-1 items-center justify-end transition-opacity', actionsVisible ? 'opacity-100' : 'opacity-0 group-hover/row:opacity-100')}>
|
|
@@ -462,7 +422,7 @@ function CompactMarkerDivider({ summary, compactedCount }: { summary: string; co
|
|
|
462
422
|
);
|
|
463
423
|
}
|
|
464
424
|
|
|
465
|
-
function BlockRenderer({ block, isUser, ts, onPermissionRespond, onQuestionRespond, pairedResult, onImageClick, busy, screenMap }: { block: MessageBlock; isUser: boolean; ts?: number; onPermissionRespond?: (id: string, allow: boolean, allowAll?: boolean) => void; onQuestionRespond?: (id: string, answers: QuestionAnswers) => void; pairedResult?: MessageBlock & { type: 'tool_result' }; onImageClick?: (src: string) => void; busy?: boolean; screenMap?: Map<string, string[]> }) {
|
|
425
|
+
function BlockRenderer({ block, isUser, ts, onPermissionRespond, onQuestionRespond, pairedResult, onImageClick, busy, live, screenMap }: { block: MessageBlock; isUser: boolean; ts?: number; onPermissionRespond?: (id: string, allow: boolean, allowAll?: boolean) => void; onQuestionRespond?: (id: string, answers: QuestionAnswers) => void; pairedResult?: MessageBlock & { type: 'tool_result' }; onImageClick?: (src: string) => void; busy?: boolean; /** This text block is the one currently streaming. */ live?: boolean; screenMap?: Map<string, string[]> }) {
|
|
466
426
|
const slots = useSlots();
|
|
467
427
|
if (block.type === 'image') {
|
|
468
428
|
return (
|
|
@@ -500,7 +460,9 @@ function BlockRenderer({ block, isUser, ts, onPermissionRespond, onQuestionRespo
|
|
|
500
460
|
}
|
|
501
461
|
return (
|
|
502
462
|
<div className="text-sm" dir="auto">
|
|
503
|
-
|
|
463
|
+
{IS_APPWRAP_NATIVE
|
|
464
|
+
? <div className="prose prose-sm max-w-none dark:prose-invert break-words min-w-0 whitespace-pre-wrap">{block.text.replace(/\[Image #\d+\]\s*/g, '').trim()}</div>
|
|
465
|
+
: <AssistantMarkdown text={block.text} streaming={live} onImageClick={onImageClick} />}
|
|
504
466
|
</div>
|
|
505
467
|
);
|
|
506
468
|
}
|
|
@@ -938,33 +900,3 @@ function QuestionRequestBlock({
|
|
|
938
900
|
</div>
|
|
939
901
|
);
|
|
940
902
|
}
|
|
941
|
-
|
|
942
|
-
function CodeBlock({ children, ...props }: any) {
|
|
943
|
-
const ref = useRef<HTMLPreElement>(null);
|
|
944
|
-
return (
|
|
945
|
-
<div className="relative group">
|
|
946
|
-
<pre ref={ref} {...props} className="rounded-lg border bg-muted p-4 overflow-x-auto text-xs text-foreground">
|
|
947
|
-
{children}
|
|
948
|
-
</pre>
|
|
949
|
-
<CopyButton getText={() => ref.current?.textContent || ''} />
|
|
950
|
-
</div>
|
|
951
|
-
);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
function CopyButton({ getText }: { getText: () => string }) {
|
|
955
|
-
const [copied, setCopied] = useState(false);
|
|
956
|
-
|
|
957
|
-
return (
|
|
958
|
-
<button
|
|
959
|
-
onClick={() => {
|
|
960
|
-
navigator.clipboard.writeText(getText());
|
|
961
|
-
setCopied(true);
|
|
962
|
-
setTimeout(() => setCopied(false), 2000);
|
|
963
|
-
}}
|
|
964
|
-
className="absolute top-2 right-2 p-1.5 rounded-md bg-background/80 border opacity-0 group-hover:opacity-100 transition-opacity"
|
|
965
|
-
title="Copy"
|
|
966
|
-
>
|
|
967
|
-
{copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
|
|
968
|
-
</button>
|
|
969
|
-
);
|
|
970
|
-
}
|
|
@@ -538,7 +538,11 @@ export class ClaudeCodeEngine implements AgentEngine {
|
|
|
538
538
|
// where the profile grants Bash — job_start is arbitrary shell — and only for a real session id
|
|
539
539
|
// (wake reports need a session to land in).
|
|
540
540
|
if (durableJobs) {
|
|
541
|
-
|
|
541
|
+
// The job store scrubs `*_API_KEY` from children, so a nested agent (the social legs' agentx worker)
|
|
542
|
+
// died "No provider key found" (2026-09-24). Hand back the provider keys THIS turn's own env already
|
|
543
|
+
// carries (profile-filtered by buildAgentEnv) — the same set the agentx engine gives its jobs.
|
|
544
|
+
const providerEnv = Object.fromEntries(Object.entries(sdkEnv).filter(([k, v]) => /_API_KEY$/.test(k) && v));
|
|
545
|
+
const server = jobsMcpServer({ sessionId: opts.sessionId!, uid: opts.uid, userEmail: opts.userEmail, cwd, env: providerEnv });
|
|
542
546
|
options['mcpServers'] = { ...(options['mcpServers'] as object | undefined), [server.name]: server };
|
|
543
547
|
options['allowedTools'] = [...((options['allowedTools'] as string[] | undefined) ?? allowedTools), ...JOBS_TOOL_IDS];
|
|
544
548
|
}
|