@plmbr/notebook-intelligence 5.0.0 → 5.1.0-a.1
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/README.md +11 -5
- package/lib/api.d.ts +5 -0
- package/lib/api.js +7 -0
- package/lib/chat-sidebar.js +166 -65
- package/lib/components/markdown-link.d.ts +33 -0
- package/lib/components/markdown-link.js +114 -0
- package/lib/components/safe-anchor.d.ts +25 -0
- package/lib/components/safe-anchor.js +33 -0
- package/lib/components/tool-call-card.d.ts +26 -0
- package/lib/components/tool-call-card.js +54 -0
- package/lib/components/tool-call-group.d.ts +11 -0
- package/lib/components/tool-call-group.js +33 -0
- package/lib/icons.d.ts +3 -0
- package/lib/icons.js +3 -0
- package/lib/index.js +5 -7
- package/lib/markdown-renderer.js +23 -1
- package/lib/tokens.d.ts +1 -0
- package/lib/tokens.js +1 -0
- package/lib/tool-call-stream.d.ts +24 -0
- package/lib/tool-call-stream.js +28 -0
- package/lib/utils.d.ts +9 -0
- package/lib/utils.js +22 -2
- package/package.json +7 -1
- package/src/api.ts +8 -0
- package/src/chat-sidebar.tsx +203 -93
- package/src/components/markdown-link.tsx +161 -0
- package/src/components/safe-anchor.tsx +60 -0
- package/src/components/tool-call-card.tsx +146 -0
- package/src/components/tool-call-group.tsx +65 -0
- package/src/icons.ts +3 -0
- package/src/index.ts +6 -6
- package/src/markdown-renderer.tsx +30 -0
- package/src/tokens.ts +1 -0
- package/src/tool-call-stream.ts +44 -0
- package/src/utils.ts +25 -2
- package/style/base.css +173 -4
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { hasDangerousTextCodepoints, safeAnchorUri } from '../utils';
|
|
4
|
+
/**
|
|
5
|
+
* The single render path for anchor elements driven by LLM / tool output.
|
|
6
|
+
*
|
|
7
|
+
* Runs `href` through `safeAnchorUri`, which mirrors the server-side
|
|
8
|
+
* `safe_anchor_uri` allowlist (`http` / `https` / `mailto`) and rejects
|
|
9
|
+
* dangerous codepoints. On accept it renders a `_blank` anchor with
|
|
10
|
+
* `rel="noopener noreferrer"` and an SR-only "(opens in new tab)" suffix;
|
|
11
|
+
* on reject it falls through to plain text plus an SR-only "(link
|
|
12
|
+
* blocked)" note so screen readers can tell why the link disappeared.
|
|
13
|
+
*
|
|
14
|
+
* The `title` attribute is scrubbed for the same dangerous codepoints
|
|
15
|
+
* the URI check rejects, since react-markdown forwards CommonMark
|
|
16
|
+
* `[text](url "title")` titles to the rendered anchor and an LLM can
|
|
17
|
+
* smuggle bidi-override or zero-width characters there to visually
|
|
18
|
+
* impersonate the link target on hover.
|
|
19
|
+
*/
|
|
20
|
+
export function SafeAnchor({ href, children, title, className }) {
|
|
21
|
+
const safeUri = safeAnchorUri(href !== null && href !== void 0 ? href : '');
|
|
22
|
+
if (!safeUri) {
|
|
23
|
+
return (React.createElement("span", { className: className },
|
|
24
|
+
children,
|
|
25
|
+
React.createElement("span", { className: "nbi-sr-only" }, " (link blocked)")));
|
|
26
|
+
}
|
|
27
|
+
const safeTitle = typeof title === 'string' && !hasDangerousTextCodepoints(title)
|
|
28
|
+
? title
|
|
29
|
+
: undefined;
|
|
30
|
+
return (React.createElement("a", { href: safeUri, target: "_blank", rel: "noopener noreferrer", title: safeTitle, className: className },
|
|
31
|
+
children,
|
|
32
|
+
React.createElement("span", { className: "nbi-sr-only" }, " (opens in new tab)")));
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
export interface IToolCallDiffLine {
|
|
3
|
+
type: 'add' | 'remove' | 'context' | string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface IToolCallDiff {
|
|
7
|
+
path: string;
|
|
8
|
+
lines: IToolCallDiffLine[];
|
|
9
|
+
truncated?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A single agent tool call surfaced as a persistent chat card. Mirrors the
|
|
13
|
+
* `ToolCallData` payload emitted by the server: it stays in the transcript
|
|
14
|
+
* after the turn ends and carries its final status, unlike the transient
|
|
15
|
+
* single progress line it replaces. File-edit tools also carry inline diffs.
|
|
16
|
+
*/
|
|
17
|
+
export interface IToolCall {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
kind: 'read' | 'edit' | 'execute' | 'other' | string;
|
|
21
|
+
status: 'in_progress' | 'completed' | 'failed' | 'cancelled' | string;
|
|
22
|
+
diffs?: IToolCallDiff[];
|
|
23
|
+
}
|
|
24
|
+
export declare function ToolCallCard(props: {
|
|
25
|
+
toolCall: IToolCall;
|
|
26
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { VscChevronDown, VscChevronRight, VscClose, VscEdit, VscError, VscEye, VscPassFilled, VscSync, VscTerminal, VscTools } from '../icons';
|
|
3
|
+
const KIND_ICONS = {
|
|
4
|
+
read: VscEye,
|
|
5
|
+
edit: VscEdit,
|
|
6
|
+
execute: VscTerminal,
|
|
7
|
+
other: VscTools
|
|
8
|
+
};
|
|
9
|
+
const STATUS_LABELS = {
|
|
10
|
+
in_progress: 'in progress',
|
|
11
|
+
completed: 'completed',
|
|
12
|
+
failed: 'failed',
|
|
13
|
+
cancelled: 'cancelled'
|
|
14
|
+
};
|
|
15
|
+
const GUTTER = { add: '+', remove: '-' };
|
|
16
|
+
function ToolCallDiffView(props) {
|
|
17
|
+
return (React.createElement("div", { className: "nbi-tool-call-diffs" }, props.diffs.map((diff, i) => (React.createElement("div", { className: "nbi-tool-call-diff", key: i },
|
|
18
|
+
diff.path ? (React.createElement("div", { className: "nbi-tool-call-diff-path", title: diff.path }, diff.path)) : null,
|
|
19
|
+
React.createElement("div", { className: "nbi-tool-call-diff-body" }, diff.lines.map((line, j) => {
|
|
20
|
+
var _a;
|
|
21
|
+
return (React.createElement("div", { className: `nbi-tool-call-diff-line nbi-diff-${line.type}`, key: j },
|
|
22
|
+
React.createElement("span", { className: "nbi-diff-gutter", "aria-hidden": "true" }, (_a = GUTTER[line.type]) !== null && _a !== void 0 ? _a : ' '),
|
|
23
|
+
line.type === 'add' || line.type === 'remove' ? (React.createElement("span", { className: "nbi-sr-only" }, line.type === 'add' ? 'added ' : 'removed ')) : null,
|
|
24
|
+
React.createElement("span", { className: "nbi-diff-text" }, line.content)));
|
|
25
|
+
})),
|
|
26
|
+
diff.truncated ? (React.createElement("div", { className: "nbi-tool-call-diff-truncated" }, "diff truncated")) : null)))));
|
|
27
|
+
}
|
|
28
|
+
export function ToolCallCard(props) {
|
|
29
|
+
var _a, _b;
|
|
30
|
+
const { title, kind, status, diffs } = props.toolCall;
|
|
31
|
+
const [expanded, setExpanded] = useState(true);
|
|
32
|
+
const KindIcon = (_a = KIND_ICONS[kind]) !== null && _a !== void 0 ? _a : VscTools;
|
|
33
|
+
let StatusIcon = VscSync;
|
|
34
|
+
if (status === 'completed') {
|
|
35
|
+
StatusIcon = VscPassFilled;
|
|
36
|
+
}
|
|
37
|
+
else if (status === 'failed') {
|
|
38
|
+
StatusIcon = VscError;
|
|
39
|
+
}
|
|
40
|
+
else if (status === 'cancelled') {
|
|
41
|
+
StatusIcon = VscClose;
|
|
42
|
+
}
|
|
43
|
+
const statusLabel = (_b = STATUS_LABELS[status]) !== null && _b !== void 0 ? _b : status;
|
|
44
|
+
const statusModifier = status.replace(/_/g, '-');
|
|
45
|
+
const hasDiffs = Array.isArray(diffs) && diffs.length > 0;
|
|
46
|
+
return (React.createElement("div", { className: "nbi-tool-call-wrapper" },
|
|
47
|
+
React.createElement("div", { className: `nbi-tool-call nbi-tool-call-${statusModifier}` },
|
|
48
|
+
React.createElement(KindIcon, { className: "nbi-tool-call-kind-icon", "aria-hidden": "true" }),
|
|
49
|
+
React.createElement("span", { className: "nbi-tool-call-title", title: title }, title),
|
|
50
|
+
hasDiffs ? (React.createElement("button", { type: "button", className: "nbi-tool-call-diff-toggle", onClick: () => setExpanded(e => !e), "aria-expanded": expanded, "aria-label": expanded ? 'Hide diff' : 'Show diff' }, expanded ? (React.createElement(VscChevronDown, { "aria-hidden": "true" })) : (React.createElement(VscChevronRight, { "aria-hidden": "true" })))) : null,
|
|
51
|
+
React.createElement(StatusIcon, { className: "nbi-tool-call-status-icon", "aria-hidden": "true" }),
|
|
52
|
+
React.createElement("span", { className: "nbi-sr-only" }, statusLabel)),
|
|
53
|
+
hasDiffs && expanded ? React.createElement(ToolCallDiffView, { diffs: diffs }) : null));
|
|
54
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IToolCall } from './tool-call-card';
|
|
3
|
+
/**
|
|
4
|
+
* Renders a run of consecutive tool calls. A single call is shown as a bare
|
|
5
|
+
* card; multiple calls are wrapped in one collapsible group with a summary
|
|
6
|
+
* header, so consecutive agent activity reads as one unit instead of a wall
|
|
7
|
+
* of rows.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ToolCallGroup(props: {
|
|
10
|
+
toolCalls: IToolCall[];
|
|
11
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { VscChevronDown, VscChevronRight } from '../icons';
|
|
3
|
+
import { ToolCallCard } from './tool-call-card';
|
|
4
|
+
// Groups with more calls than this start collapsed so a tool-heavy turn (or a
|
|
5
|
+
// reloaded transcript) doesn't flood the chat. A live turn mounts the group at
|
|
6
|
+
// length 1, so it starts expanded and stays visible as calls stream in.
|
|
7
|
+
const GROUP_COLLAPSE_THRESHOLD = 3;
|
|
8
|
+
/**
|
|
9
|
+
* Renders a run of consecutive tool calls. A single call is shown as a bare
|
|
10
|
+
* card; multiple calls are wrapped in one collapsible group with a summary
|
|
11
|
+
* header, so consecutive agent activity reads as one unit instead of a wall
|
|
12
|
+
* of rows.
|
|
13
|
+
*/
|
|
14
|
+
export function ToolCallGroup(props) {
|
|
15
|
+
const { toolCalls } = props;
|
|
16
|
+
// Hooks must run unconditionally and in a stable order: the group's length
|
|
17
|
+
// grows as calls stream in, so call useState before any length branch.
|
|
18
|
+
// Start collapsed only for a large group whose calls are all settled --
|
|
19
|
+
// never hide a still-running or failed call (matters on transcript reload,
|
|
20
|
+
// where a group can mount already large).
|
|
21
|
+
const [expanded, setExpanded] = useState(() => toolCalls.length <= GROUP_COLLAPSE_THRESHOLD ||
|
|
22
|
+
toolCalls.some(t => t.status === 'in_progress' || t.status === 'failed'));
|
|
23
|
+
if (toolCalls.length <= 1) {
|
|
24
|
+
return toolCalls.length === 1 ? (React.createElement(ToolCallCard, { toolCall: toolCalls[0] })) : (React.createElement(React.Fragment, null));
|
|
25
|
+
}
|
|
26
|
+
const failed = toolCalls.filter(t => t.status === 'failed').length;
|
|
27
|
+
const summary = `${toolCalls.length} tool calls` + (failed ? ` (${failed} failed)` : '');
|
|
28
|
+
return (React.createElement("div", { className: "nbi-tool-call-group" },
|
|
29
|
+
React.createElement("button", { type: "button", className: "nbi-tool-call-group-header", onClick: () => setExpanded(e => !e), "aria-expanded": expanded },
|
|
30
|
+
expanded ? (React.createElement(VscChevronDown, { "aria-hidden": "true" })) : (React.createElement(VscChevronRight, { "aria-hidden": "true" })),
|
|
31
|
+
React.createElement("span", { className: "nbi-tool-call-group-summary" }, summary)),
|
|
32
|
+
expanded ? (React.createElement("div", { className: "nbi-tool-call-group-body" }, toolCalls.map(toolCall => (React.createElement(ToolCallCard, { key: toolCall.id, toolCall: toolCall }))))) : null));
|
|
33
|
+
}
|
package/lib/icons.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare const VscClose: IconLike;
|
|
|
17
17
|
export declare const VscCloudUpload: IconLike;
|
|
18
18
|
export declare const VscCopy: IconLike;
|
|
19
19
|
export declare const VscEdit: IconLike;
|
|
20
|
+
export declare const VscError: IconLike;
|
|
20
21
|
export declare const VscEye: IconLike;
|
|
21
22
|
export declare const VscEyeClosed: IconLike;
|
|
22
23
|
export declare const VscFile: IconLike;
|
|
@@ -31,6 +32,8 @@ export declare const VscSend: IconLike;
|
|
|
31
32
|
export declare const VscSettingsGear: IconLike;
|
|
32
33
|
export declare const VscSparkle: IconLike;
|
|
33
34
|
export declare const VscStopCircle: IconLike;
|
|
35
|
+
export declare const VscSync: IconLike;
|
|
36
|
+
export declare const VscTerminal: IconLike;
|
|
34
37
|
export declare const VscThumbsdown: IconLike;
|
|
35
38
|
export declare const VscThumbsdownFilled: IconLike;
|
|
36
39
|
export declare const VscThumbsup: IconLike;
|
package/lib/icons.js
CHANGED
|
@@ -27,6 +27,7 @@ export const VscClose = asIcon(Vsc.VscClose);
|
|
|
27
27
|
export const VscCloudUpload = asIcon(Vsc.VscCloudUpload);
|
|
28
28
|
export const VscCopy = asIcon(Vsc.VscCopy);
|
|
29
29
|
export const VscEdit = asIcon(Vsc.VscEdit);
|
|
30
|
+
export const VscError = asIcon(Vsc.VscError);
|
|
30
31
|
export const VscEye = asIcon(Vsc.VscEye);
|
|
31
32
|
export const VscEyeClosed = asIcon(Vsc.VscEyeClosed);
|
|
32
33
|
export const VscFile = asIcon(Vsc.VscFile);
|
|
@@ -41,6 +42,8 @@ export const VscSend = asIcon(Vsc.VscSend);
|
|
|
41
42
|
export const VscSettingsGear = asIcon(Vsc.VscSettingsGear);
|
|
42
43
|
export const VscSparkle = asIcon(Vsc.VscSparkle);
|
|
43
44
|
export const VscStopCircle = asIcon(Vsc.VscStopCircle);
|
|
45
|
+
export const VscSync = asIcon(Vsc.VscSync);
|
|
46
|
+
export const VscTerminal = asIcon(Vsc.VscTerminal);
|
|
44
47
|
export const VscThumbsdown = asIcon(Vsc.VscThumbsdown);
|
|
45
48
|
export const VscThumbsdownFilled = asIcon(Vsc.VscThumbsdownFilled);
|
|
46
49
|
export const VscThumbsup = asIcon(Vsc.VscThumbsup);
|
package/lib/index.js
CHANGED
|
@@ -36,7 +36,7 @@ import sparklesWarningSvgstr from '../style/icons/sparkles-warning.svg';
|
|
|
36
36
|
import claudeSvgstr from '../style/icons/claude.svg';
|
|
37
37
|
import openaiSvgstr from '../style/icons/openai.svg';
|
|
38
38
|
import opencodeSvgstr from '../style/icons/opencode.svg';
|
|
39
|
-
import { applyCodeToSelectionInEditor, cellOutputAsText, cellOutputHasError, chooseWorkspaceDirectory, compareSelections, extractLLMGeneratedCode, getSelectionInEditor, getTokenCount, getWholeNotebookContent, isSelectionEmpty, markdownToComment, waitForDuration } from './utils';
|
|
39
|
+
import { applyCodeToSelectionInEditor, cellOutputAsText, cellOutputHasError, chooseWorkspaceDirectory, compareSelections, buildResumeCommand, extractLLMGeneratedCode, getSelectionInEditor, getTokenCount, getWholeNotebookContent, isSelectionEmpty, markdownToComment, waitForDuration } from './utils';
|
|
40
40
|
import { cellOutputAsContextBundle } from './cell-output-bundle';
|
|
41
41
|
import { UUID } from '@lumino/coreutils';
|
|
42
42
|
import * as path from 'path';
|
|
@@ -1108,11 +1108,9 @@ const plugin = {
|
|
|
1108
1108
|
render() {
|
|
1109
1109
|
return React.createElement(LauncherPicker, {
|
|
1110
1110
|
onSessionSelected: (session) => {
|
|
1111
|
+
var _a;
|
|
1111
1112
|
dialog.close();
|
|
1112
|
-
|
|
1113
|
-
? `cd ${session.cwd} && claude --resume ${session.session_id}`
|
|
1114
|
-
: `claude --resume ${session.session_id}`;
|
|
1115
|
-
launchCliInTerminal(cmd);
|
|
1113
|
+
launchCliInTerminal(buildResumeCommand((_a = session.cwd) !== null && _a !== void 0 ? _a : '', session.session_id));
|
|
1116
1114
|
}
|
|
1117
1115
|
});
|
|
1118
1116
|
}
|
|
@@ -1360,7 +1358,7 @@ const plugin = {
|
|
|
1360
1358
|
metadata: { trusted: true },
|
|
1361
1359
|
source: args.source
|
|
1362
1360
|
});
|
|
1363
|
-
return
|
|
1361
|
+
return { cellIndex: newCellIndex };
|
|
1364
1362
|
}
|
|
1365
1363
|
});
|
|
1366
1364
|
app.commands.addCommand(CommandIDs.addCodeCellToActiveNotebook, {
|
|
@@ -1378,7 +1376,7 @@ const plugin = {
|
|
|
1378
1376
|
metadata: { trusted: true },
|
|
1379
1377
|
source: args.source
|
|
1380
1378
|
});
|
|
1381
|
-
return
|
|
1379
|
+
return { cellIndex: newCellIndex };
|
|
1382
1380
|
}
|
|
1383
1381
|
});
|
|
1384
1382
|
app.commands.addCommand(CommandIDs.getCellTypeAndSource, {
|
package/lib/markdown-renderer.js
CHANGED
|
@@ -6,12 +6,34 @@ import { Prism as SyntaxHighlighterBase } from 'react-syntax-highlighter';
|
|
|
6
6
|
const SyntaxHighlighter = SyntaxHighlighterBase;
|
|
7
7
|
import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
|
8
8
|
import { VscNewFile, VscInsert, VscCopy, VscNotebook, VscAdd } from './icons';
|
|
9
|
+
import { PathExt } from '@jupyterlab/coreutils';
|
|
10
|
+
import { MarkdownLink } from './components/markdown-link';
|
|
9
11
|
import { isDarkTheme, writeTextToClipboard } from './utils';
|
|
10
12
|
export function MarkdownRenderer({ children: markdown, getApp, getActiveDocumentInfo }) {
|
|
11
13
|
const app = getApp();
|
|
12
14
|
const activeDocumentInfo = getActiveDocumentInfo();
|
|
13
15
|
const isNotebook = activeDocumentInfo.filename.endsWith('.ipynb');
|
|
14
|
-
|
|
16
|
+
// Resolve workspace-relative LLM links against the active document's
|
|
17
|
+
// directory so `[file](README.md)` from a chat scoped to
|
|
18
|
+
// `notebooks/proj/work.ipynb` lands at `notebooks/proj/README.md` (the
|
|
19
|
+
// user's mental model) rather than the server-root README.
|
|
20
|
+
const linkBaseDir = activeDocumentInfo.filePath
|
|
21
|
+
? PathExt.dirname(activeDocumentInfo.filePath)
|
|
22
|
+
: '';
|
|
23
|
+
return (
|
|
24
|
+
// No `rehype-raw` plugin: raw HTML in chat markdown (e.g. an LLM
|
|
25
|
+
// emitting `<a href="javascript:...">`) renders as literal text, not
|
|
26
|
+
// a DOM anchor, so the only anchor sink is the CommonMark/GFM `a`
|
|
27
|
+
// node handled by `SafeAnchor` below. Any future change that enables
|
|
28
|
+
// raw HTML needs to add a rehype-sanitize pass alongside.
|
|
29
|
+
React.createElement(Markdown, { remarkPlugins: [remarkGfm], components: {
|
|
30
|
+
// CommonMark `<https://...>` autolinks, `[text](url)`, and
|
|
31
|
+
// reference-style links all normalize to the same `a` node.
|
|
32
|
+
// `MarkdownLink` routes fragment-only and workspace-relative
|
|
33
|
+
// hrefs through Lab's docmanager so an LLM-emitted link can't
|
|
34
|
+
// replace the JupyterLab shell, and hands everything else to
|
|
35
|
+
// SafeAnchor for the `_blank` + scheme-allowlist treatment.
|
|
36
|
+
a: ({ href, title, children }) => (React.createElement(MarkdownLink, { app: app, baseDir: linkBaseDir, href: href, title: title }, children)),
|
|
15
37
|
code({ node, inline, className, children, getApp, ...props }) {
|
|
16
38
|
const match = /language-(\w+)/.exec(className || '');
|
|
17
39
|
const codeString = String(children).replace(/\n$/, '');
|
package/lib/tokens.d.ts
CHANGED
package/lib/tokens.js
CHANGED
|
@@ -32,6 +32,7 @@ export var ResponseStreamDataType;
|
|
|
32
32
|
ResponseStreamDataType["Button"] = "button";
|
|
33
33
|
ResponseStreamDataType["Anchor"] = "anchor";
|
|
34
34
|
ResponseStreamDataType["Progress"] = "progress";
|
|
35
|
+
ResponseStreamDataType["ToolCall"] = "tool-call";
|
|
35
36
|
ResponseStreamDataType["Confirmation"] = "confirmation";
|
|
36
37
|
ResponseStreamDataType["AskUserQuestion"] = "ask-user-question";
|
|
37
38
|
})(ResponseStreamDataType || (ResponseStreamDataType = {}));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ResponseStreamDataType } from './tokens';
|
|
2
|
+
/**
|
|
3
|
+
* The subset of a chat message's stream-content item this helper needs. A
|
|
4
|
+
* structural subset of `IChatMessageContent` (defined in chat-sidebar) so it
|
|
5
|
+
* can be unit-tested without importing the sidebar (and creating a cycle).
|
|
6
|
+
*/
|
|
7
|
+
export interface IToolCallStreamItem {
|
|
8
|
+
id: string;
|
|
9
|
+
type: ResponseStreamDataType;
|
|
10
|
+
content: any;
|
|
11
|
+
created: Date;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Merge a streamed tool-call payload into `contents` by its tool-call id.
|
|
15
|
+
*
|
|
16
|
+
* A tool call streams twice under one id (once when it starts, once when it
|
|
17
|
+
* finishes). The first emission pushes a new card; the second updates that
|
|
18
|
+
* card's content (its status) in place, so the call stays a single persistent
|
|
19
|
+
* row rather than appending a duplicate. Mutates `contents`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function upsertToolCallContent(contents: IToolCallStreamItem[], content: {
|
|
22
|
+
id: string;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}, created: Date): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { UUID } from '@lumino/coreutils';
|
|
2
|
+
import { ResponseStreamDataType } from './tokens';
|
|
3
|
+
/**
|
|
4
|
+
* Merge a streamed tool-call payload into `contents` by its tool-call id.
|
|
5
|
+
*
|
|
6
|
+
* A tool call streams twice under one id (once when it starts, once when it
|
|
7
|
+
* finishes). The first emission pushes a new card; the second updates that
|
|
8
|
+
* card's content (its status) in place, so the call stays a single persistent
|
|
9
|
+
* row rather than appending a duplicate. Mutates `contents`.
|
|
10
|
+
*/
|
|
11
|
+
export function upsertToolCallContent(contents, content, created) {
|
|
12
|
+
const existing = contents.find(c => {
|
|
13
|
+
var _a;
|
|
14
|
+
return c.type === ResponseStreamDataType.ToolCall &&
|
|
15
|
+
((_a = c.content) === null || _a === void 0 ? void 0 : _a.id) === (content === null || content === void 0 ? void 0 : content.id);
|
|
16
|
+
});
|
|
17
|
+
if (existing) {
|
|
18
|
+
existing.content = content;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
contents.push({
|
|
22
|
+
id: UUID.uuid4(),
|
|
23
|
+
type: ResponseStreamDataType.ToolCall,
|
|
24
|
+
content,
|
|
25
|
+
created
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
package/lib/utils.d.ts
CHANGED
|
@@ -26,6 +26,15 @@ export declare function getSelectionInEditor(editor: CodeEditor.IEditor): string
|
|
|
26
26
|
export declare function getWholeNotebookContent(np: NotebookPanel): string;
|
|
27
27
|
export declare function applyCodeToSelectionInEditor(editor: CodeEditor.IEditor, code: string): void;
|
|
28
28
|
export { shellSingleQuote };
|
|
29
|
+
/**
|
|
30
|
+
* True when `s` contains any codepoint in the same set `safeAnchorUri`
|
|
31
|
+
* rejects (C0/DEL/C1, NEL/NBSP/LS/PS/BOM, ZWSP, bidi-override controls).
|
|
32
|
+
* Mirrors the Python `has_dangerous_text_codepoints`. Used to scrub the
|
|
33
|
+
* `title` attribute on rendered anchors so an LLM-emitted hover tooltip
|
|
34
|
+
* can't visually impersonate the link via bidi-reorder or zero-width
|
|
35
|
+
* tricks.
|
|
36
|
+
*/
|
|
37
|
+
export declare function hasDangerousTextCodepoints(s: string | undefined | null): boolean;
|
|
29
38
|
/**
|
|
30
39
|
* Return `uri` if its scheme is in the chat-anchor allowlist, else null.
|
|
31
40
|
* Mirrors the server-side `safe_anchor_uri` check so that anchor parts
|
package/lib/utils.js
CHANGED
|
@@ -277,6 +277,25 @@ function isDisallowedUriCodepoint(code) {
|
|
|
277
277
|
}
|
|
278
278
|
return false;
|
|
279
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* True when `s` contains any codepoint in the same set `safeAnchorUri`
|
|
282
|
+
* rejects (C0/DEL/C1, NEL/NBSP/LS/PS/BOM, ZWSP, bidi-override controls).
|
|
283
|
+
* Mirrors the Python `has_dangerous_text_codepoints`. Used to scrub the
|
|
284
|
+
* `title` attribute on rendered anchors so an LLM-emitted hover tooltip
|
|
285
|
+
* can't visually impersonate the link via bidi-reorder or zero-width
|
|
286
|
+
* tricks.
|
|
287
|
+
*/
|
|
288
|
+
export function hasDangerousTextCodepoints(s) {
|
|
289
|
+
if (typeof s !== 'string') {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
for (let i = 0; i < s.length; i++) {
|
|
293
|
+
if (isDisallowedUriCodepoint(s.charCodeAt(i))) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
280
299
|
/**
|
|
281
300
|
* Return `uri` if its scheme is in the chat-anchor allowlist, else null.
|
|
282
301
|
* Mirrors the server-side `safe_anchor_uri` check so that anchor parts
|
|
@@ -322,10 +341,11 @@ export function safeAnchorUri(uri) {
|
|
|
322
341
|
* user happens to be in the JupyterLab working directory.
|
|
323
342
|
*/
|
|
324
343
|
export function buildResumeCommand(cwd, sessionId) {
|
|
344
|
+
const quotedSessionId = shellSingleQuote(sessionId);
|
|
325
345
|
if (!cwd) {
|
|
326
|
-
return `claude --resume ${
|
|
346
|
+
return `claude --resume ${quotedSessionId}`;
|
|
327
347
|
}
|
|
328
|
-
return `cd ${shellSingleQuote(cwd)} && claude --resume ${
|
|
348
|
+
return `cd ${shellSingleQuote(cwd)} && claude --resume ${quotedSessionId}`;
|
|
329
349
|
}
|
|
330
350
|
/**
|
|
331
351
|
* Write `text` to the system clipboard. Falls back to a hidden textarea +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plmbr/notebook-intelligence",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.1.0-a.1",
|
|
4
4
|
"description": "AI coding assistant for JupyterLab",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"AI",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"install:extension": "jlpm build",
|
|
50
50
|
"lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
|
|
51
51
|
"lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
|
|
52
|
+
"postinstall": "husky || exit 0",
|
|
52
53
|
"prettier": "jlpm prettier:base --write --list-different",
|
|
53
54
|
"prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
|
|
54
55
|
"prettier:check": "jlpm prettier:base --check",
|
|
@@ -98,8 +99,10 @@
|
|
|
98
99
|
"eslint": "^8.36.0",
|
|
99
100
|
"eslint-config-prettier": "^8.8.0",
|
|
100
101
|
"eslint-plugin-prettier": "^5.0.0",
|
|
102
|
+
"husky": "^9.1.7",
|
|
101
103
|
"jest": "^29.7.0",
|
|
102
104
|
"jest-environment-jsdom": "^29.7.0",
|
|
105
|
+
"lint-staged": "^15.2.10",
|
|
103
106
|
"mkdirp": "^1.0.3",
|
|
104
107
|
"monaco-editor-webpack-plugin": "^2.0.0",
|
|
105
108
|
"npm-run-all": "^4.1.5",
|
|
@@ -238,6 +241,9 @@
|
|
|
238
241
|
}
|
|
239
242
|
]
|
|
240
243
|
},
|
|
244
|
+
"lint-staged": {
|
|
245
|
+
"*": "prettier --write --ignore-unknown"
|
|
246
|
+
},
|
|
241
247
|
"stylelint": {
|
|
242
248
|
"extends": [
|
|
243
249
|
"stylelint-config-recommended",
|
package/src/api.ts
CHANGED
|
@@ -388,6 +388,10 @@ export class NBIConfig {
|
|
|
388
388
|
return this.capabilities.claude_settings;
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
get spinnerVerbs(): { mode: string; verbs: string[] } | null {
|
|
392
|
+
return this.capabilities.spinner_verbs ?? null;
|
|
393
|
+
}
|
|
394
|
+
|
|
391
395
|
get claudeModels(): IClaudeModelInfo[] {
|
|
392
396
|
return (this.capabilities.claude_models ?? []).map(claudeModelFromWire);
|
|
393
397
|
}
|
|
@@ -435,6 +439,10 @@ export class NBIConfig {
|
|
|
435
439
|
return this.capabilities.chat_feedback_enabled === true;
|
|
436
440
|
}
|
|
437
441
|
|
|
442
|
+
get chatFeedbackAlwaysVisible(): boolean {
|
|
443
|
+
return this.capabilities.chat_feedback_always_visible === true;
|
|
444
|
+
}
|
|
445
|
+
|
|
438
446
|
// Admin-supplied tour-copy overrides, served from the capabilities
|
|
439
447
|
// response after server-side validation. Returns the raw dict; the
|
|
440
448
|
// tour module decides how to apply it. Defaults to a shared frozen
|