@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,146 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
VscChevronDown,
|
|
4
|
+
VscChevronRight,
|
|
5
|
+
VscClose,
|
|
6
|
+
VscEdit,
|
|
7
|
+
VscError,
|
|
8
|
+
VscEye,
|
|
9
|
+
VscPassFilled,
|
|
10
|
+
VscSync,
|
|
11
|
+
VscTerminal,
|
|
12
|
+
VscTools
|
|
13
|
+
} from '../icons';
|
|
14
|
+
|
|
15
|
+
export interface IToolCallDiffLine {
|
|
16
|
+
type: 'add' | 'remove' | 'context' | string;
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface IToolCallDiff {
|
|
21
|
+
path: string;
|
|
22
|
+
lines: IToolCallDiffLine[];
|
|
23
|
+
truncated?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A single agent tool call surfaced as a persistent chat card. Mirrors the
|
|
28
|
+
* `ToolCallData` payload emitted by the server: it stays in the transcript
|
|
29
|
+
* after the turn ends and carries its final status, unlike the transient
|
|
30
|
+
* single progress line it replaces. File-edit tools also carry inline diffs.
|
|
31
|
+
*/
|
|
32
|
+
export interface IToolCall {
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
// Coarse category, used only to pick the leading icon.
|
|
36
|
+
kind: 'read' | 'edit' | 'execute' | 'other' | string;
|
|
37
|
+
status: 'in_progress' | 'completed' | 'failed' | 'cancelled' | string;
|
|
38
|
+
diffs?: IToolCallDiff[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const KIND_ICONS: Record<string, React.FC<any>> = {
|
|
42
|
+
read: VscEye,
|
|
43
|
+
edit: VscEdit,
|
|
44
|
+
execute: VscTerminal,
|
|
45
|
+
other: VscTools
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const STATUS_LABELS: Record<string, string> = {
|
|
49
|
+
in_progress: 'in progress',
|
|
50
|
+
completed: 'completed',
|
|
51
|
+
failed: 'failed',
|
|
52
|
+
cancelled: 'cancelled'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const GUTTER: Record<string, string> = { add: '+', remove: '-' };
|
|
56
|
+
|
|
57
|
+
function ToolCallDiffView(props: { diffs: IToolCallDiff[] }): JSX.Element {
|
|
58
|
+
return (
|
|
59
|
+
<div className="nbi-tool-call-diffs">
|
|
60
|
+
{props.diffs.map((diff, i) => (
|
|
61
|
+
<div className="nbi-tool-call-diff" key={i}>
|
|
62
|
+
{diff.path ? (
|
|
63
|
+
<div className="nbi-tool-call-diff-path" title={diff.path}>
|
|
64
|
+
{diff.path}
|
|
65
|
+
</div>
|
|
66
|
+
) : null}
|
|
67
|
+
<div className="nbi-tool-call-diff-body">
|
|
68
|
+
{diff.lines.map((line, j) => (
|
|
69
|
+
<div
|
|
70
|
+
className={`nbi-tool-call-diff-line nbi-diff-${line.type}`}
|
|
71
|
+
key={j}
|
|
72
|
+
>
|
|
73
|
+
<span className="nbi-diff-gutter" aria-hidden="true">
|
|
74
|
+
{GUTTER[line.type] ?? ' '}
|
|
75
|
+
</span>
|
|
76
|
+
{/* The +/- gutter is decorative; give screen readers the
|
|
77
|
+
add/remove distinction as text instead. */}
|
|
78
|
+
{line.type === 'add' || line.type === 'remove' ? (
|
|
79
|
+
<span className="nbi-sr-only">
|
|
80
|
+
{line.type === 'add' ? 'added ' : 'removed '}
|
|
81
|
+
</span>
|
|
82
|
+
) : null}
|
|
83
|
+
<span className="nbi-diff-text">{line.content}</span>
|
|
84
|
+
</div>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
{diff.truncated ? (
|
|
88
|
+
<div className="nbi-tool-call-diff-truncated">diff truncated</div>
|
|
89
|
+
) : null}
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function ToolCallCard(props: { toolCall: IToolCall }): JSX.Element {
|
|
97
|
+
const { title, kind, status, diffs } = props.toolCall;
|
|
98
|
+
const [expanded, setExpanded] = useState(true);
|
|
99
|
+
|
|
100
|
+
const KindIcon = KIND_ICONS[kind] ?? VscTools;
|
|
101
|
+
|
|
102
|
+
let StatusIcon = VscSync;
|
|
103
|
+
if (status === 'completed') {
|
|
104
|
+
StatusIcon = VscPassFilled;
|
|
105
|
+
} else if (status === 'failed') {
|
|
106
|
+
StatusIcon = VscError;
|
|
107
|
+
} else if (status === 'cancelled') {
|
|
108
|
+
StatusIcon = VscClose;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const statusLabel = STATUS_LABELS[status] ?? status;
|
|
112
|
+
const statusModifier = status.replace(/_/g, '-');
|
|
113
|
+
const hasDiffs = Array.isArray(diffs) && diffs.length > 0;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className="nbi-tool-call-wrapper">
|
|
117
|
+
<div className={`nbi-tool-call nbi-tool-call-${statusModifier}`}>
|
|
118
|
+
<KindIcon className="nbi-tool-call-kind-icon" aria-hidden="true" />
|
|
119
|
+
<span className="nbi-tool-call-title" title={title}>
|
|
120
|
+
{title}
|
|
121
|
+
</span>
|
|
122
|
+
{hasDiffs ? (
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
className="nbi-tool-call-diff-toggle"
|
|
126
|
+
onClick={() => setExpanded(e => !e)}
|
|
127
|
+
aria-expanded={expanded}
|
|
128
|
+
aria-label={expanded ? 'Hide diff' : 'Show diff'}
|
|
129
|
+
>
|
|
130
|
+
{expanded ? (
|
|
131
|
+
<VscChevronDown aria-hidden="true" />
|
|
132
|
+
) : (
|
|
133
|
+
<VscChevronRight aria-hidden="true" />
|
|
134
|
+
)}
|
|
135
|
+
</button>
|
|
136
|
+
) : null}
|
|
137
|
+
<StatusIcon className="nbi-tool-call-status-icon" aria-hidden="true" />
|
|
138
|
+
{/* Status reaches screen readers as text; the icons are decorative.
|
|
139
|
+
The visible title is already in the accessibility tree, so this
|
|
140
|
+
span carries only the status to avoid announcing the title twice. */}
|
|
141
|
+
<span className="nbi-sr-only">{statusLabel}</span>
|
|
142
|
+
</div>
|
|
143
|
+
{hasDiffs && expanded ? <ToolCallDiffView diffs={diffs!} /> : null}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { VscChevronDown, VscChevronRight } from '../icons';
|
|
3
|
+
import { IToolCall, ToolCallCard } from './tool-call-card';
|
|
4
|
+
|
|
5
|
+
// Groups with more calls than this start collapsed so a tool-heavy turn (or a
|
|
6
|
+
// reloaded transcript) doesn't flood the chat. A live turn mounts the group at
|
|
7
|
+
// length 1, so it starts expanded and stays visible as calls stream in.
|
|
8
|
+
const GROUP_COLLAPSE_THRESHOLD = 3;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders a run of consecutive tool calls. A single call is shown as a bare
|
|
12
|
+
* card; multiple calls are wrapped in one collapsible group with a summary
|
|
13
|
+
* header, so consecutive agent activity reads as one unit instead of a wall
|
|
14
|
+
* of rows.
|
|
15
|
+
*/
|
|
16
|
+
export function ToolCallGroup(props: { toolCalls: IToolCall[] }): JSX.Element {
|
|
17
|
+
const { toolCalls } = props;
|
|
18
|
+
// Hooks must run unconditionally and in a stable order: the group's length
|
|
19
|
+
// grows as calls stream in, so call useState before any length branch.
|
|
20
|
+
// Start collapsed only for a large group whose calls are all settled --
|
|
21
|
+
// never hide a still-running or failed call (matters on transcript reload,
|
|
22
|
+
// where a group can mount already large).
|
|
23
|
+
const [expanded, setExpanded] = useState(
|
|
24
|
+
() =>
|
|
25
|
+
toolCalls.length <= GROUP_COLLAPSE_THRESHOLD ||
|
|
26
|
+
toolCalls.some(t => t.status === 'in_progress' || t.status === 'failed')
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (toolCalls.length <= 1) {
|
|
30
|
+
return toolCalls.length === 1 ? (
|
|
31
|
+
<ToolCallCard toolCall={toolCalls[0]} />
|
|
32
|
+
) : (
|
|
33
|
+
<></>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const failed = toolCalls.filter(t => t.status === 'failed').length;
|
|
38
|
+
const summary =
|
|
39
|
+
`${toolCalls.length} tool calls` + (failed ? ` (${failed} failed)` : '');
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="nbi-tool-call-group">
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
className="nbi-tool-call-group-header"
|
|
46
|
+
onClick={() => setExpanded(e => !e)}
|
|
47
|
+
aria-expanded={expanded}
|
|
48
|
+
>
|
|
49
|
+
{expanded ? (
|
|
50
|
+
<VscChevronDown aria-hidden="true" />
|
|
51
|
+
) : (
|
|
52
|
+
<VscChevronRight aria-hidden="true" />
|
|
53
|
+
)}
|
|
54
|
+
<span className="nbi-tool-call-group-summary">{summary}</span>
|
|
55
|
+
</button>
|
|
56
|
+
{expanded ? (
|
|
57
|
+
<div className="nbi-tool-call-group-body">
|
|
58
|
+
{toolCalls.map(toolCall => (
|
|
59
|
+
<ToolCallCard key={toolCall.id} toolCall={toolCall} />
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
) : null}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
package/src/icons.ts
CHANGED
|
@@ -41,6 +41,7 @@ export const VscClose = asIcon(Vsc.VscClose);
|
|
|
41
41
|
export const VscCloudUpload = asIcon(Vsc.VscCloudUpload);
|
|
42
42
|
export const VscCopy = asIcon(Vsc.VscCopy);
|
|
43
43
|
export const VscEdit = asIcon(Vsc.VscEdit);
|
|
44
|
+
export const VscError = asIcon(Vsc.VscError);
|
|
44
45
|
export const VscEye = asIcon(Vsc.VscEye);
|
|
45
46
|
export const VscEyeClosed = asIcon(Vsc.VscEyeClosed);
|
|
46
47
|
export const VscFile = asIcon(Vsc.VscFile);
|
|
@@ -55,6 +56,8 @@ export const VscSend = asIcon(Vsc.VscSend);
|
|
|
55
56
|
export const VscSettingsGear = asIcon(Vsc.VscSettingsGear);
|
|
56
57
|
export const VscSparkle = asIcon(Vsc.VscSparkle);
|
|
57
58
|
export const VscStopCircle = asIcon(Vsc.VscStopCircle);
|
|
59
|
+
export const VscSync = asIcon(Vsc.VscSync);
|
|
60
|
+
export const VscTerminal = asIcon(Vsc.VscTerminal);
|
|
58
61
|
export const VscThumbsdown = asIcon(Vsc.VscThumbsdown);
|
|
59
62
|
export const VscThumbsdownFilled = asIcon(Vsc.VscThumbsdownFilled);
|
|
60
63
|
export const VscThumbsup = asIcon(Vsc.VscThumbsup);
|
package/src/index.ts
CHANGED
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
cellOutputHasError,
|
|
107
107
|
chooseWorkspaceDirectory,
|
|
108
108
|
compareSelections,
|
|
109
|
+
buildResumeCommand,
|
|
109
110
|
extractLLMGeneratedCode,
|
|
110
111
|
getSelectionInEditor,
|
|
111
112
|
getTokenCount,
|
|
@@ -1403,10 +1404,9 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
|
|
|
1403
1404
|
return React.createElement(LauncherPicker, {
|
|
1404
1405
|
onSessionSelected: (session: IClaudeSessionInfo) => {
|
|
1405
1406
|
dialog.close();
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
launchCliInTerminal(cmd);
|
|
1407
|
+
launchCliInTerminal(
|
|
1408
|
+
buildResumeCommand(session.cwd ?? '', session.session_id)
|
|
1409
|
+
);
|
|
1410
1410
|
}
|
|
1411
1411
|
});
|
|
1412
1412
|
}
|
|
@@ -1734,7 +1734,7 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
|
|
|
1734
1734
|
source: args.source as string
|
|
1735
1735
|
});
|
|
1736
1736
|
|
|
1737
|
-
return
|
|
1737
|
+
return { cellIndex: newCellIndex };
|
|
1738
1738
|
}
|
|
1739
1739
|
});
|
|
1740
1740
|
|
|
@@ -1755,7 +1755,7 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
|
|
|
1755
1755
|
source: args.source as string
|
|
1756
1756
|
});
|
|
1757
1757
|
|
|
1758
|
-
return
|
|
1758
|
+
return { cellIndex: newCellIndex };
|
|
1759
1759
|
}
|
|
1760
1760
|
});
|
|
1761
1761
|
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
} from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
|
13
13
|
import { VscNewFile, VscInsert, VscCopy, VscNotebook, VscAdd } from './icons';
|
|
14
14
|
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
15
|
+
import { PathExt } from '@jupyterlab/coreutils';
|
|
16
|
+
import { MarkdownLink } from './components/markdown-link';
|
|
15
17
|
import { isDarkTheme, writeTextToClipboard } from './utils';
|
|
16
18
|
import { IActiveDocumentInfo } from './tokens';
|
|
17
19
|
|
|
@@ -29,11 +31,39 @@ export function MarkdownRenderer({
|
|
|
29
31
|
const app = getApp();
|
|
30
32
|
const activeDocumentInfo = getActiveDocumentInfo();
|
|
31
33
|
const isNotebook = activeDocumentInfo.filename.endsWith('.ipynb');
|
|
34
|
+
// Resolve workspace-relative LLM links against the active document's
|
|
35
|
+
// directory so `[file](README.md)` from a chat scoped to
|
|
36
|
+
// `notebooks/proj/work.ipynb` lands at `notebooks/proj/README.md` (the
|
|
37
|
+
// user's mental model) rather than the server-root README.
|
|
38
|
+
const linkBaseDir = activeDocumentInfo.filePath
|
|
39
|
+
? PathExt.dirname(activeDocumentInfo.filePath)
|
|
40
|
+
: '';
|
|
32
41
|
|
|
33
42
|
return (
|
|
43
|
+
// No `rehype-raw` plugin: raw HTML in chat markdown (e.g. an LLM
|
|
44
|
+
// emitting `<a href="javascript:...">`) renders as literal text, not
|
|
45
|
+
// a DOM anchor, so the only anchor sink is the CommonMark/GFM `a`
|
|
46
|
+
// node handled by `SafeAnchor` below. Any future change that enables
|
|
47
|
+
// raw HTML needs to add a rehype-sanitize pass alongside.
|
|
34
48
|
<Markdown
|
|
35
49
|
remarkPlugins={[remarkGfm]}
|
|
36
50
|
components={{
|
|
51
|
+
// CommonMark `<https://...>` autolinks, `[text](url)`, and
|
|
52
|
+
// reference-style links all normalize to the same `a` node.
|
|
53
|
+
// `MarkdownLink` routes fragment-only and workspace-relative
|
|
54
|
+
// hrefs through Lab's docmanager so an LLM-emitted link can't
|
|
55
|
+
// replace the JupyterLab shell, and hands everything else to
|
|
56
|
+
// SafeAnchor for the `_blank` + scheme-allowlist treatment.
|
|
57
|
+
a: ({ href, title, children }: any) => (
|
|
58
|
+
<MarkdownLink
|
|
59
|
+
app={app}
|
|
60
|
+
baseDir={linkBaseDir}
|
|
61
|
+
href={href}
|
|
62
|
+
title={title}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
</MarkdownLink>
|
|
66
|
+
),
|
|
37
67
|
code({ node, inline, className, children, getApp, ...props }: any) {
|
|
38
68
|
const match = /language-(\w+)/.exec(className || '');
|
|
39
69
|
const codeString = String(children).replace(/\n$/, '');
|
package/src/tokens.ts
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { UUID } from '@lumino/coreutils';
|
|
2
|
+
import { ResponseStreamDataType } from './tokens';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The subset of a chat message's stream-content item this helper needs. A
|
|
6
|
+
* structural subset of `IChatMessageContent` (defined in chat-sidebar) so it
|
|
7
|
+
* can be unit-tested without importing the sidebar (and creating a cycle).
|
|
8
|
+
*/
|
|
9
|
+
export interface IToolCallStreamItem {
|
|
10
|
+
id: string;
|
|
11
|
+
type: ResponseStreamDataType;
|
|
12
|
+
content: any;
|
|
13
|
+
created: Date;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Merge a streamed tool-call payload into `contents` by its tool-call id.
|
|
18
|
+
*
|
|
19
|
+
* A tool call streams twice under one id (once when it starts, once when it
|
|
20
|
+
* finishes). The first emission pushes a new card; the second updates that
|
|
21
|
+
* card's content (its status) in place, so the call stays a single persistent
|
|
22
|
+
* row rather than appending a duplicate. Mutates `contents`.
|
|
23
|
+
*/
|
|
24
|
+
export function upsertToolCallContent(
|
|
25
|
+
contents: IToolCallStreamItem[],
|
|
26
|
+
content: { id: string; [key: string]: any },
|
|
27
|
+
created: Date
|
|
28
|
+
): void {
|
|
29
|
+
const existing = contents.find(
|
|
30
|
+
c =>
|
|
31
|
+
c.type === ResponseStreamDataType.ToolCall &&
|
|
32
|
+
c.content?.id === content?.id
|
|
33
|
+
);
|
|
34
|
+
if (existing) {
|
|
35
|
+
existing.content = content;
|
|
36
|
+
} else {
|
|
37
|
+
contents.push({
|
|
38
|
+
id: UUID.uuid4(),
|
|
39
|
+
type: ResponseStreamDataType.ToolCall,
|
|
40
|
+
content,
|
|
41
|
+
created
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -339,6 +339,28 @@ function isDisallowedUriCodepoint(code: number): boolean {
|
|
|
339
339
|
return false;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
/**
|
|
343
|
+
* True when `s` contains any codepoint in the same set `safeAnchorUri`
|
|
344
|
+
* rejects (C0/DEL/C1, NEL/NBSP/LS/PS/BOM, ZWSP, bidi-override controls).
|
|
345
|
+
* Mirrors the Python `has_dangerous_text_codepoints`. Used to scrub the
|
|
346
|
+
* `title` attribute on rendered anchors so an LLM-emitted hover tooltip
|
|
347
|
+
* can't visually impersonate the link via bidi-reorder or zero-width
|
|
348
|
+
* tricks.
|
|
349
|
+
*/
|
|
350
|
+
export function hasDangerousTextCodepoints(
|
|
351
|
+
s: string | undefined | null
|
|
352
|
+
): boolean {
|
|
353
|
+
if (typeof s !== 'string') {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
for (let i = 0; i < s.length; i++) {
|
|
357
|
+
if (isDisallowedUriCodepoint(s.charCodeAt(i))) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
342
364
|
/**
|
|
343
365
|
* Return `uri` if its scheme is in the chat-anchor allowlist, else null.
|
|
344
366
|
* Mirrors the server-side `safe_anchor_uri` check so that anchor parts
|
|
@@ -385,10 +407,11 @@ export function safeAnchorUri(uri: string | undefined | null): string | null {
|
|
|
385
407
|
* user happens to be in the JupyterLab working directory.
|
|
386
408
|
*/
|
|
387
409
|
export function buildResumeCommand(cwd: string, sessionId: string): string {
|
|
410
|
+
const quotedSessionId = shellSingleQuote(sessionId);
|
|
388
411
|
if (!cwd) {
|
|
389
|
-
return `claude --resume ${
|
|
412
|
+
return `claude --resume ${quotedSessionId}`;
|
|
390
413
|
}
|
|
391
|
-
return `cd ${shellSingleQuote(cwd)} && claude --resume ${
|
|
414
|
+
return `cd ${shellSingleQuote(cwd)} && claude --resume ${quotedSessionId}`;
|
|
392
415
|
}
|
|
393
416
|
|
|
394
417
|
/**
|
package/style/base.css
CHANGED
|
@@ -643,6 +643,7 @@ pre:has(.code-block-header) {
|
|
|
643
643
|
|
|
644
644
|
.chat-message-feedback:focus-within,
|
|
645
645
|
.chat-message-feedback:has(.selected),
|
|
646
|
+
.chat-message-feedback.always-visible,
|
|
646
647
|
.chat-message:hover .chat-message-feedback {
|
|
647
648
|
opacity: 1;
|
|
648
649
|
}
|
|
@@ -779,6 +780,169 @@ pre:has(.code-block-header) {
|
|
|
779
780
|
margin-top: 2px;
|
|
780
781
|
}
|
|
781
782
|
|
|
783
|
+
.nbi-tool-call-wrapper {
|
|
784
|
+
margin: 2px 0;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.nbi-tool-call {
|
|
788
|
+
display: flex;
|
|
789
|
+
align-items: center;
|
|
790
|
+
gap: 6px;
|
|
791
|
+
padding: 3px 6px;
|
|
792
|
+
border-radius: 4px;
|
|
793
|
+
background-color: var(--jp-layout-color2);
|
|
794
|
+
font-size: var(--jp-ui-font-size1);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.nbi-tool-call-kind-icon {
|
|
798
|
+
flex-shrink: 0;
|
|
799
|
+
color: var(--jp-ui-font-color2);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.nbi-tool-call-title {
|
|
803
|
+
flex: 1;
|
|
804
|
+
min-width: 0;
|
|
805
|
+
overflow: hidden;
|
|
806
|
+
white-space: nowrap;
|
|
807
|
+
text-overflow: ellipsis;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.nbi-tool-call-status-icon {
|
|
811
|
+
flex-shrink: 0;
|
|
812
|
+
color: var(--jp-ui-font-color2);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.nbi-tool-call-completed .nbi-tool-call-status-icon {
|
|
816
|
+
color: var(--jp-success-color1);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
.nbi-tool-call-failed .nbi-tool-call-status-icon {
|
|
820
|
+
color: var(--jp-error-color1);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.nbi-tool-call-in-progress .nbi-tool-call-status-icon {
|
|
824
|
+
animation: nbi-tool-call-spin 1s linear infinite;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
@keyframes nbi-tool-call-spin {
|
|
828
|
+
from {
|
|
829
|
+
transform: rotate(0deg);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
to {
|
|
833
|
+
transform: rotate(360deg);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
@media (prefers-reduced-motion: reduce) {
|
|
838
|
+
.nbi-tool-call-in-progress .nbi-tool-call-status-icon {
|
|
839
|
+
animation: none;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.nbi-tool-call-diff-toggle {
|
|
844
|
+
display: flex;
|
|
845
|
+
flex-shrink: 0;
|
|
846
|
+
align-items: center;
|
|
847
|
+
padding: 0;
|
|
848
|
+
border: none;
|
|
849
|
+
background: none;
|
|
850
|
+
color: var(--jp-ui-font-color2);
|
|
851
|
+
cursor: pointer;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.nbi-tool-call-diffs {
|
|
855
|
+
margin: 2px 0 2px 18px;
|
|
856
|
+
padding-left: 6px;
|
|
857
|
+
border-left: 2px solid var(--jp-border-color2);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.nbi-tool-call-diff + .nbi-tool-call-diff {
|
|
861
|
+
margin-top: 4px;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.nbi-tool-call-diff-path {
|
|
865
|
+
overflow: hidden;
|
|
866
|
+
font-size: var(--jp-ui-font-size0);
|
|
867
|
+
color: var(--jp-ui-font-color2);
|
|
868
|
+
white-space: nowrap;
|
|
869
|
+
text-overflow: ellipsis;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.nbi-tool-call-diff-body {
|
|
873
|
+
margin: 2px 0;
|
|
874
|
+
overflow-x: auto;
|
|
875
|
+
font-family: var(--jp-code-font-family);
|
|
876
|
+
font-size: var(--jp-code-font-size);
|
|
877
|
+
line-height: 1.4;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.nbi-tool-call-diff-line {
|
|
881
|
+
display: flex;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.nbi-diff-gutter {
|
|
885
|
+
flex-shrink: 0;
|
|
886
|
+
width: 1ch;
|
|
887
|
+
user-select: none;
|
|
888
|
+
text-align: center;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.nbi-diff-text {
|
|
892
|
+
white-space: pre-wrap;
|
|
893
|
+
word-break: break-word;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/* Semi-transparent tints rather than the solid --jp-*-color3 fills (which are
|
|
897
|
+
light pastels in BOTH themes): the tint sits over the card background so the
|
|
898
|
+
line stays readable with the theme's own (light-on-dark / dark-on-light)
|
|
899
|
+
text color instead of forcing dark text onto a pale fill. */
|
|
900
|
+
.nbi-diff-add {
|
|
901
|
+
background-color: rgb(64 160 43 / 22%);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.nbi-diff-remove {
|
|
905
|
+
background-color: rgb(248 81 73 / 22%);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.nbi-diff-context {
|
|
909
|
+
color: var(--jp-ui-font-color2);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.nbi-tool-call-diff-truncated {
|
|
913
|
+
font-size: var(--jp-ui-font-size0);
|
|
914
|
+
font-style: italic;
|
|
915
|
+
color: var(--jp-ui-font-color2);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.nbi-tool-call-group {
|
|
919
|
+
margin: 2px 0;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
.nbi-tool-call-group-header {
|
|
923
|
+
display: flex;
|
|
924
|
+
align-items: center;
|
|
925
|
+
gap: 4px;
|
|
926
|
+
width: 100%;
|
|
927
|
+
padding: 2px 4px;
|
|
928
|
+
border: none;
|
|
929
|
+
background: none;
|
|
930
|
+
color: var(--jp-ui-font-color2);
|
|
931
|
+
font-size: var(--jp-ui-font-size0);
|
|
932
|
+
text-align: left;
|
|
933
|
+
cursor: pointer;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
.nbi-tool-call-group-summary {
|
|
937
|
+
overflow: hidden;
|
|
938
|
+
white-space: nowrap;
|
|
939
|
+
text-overflow: ellipsis;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.nbi-tool-call-group-body {
|
|
943
|
+
margin-left: 6px;
|
|
944
|
+
}
|
|
945
|
+
|
|
782
946
|
.user-input-autocomplete,
|
|
783
947
|
.mode-tools-popover,
|
|
784
948
|
.workspace-file-popover {
|
|
@@ -1503,12 +1667,17 @@ button.send-button:disabled:active:not(.send-button-stop) {
|
|
|
1503
1667
|
.expandable-content-text {
|
|
1504
1668
|
display: none;
|
|
1505
1669
|
margin: 5px;
|
|
1506
|
-
padding:
|
|
1507
|
-
border:
|
|
1508
|
-
border-radius: 5px;
|
|
1670
|
+
padding: 8px 10px;
|
|
1671
|
+
border-radius: 6px;
|
|
1509
1672
|
max-height: 200px;
|
|
1510
1673
|
overflow-y: auto;
|
|
1511
|
-
|
|
1674
|
+
background-color: var(--jp-layout-color2);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/* Inline code also fills with --jp-layout-color2, which now matches this box's
|
|
1678
|
+
fill; recess nested code to --jp-layout-color1 so it stays distinct. */
|
|
1679
|
+
.expandable-content-text :not(pre) > code {
|
|
1680
|
+
background-color: var(--jp-layout-color1);
|
|
1512
1681
|
}
|
|
1513
1682
|
|
|
1514
1683
|
.expandable-content .collapsed-icon {
|