@parallelworks/ui 0.26.0 → 0.28.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/dist/ai/adapter/types.d.ts +5 -0
- package/dist/ai/ai.css +197 -21
- package/dist/ai/components/AllocationSelector.js +2 -2
- package/dist/ai/components/BranchNavigator.js +6 -6
- package/dist/ai/components/ChatEmptyState.js +39 -36
- package/dist/ai/components/ChatInput.js +67 -60
- package/dist/ai/components/ChatMessage.js +21 -20
- package/dist/ai/components/ChatMessageList.d.ts +7 -2
- package/dist/ai/components/ChatMessageList.js +65 -54
- package/dist/ai/components/ChatSidebar.js +6 -6
- package/dist/ai/components/ChatThread.js +3 -63
- package/dist/ai/components/ModelSelector.js +7 -7
- package/dist/ai/components/PastedText.d.ts +3 -1
- package/dist/ai/components/PastedText.js +21 -6
- package/dist/ai/components/Reasoning.d.ts +17 -0
- package/dist/ai/components/Reasoning.js +29 -0
- package/dist/ai/components/agent/AgentMessageParts.js +32 -22
- package/dist/ai/core/ChatProvider.js +3 -1
- package/dist/ai/core/parts.js +20 -0
- package/dist/ai/core/pasteLanguage.d.ts +6 -0
- package/dist/ai/core/pasteLanguage.js +45 -0
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/index.js +2 -2
- package/dist/ai/strings.d.ts +9 -0
- package/dist/ai/strings.js +7 -1
- package/dist/ai/ui/Notice.d.ts +8 -0
- package/dist/ai/ui/Notice.js +27 -1
- package/dist/ai/ui/sidebar.js +3 -3
- package/dist/file-explorer/FileExplorer.js +34 -11
- package/dist/file-explorer/FilePreview/previewCache.d.ts +1 -1
- package/dist/file-explorer/FilePreview/previewCache.js +6 -4
- package/dist/file-explorer/lib/types.d.ts +1 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,8 @@ import { FiX } from "../../icons/fi/FiX.js";
|
|
|
3
3
|
import { ImSpinner5 } from "../../icons/im/ImSpinner5.js";
|
|
4
4
|
import { useChatConfig } from "../core/config.js";
|
|
5
5
|
import { formatFileSize } from "../utils.js";
|
|
6
|
+
import Markdown from "../ui/Markdown.js";
|
|
7
|
+
import { fencePaste } from "../core/pasteLanguage.js";
|
|
6
8
|
import { splitPastes } from "../core/pastes.js";
|
|
7
9
|
import cx from "classnames";
|
|
8
10
|
import { Fragment } from "react";
|
|
@@ -14,7 +16,7 @@ function PasteFileCard({ id, lines, bytes, saving = false, error, onRemove }) {
|
|
|
14
16
|
const detail = error ?? (saving ? t.saving : `${t.lines(lines)} · ${formatFileSize(bytes)}`);
|
|
15
17
|
return /* @__PURE__ */ jsxs("div", {
|
|
16
18
|
"data-testid": "paste-file-card",
|
|
17
|
-
className: cx("inline-flex max-w-full items-center gap-2.5 rounded-xl
|
|
19
|
+
className: cx("inline-flex max-w-full items-center gap-2.5 rounded-xl px-3 py-2 align-top whitespace-normal", "bg-(--theme-panel-bg)", error ? "border border-red-500" : "chat-hairline"),
|
|
18
20
|
children: [
|
|
19
21
|
/* @__PURE__ */ jsx(FiFile, { className: "h-5 w-5 flex-shrink-0 theme-muted-text" }),
|
|
20
22
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -39,14 +41,27 @@ function PasteFileCard({ id, lines, bytes, saving = false, error, onRemove }) {
|
|
|
39
41
|
]
|
|
40
42
|
});
|
|
41
43
|
}
|
|
42
|
-
/** A user message as typed: an inline paste as
|
|
44
|
+
/** A user message as typed: an inline paste as a code block in the language
|
|
45
|
+
* it reads as, one sent as its file as a card. The blank lines the composer puts around a
|
|
46
|
+
* paste would stack on the block's own spacing, so they are dropped. */
|
|
43
47
|
function UserMessageText({ content, pastes }) {
|
|
44
48
|
if (!pastes?.length) return /* @__PURE__ */ jsx(Fragment$1, { children: content });
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
const segments = splitPastes(content, pastes);
|
|
50
|
+
const shows = (seg) => typeof seg !== "string" || seg.trim() !== "";
|
|
51
|
+
const blockSpacing = (i) => cx(segments.slice(0, i).some(shows) ? "mt-2.5" : "mt-0", segments.slice(i + 1).some(shows) ? "mb-2.5" : "mb-0");
|
|
52
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: segments.map((segment, i) => {
|
|
53
|
+
if (typeof segment === "string") {
|
|
54
|
+
let text = segment;
|
|
55
|
+
if (i > 0) text = text.replace(/^\n+/, "");
|
|
56
|
+
if (i < segments.length - 1) text = text.replace(/\n+$/, "");
|
|
57
|
+
return text ? /* @__PURE__ */ jsx(Fragment, { children: text }, i) : null;
|
|
58
|
+
}
|
|
59
|
+
if (segment.inline && segment.text !== void 0) return /* @__PURE__ */ jsx("span", {
|
|
60
|
+
className: cx("chat-paste-code block", blockSpacing(i)),
|
|
61
|
+
children: /* @__PURE__ */ jsx(Markdown, { children: fencePaste(segment.text) })
|
|
62
|
+
}, i);
|
|
48
63
|
return /* @__PURE__ */ jsx("span", {
|
|
49
|
-
className: "
|
|
64
|
+
className: cx("flex [&>*]:flex-1", blockSpacing(i)),
|
|
50
65
|
children: /* @__PURE__ */ jsx(PasteFileCard, {
|
|
51
66
|
id: segment.id,
|
|
52
67
|
lines: segment.lines,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/** The line a turn's reasoning folds under. It opens the reasoning in place,
|
|
3
|
+
* above the answer it led to, rather than in a panel the eye has to cross
|
|
4
|
+
* the page to read. */
|
|
5
|
+
export declare function ReasoningToggle({ open, onToggle, shimmer, children, }: {
|
|
6
|
+
open: boolean;
|
|
7
|
+
onToggle: () => void;
|
|
8
|
+
/** While the model is still thinking the label shimmers. */
|
|
9
|
+
shimmer?: boolean;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
/** The reasoning itself: smaller and quieter than the answer, hung off a
|
|
13
|
+
* hairline of the accent so it reads as the assistant's aside. */
|
|
14
|
+
export declare function ReasoningBody({ className, children, }: {
|
|
15
|
+
className?: string;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { LuChevronRight } from "../../icons/lu/LuChevronRight.js";
|
|
2
|
+
import cx from "classnames";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/ai/components/Reasoning.tsx
|
|
5
|
+
/** The line a turn's reasoning folds under. It opens the reasoning in place,
|
|
6
|
+
* above the answer it led to, rather than in a panel the eye has to cross
|
|
7
|
+
* the page to read. */
|
|
8
|
+
function ReasoningToggle({ open, onToggle, shimmer = false, children }) {
|
|
9
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
10
|
+
type: "button",
|
|
11
|
+
onClick: onToggle,
|
|
12
|
+
"aria-expanded": open,
|
|
13
|
+
className: "group/reasoning -ml-1 flex items-center gap-x-1 rounded-md px-1 py-0.5 text-sm theme-muted-text transition-colors hover:theme-text",
|
|
14
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
15
|
+
className: cx(shimmer && "animate-shimmer text-shimmer"),
|
|
16
|
+
children
|
|
17
|
+
}), /* @__PURE__ */ jsx(LuChevronRight, { className: cx("h-3.5 w-3.5 transition-transform duration-200 motion-reduce:transition-none", open && "rotate-90") })]
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** The reasoning itself: smaller and quieter than the answer, hung off a
|
|
21
|
+
* hairline of the accent so it reads as the assistant's aside. */
|
|
22
|
+
function ReasoningBody({ className, children }) {
|
|
23
|
+
return /* @__PURE__ */ jsx("div", {
|
|
24
|
+
className: cx("chat-reasoning-body ml-1 mt-1.5 mb-3 pl-4 text-sm leading-relaxed theme-muted-text", className),
|
|
25
|
+
children
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { ReasoningBody, ReasoningToggle };
|
|
@@ -10,6 +10,7 @@ import { thinkingHeading } from "../../agent/transcript.js";
|
|
|
10
10
|
import { useChatConfig } from "../../core/config.js";
|
|
11
11
|
import Markdown from "../../ui/Markdown.js";
|
|
12
12
|
import { chatProseClasses } from "../../ui/prose.js";
|
|
13
|
+
import { ReasoningBody, ReasoningToggle } from "../Reasoning.js";
|
|
13
14
|
import cx from "classnames";
|
|
14
15
|
import { useEffect, useMemo, useState } from "react";
|
|
15
16
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -520,41 +521,50 @@ function formatThinkingDuration(ms) {
|
|
|
520
521
|
if (s < 60) return `${s}s`;
|
|
521
522
|
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
522
523
|
}
|
|
523
|
-
/** The TUI's thought
|
|
524
|
-
*
|
|
525
|
-
*
|
|
524
|
+
/** The TUI's thought, drawn like a chat reply's: while it streams its label
|
|
525
|
+
* shimmers over the latest lines, then it folds to the label and opens in
|
|
526
|
+
* place on request. */
|
|
526
527
|
function ReasoningBlock({ part, isStreaming }) {
|
|
527
528
|
const { strings } = useChatConfig();
|
|
528
529
|
const t = strings.thinking;
|
|
529
530
|
const [expanded, setExpanded] = useState(false);
|
|
530
531
|
const label = thinkingHeading(part.text) || (part.durationMs ? t.thoughtFor(formatThinkingDuration(part.durationMs)) : t.label);
|
|
531
532
|
const hasBody = part.text.trim() !== "";
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
children: /* @__PURE__ */ jsx(Markdown, {
|
|
536
|
-
isStreaming: isStreaming ?? false,
|
|
537
|
-
children: part.text
|
|
538
|
-
})
|
|
533
|
+
const markdown = /* @__PURE__ */ jsx(Markdown, {
|
|
534
|
+
isStreaming: isStreaming ?? false,
|
|
535
|
+
children: part.text
|
|
539
536
|
});
|
|
540
|
-
if (isStreaming) return /* @__PURE__ */
|
|
537
|
+
if (isStreaming) return /* @__PURE__ */ jsxs("div", {
|
|
541
538
|
className: "py-1.5",
|
|
542
539
|
"data-testid": "reasoning-part",
|
|
543
|
-
children:
|
|
540
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
541
|
+
className: "animate-shimmer text-shimmer text-sm",
|
|
542
|
+
children: label
|
|
543
|
+
}), hasBody && /* @__PURE__ */ jsx(ReasoningBody, {
|
|
544
|
+
className: "chat-reasoning-tail",
|
|
545
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
546
|
+
className: "flex max-h-20 flex-col justify-end overflow-hidden",
|
|
547
|
+
"data-testid": "reasoning-body",
|
|
548
|
+
children: markdown
|
|
549
|
+
})
|
|
550
|
+
})]
|
|
551
|
+
});
|
|
552
|
+
if (!hasBody) return /* @__PURE__ */ jsx("div", {
|
|
553
|
+
className: "py-1.5 text-sm theme-muted-text",
|
|
554
|
+
"data-testid": "reasoning-part",
|
|
555
|
+
children: label
|
|
544
556
|
});
|
|
545
557
|
return /* @__PURE__ */ jsxs("div", {
|
|
546
558
|
className: "py-1.5",
|
|
547
559
|
"data-testid": "reasoning-part",
|
|
548
|
-
children: [/* @__PURE__ */
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
children: body
|
|
557
|
-
})]
|
|
560
|
+
children: [/* @__PURE__ */ jsx(ReasoningToggle, {
|
|
561
|
+
open: expanded,
|
|
562
|
+
onToggle: () => setExpanded((v) => !v),
|
|
563
|
+
children: label
|
|
564
|
+
}), expanded && /* @__PURE__ */ jsx(ReasoningBody, { children: /* @__PURE__ */ jsx("div", {
|
|
565
|
+
"data-testid": "reasoning-body",
|
|
566
|
+
children: markdown
|
|
567
|
+
}) })]
|
|
558
568
|
});
|
|
559
569
|
}
|
|
560
570
|
function TextBlock({ part, isStreaming }) {
|
|
@@ -441,6 +441,7 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
441
441
|
try {
|
|
442
442
|
const useResponses = state.models.find((m) => m.id === modelString)?.supports_responses === true;
|
|
443
443
|
let thinkingStart = null;
|
|
444
|
+
let thinkingEnd = null;
|
|
444
445
|
const markStreamStart = () => {
|
|
445
446
|
if (thinkingStart === null) thinkingStart = Date.now();
|
|
446
447
|
};
|
|
@@ -460,6 +461,7 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
460
461
|
},
|
|
461
462
|
onReasoning: (reasoning) => {
|
|
462
463
|
markStreamStart();
|
|
464
|
+
thinkingEnd = Date.now();
|
|
463
465
|
dispatch({
|
|
464
466
|
type: "APPEND_STREAMING_REASONING",
|
|
465
467
|
reasoning,
|
|
@@ -485,7 +487,7 @@ function ChatProvider({ adapter, currentUser, navigation, notify, activeConversa
|
|
|
485
487
|
if (bgStream) bgStream.parts = liveParts;
|
|
486
488
|
}
|
|
487
489
|
}, controller.signal);
|
|
488
|
-
const thinkingDuration = assistantReasoning && thinkingStart !== null ? Date.now() - thinkingStart : null;
|
|
490
|
+
const thinkingDuration = assistantReasoning && thinkingStart !== null ? (thinkingEnd ?? Date.now()) - thinkingStart : null;
|
|
489
491
|
const assistantMessage = {
|
|
490
492
|
id: assistantMessageId || crypto.randomUUID(),
|
|
491
493
|
parentId: userMessageId,
|
package/dist/ai/core/parts.js
CHANGED
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
* rather than invented — an end without a start has nothing to render. */
|
|
6
6
|
function applyPartDelta(parts, delta) {
|
|
7
7
|
switch (delta.type) {
|
|
8
|
+
case "text":
|
|
9
|
+
case "reasoning": {
|
|
10
|
+
const last = parts.at(-1);
|
|
11
|
+
if (last?.kind === delta.type) return [...parts.slice(0, -1), {
|
|
12
|
+
...last,
|
|
13
|
+
text: last.text + delta.text
|
|
14
|
+
}];
|
|
15
|
+
return [...parts, {
|
|
16
|
+
kind: delta.type,
|
|
17
|
+
text: delta.text
|
|
18
|
+
}];
|
|
19
|
+
}
|
|
20
|
+
case "retract": {
|
|
21
|
+
let end = parts.length;
|
|
22
|
+
while (end > 0 && isStreamedText(parts[end - 1])) end -= 1;
|
|
23
|
+
return end === parts.length ? parts : parts.slice(0, end);
|
|
24
|
+
}
|
|
8
25
|
case "tool_start": return [...parts, {
|
|
9
26
|
kind: "tool_call",
|
|
10
27
|
id: delta.id,
|
|
@@ -70,6 +87,9 @@ function applyPartDelta(parts, delta) {
|
|
|
70
87
|
default: return parts;
|
|
71
88
|
}
|
|
72
89
|
}
|
|
90
|
+
function isStreamedText(part) {
|
|
91
|
+
return part?.kind === "text" || part?.kind === "reasoning";
|
|
92
|
+
}
|
|
73
93
|
/** A turn can end (completed or stopped) with tool calls still marked
|
|
74
94
|
* running — the transport died before their tool_end arrived. A stored
|
|
75
95
|
* message must not spin forever, so close them out as interrupted. */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The highlighter's name for the language a paste is written in, or ''
|
|
2
|
+
* when it is not recognisably one. */
|
|
3
|
+
export declare function guessPasteLanguage(text: string): string;
|
|
4
|
+
/** The paste as a fenced code block in its language. The fence outruns any
|
|
5
|
+
* run of backticks inside the paste, so nothing in it can close the fence. */
|
|
6
|
+
export declare function fencePaste(text: string): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/ai/core/pasteLanguage.ts
|
|
2
|
+
function meaningfulLines(text) {
|
|
3
|
+
return text.split("\n").filter((l) => l.trim() !== "" && !/^\s*#/.test(l));
|
|
4
|
+
}
|
|
5
|
+
function mostly(lines, test) {
|
|
6
|
+
return lines.length > 0 && lines.filter(test).length / lines.length >= .8;
|
|
7
|
+
}
|
|
8
|
+
function isJson(trimmed) {
|
|
9
|
+
if (!/^[[{]/.test(trimmed)) return false;
|
|
10
|
+
try {
|
|
11
|
+
JSON.parse(trimmed);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
var YAML_LINE = /^\s*(-\s+)?["']?[\w./-]+["']?\s*:(\s|$)|^\s*-(\s|$)|^---\s*$/;
|
|
18
|
+
var TOML_LINE = /^\s*\[{1,2}[\w."-]+\]{1,2}\s*$|^\s*[\w."-]+\s*=\s*\S/;
|
|
19
|
+
/** The highlighter's name for the language a paste is written in, or ''
|
|
20
|
+
* when it is not recognisably one. */
|
|
21
|
+
function guessPasteLanguage(text) {
|
|
22
|
+
const trimmed = text.trim();
|
|
23
|
+
if (!trimmed) return "";
|
|
24
|
+
if (isJson(trimmed)) return "json";
|
|
25
|
+
const all = trimmed.split("\n");
|
|
26
|
+
if (all.some((l) => /^(diff --git |@@ -\d)/.test(l))) return "diff";
|
|
27
|
+
if (/^<(!doctype html|html)\b/i.test(trimmed)) return "html";
|
|
28
|
+
if (/^<[?!\w]/.test(trimmed) && trimmed.endsWith(">")) return "xml";
|
|
29
|
+
const first = all[0] ?? "";
|
|
30
|
+
if (/^#!.*\b(ba|z)?sh\b/.test(first) || mostly(meaningfulLines(trimmed), (l) => /^\$ /.test(l))) return "bash";
|
|
31
|
+
if (/^(select|insert|update|delete|create|alter|with)\s/i.test(first)) return "sql";
|
|
32
|
+
const lines = meaningfulLines(trimmed);
|
|
33
|
+
if (lines.length >= 2 && mostly(lines, (l) => YAML_LINE.test(l))) return "yaml";
|
|
34
|
+
if (lines.length >= 2 && mostly(lines, (l) => TOML_LINE.test(l))) return "toml";
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
/** The paste as a fenced code block in its language. The fence outruns any
|
|
38
|
+
* run of backticks inside the paste, so nothing in it can close the fence. */
|
|
39
|
+
function fencePaste(text) {
|
|
40
|
+
const longest = Math.max(2, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
|
|
41
|
+
const fence = "`".repeat(longest + 1);
|
|
42
|
+
return `${fence}${guessPasteLanguage(text)}\n${text.replace(/\n+$/, "")}\n${fence}`;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { fencePaste, guessPasteLanguage };
|
package/dist/ai/index.d.ts
CHANGED
|
@@ -39,6 +39,6 @@ export { useKeyboardShortcuts } from './core/useKeyboardShortcuts.js';
|
|
|
39
39
|
export { type ChatStrings, defaultChatStrings } from './strings.js';
|
|
40
40
|
export * from './types.js';
|
|
41
41
|
export { type DropdownOption, default as Dropdown } from './ui/Dropdown.js';
|
|
42
|
-
export { NoticeBar, NoticeCard, type NoticeTone } from './ui/Notice.js';
|
|
42
|
+
export { NoticeBar, NoticeCard, type NoticeTone, OutputCard } from './ui/Notice.js';
|
|
43
43
|
export { RenameDialog, type RowDialogStrings, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarToggle, useRowDialogs, } from './ui/sidebar.js';
|
|
44
44
|
export { formatFileSize, isMac, isOrgModel } from './utils.js';
|
package/dist/ai/index.js
CHANGED
|
@@ -43,5 +43,5 @@ import ChatMessage_default from "./components/ChatMessage.js";
|
|
|
43
43
|
import ChatMessageList from "./components/ChatMessageList.js";
|
|
44
44
|
import ChatThread from "./components/ChatThread.js";
|
|
45
45
|
import ComposerFrame from "./components/ComposerFrame.js";
|
|
46
|
-
import { NoticeBar, NoticeCard } from "./ui/Notice.js";
|
|
47
|
-
export { AGENT_COLOR_NAMES, ActivityLine, AgentMessageParts, AllocationSelector, ApprovalBlock, AttachmentManager, AttachmentUpload, BlockedGroupBanner, BranchNavigator, ChatAdapterError, ChatEmptyState, ChatInput, ChatLayout, ChatMessageList, ChatMessage_default as ChatMessageView, ChatProvider, ChatSidebar, ChatThread, ComposerContext, ComposerControls, ComposerFrame, ComposerSettings, ConnectToolsLink, DEFAULT_PERMISSION_MODE, DragOverlay, Dropdown, FOCUS_SIDEBAR_SEARCH_EVENT, INTERRUPTED_NOTICE, KeyboardShortcutsProvider, MAX_EDIT_DIFF_LINES, MAX_WRITE_DIFF_LINES, ModelSelector, NAMED_PERMISSION_MODES, NoticeBar, NoticeCard, PermissionPicker, ProviderIssueBanner, RenameDialog, ShareButton, ShareDialog, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarToggle, agentColorHex, agentTypeLabel, applyPartDelta, askQuestionFromArgs, bashExitCode, chatReducer, clampPermissionMode, classifyPseudoUserMessage, clip, compactCount, computeEditDiff, computeWriteRows, defaultChatStrings, durationToAbsHumanDuration, editSummary, finalizeParts, firstNonEmptyLine, formatElapsed, formatFileSize, fullToolArgs, getGreeting, getProviderKeyFromModelId, hiddenTranscriptTool, initialState, interruptionNotice, isEditDiffTool, isGroupableTool, isMac, isNamedPermissionMode, isOrgModel, languageForPath, lineDelta, parseEditArgs, parseSubagentNotice, pasteInlineMaxBytes, permissionModesUpTo, planFromArgs, plural, providerIssueFor, resolveChatConfig, resultFailed, rollupSentence, sanitizeLabel, sanitizePanelText, slashMatches, subagentDeliveryLine, summarizeToolArgs, taskCallFromArgs, thinkingHeading, tokenizeDiffLines, toolHeaderFor, toolSummary, useChat, useChatConfig, useDragDrop, useKeyboardShortcuts, useRowDialogs, wordEmphasisRanges };
|
|
46
|
+
import { NoticeBar, NoticeCard, OutputCard } from "./ui/Notice.js";
|
|
47
|
+
export { AGENT_COLOR_NAMES, ActivityLine, AgentMessageParts, AllocationSelector, ApprovalBlock, AttachmentManager, AttachmentUpload, BlockedGroupBanner, BranchNavigator, ChatAdapterError, ChatEmptyState, ChatInput, ChatLayout, ChatMessageList, ChatMessage_default as ChatMessageView, ChatProvider, ChatSidebar, ChatThread, ComposerContext, ComposerControls, ComposerFrame, ComposerSettings, ConnectToolsLink, DEFAULT_PERMISSION_MODE, DragOverlay, Dropdown, FOCUS_SIDEBAR_SEARCH_EVENT, INTERRUPTED_NOTICE, KeyboardShortcutsProvider, MAX_EDIT_DIFF_LINES, MAX_WRITE_DIFF_LINES, ModelSelector, NAMED_PERMISSION_MODES, NoticeBar, NoticeCard, OutputCard, PermissionPicker, ProviderIssueBanner, RenameDialog, ShareButton, ShareDialog, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarToggle, agentColorHex, agentTypeLabel, applyPartDelta, askQuestionFromArgs, bashExitCode, chatReducer, clampPermissionMode, classifyPseudoUserMessage, clip, compactCount, computeEditDiff, computeWriteRows, defaultChatStrings, durationToAbsHumanDuration, editSummary, finalizeParts, firstNonEmptyLine, formatElapsed, formatFileSize, fullToolArgs, getGreeting, getProviderKeyFromModelId, hiddenTranscriptTool, initialState, interruptionNotice, isEditDiffTool, isGroupableTool, isMac, isNamedPermissionMode, isOrgModel, languageForPath, lineDelta, parseEditArgs, parseSubagentNotice, pasteInlineMaxBytes, permissionModesUpTo, planFromArgs, plural, providerIssueFor, resolveChatConfig, resultFailed, rollupSentence, sanitizeLabel, sanitizePanelText, slashMatches, subagentDeliveryLine, summarizeToolArgs, taskCallFromArgs, thinkingHeading, tokenizeDiffLines, toolHeaderFor, toolSummary, useChat, useChatConfig, useDragDrop, useKeyboardShortcuts, useRowDialogs, wordEmphasisRanges };
|
package/dist/ai/strings.d.ts
CHANGED
|
@@ -35,6 +35,15 @@ export interface ChatStrings {
|
|
|
35
35
|
};
|
|
36
36
|
queue: {
|
|
37
37
|
remove: string;
|
|
38
|
+
/** Under a message that goes out once the running reply finishes. */
|
|
39
|
+
waiting: string;
|
|
40
|
+
/** Under a message a steering host hands to the agent between steps. */
|
|
41
|
+
waitingNextStep: string;
|
|
42
|
+
/** Under a message the host has already handed to the running turn. */
|
|
43
|
+
handedOver: string;
|
|
44
|
+
/** The visible label of the remove control; `remove` names it for
|
|
45
|
+
* assistive technology. */
|
|
46
|
+
removeShort: string;
|
|
38
47
|
};
|
|
39
48
|
activity: {
|
|
40
49
|
tokens: (input: string, output: string) => string;
|
package/dist/ai/strings.js
CHANGED
|
@@ -32,7 +32,13 @@ var defaultChatStrings = {
|
|
|
32
32
|
title: "Billing group \"{group}\" is over its allocation",
|
|
33
33
|
message: "It has reached its allocation limit. Messages to this provider are paused until your administrator increases the allocation."
|
|
34
34
|
},
|
|
35
|
-
queue: {
|
|
35
|
+
queue: {
|
|
36
|
+
remove: "Remove queued message",
|
|
37
|
+
waiting: "Queued · sends when this reply finishes",
|
|
38
|
+
waitingNextStep: "Queued · reaches the agent after its current step",
|
|
39
|
+
handedOver: "Sent to the running turn",
|
|
40
|
+
removeShort: "Remove"
|
|
41
|
+
},
|
|
36
42
|
providerIssue: {
|
|
37
43
|
keyRejected: "API key rejected",
|
|
38
44
|
unreachable: "Endpoint unreachable",
|
package/dist/ai/ui/Notice.d.ts
CHANGED
|
@@ -10,6 +10,14 @@ export declare function NoticeBar({ tone, title, children, action, onAction, tes
|
|
|
10
10
|
onAction?: () => void;
|
|
11
11
|
testId?: string;
|
|
12
12
|
}): import("react").JSX.Element;
|
|
13
|
+
/** What a command printed, kept verbatim under the command that printed it. */
|
|
14
|
+
export declare function OutputCard({ title, text, onDismiss, dismissLabel, testId, }: {
|
|
15
|
+
title: string;
|
|
16
|
+
text: string;
|
|
17
|
+
onDismiss?: () => void;
|
|
18
|
+
dismissLabel?: string;
|
|
19
|
+
testId?: string;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
13
21
|
/** An inline notice next to the composer. One with no dismiss states a fact
|
|
14
22
|
* that is still true; one with a dismiss is a message the reader can put away. */
|
|
15
23
|
export declare function NoticeCard({ tone, children, onDismiss, dismissLabel, testId, }: {
|
package/dist/ai/ui/Notice.js
CHANGED
|
@@ -56,6 +56,32 @@ function NoticeBar({ tone, title, children, action, onAction, testId }) {
|
|
|
56
56
|
]
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
+
/** What a command printed, kept verbatim under the command that printed it. */
|
|
60
|
+
function OutputCard({ title, text, onDismiss, dismissLabel, testId }) {
|
|
61
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
62
|
+
"data-testid": testId,
|
|
63
|
+
"aria-label": title,
|
|
64
|
+
className: "relative my-3 rounded-lg border theme-border theme-muted-panel px-3 py-2",
|
|
65
|
+
children: [
|
|
66
|
+
/* @__PURE__ */ jsx("p", {
|
|
67
|
+
className: cx("font-mono text-[11px] theme-muted-text", onDismiss && "pr-6"),
|
|
68
|
+
children: title
|
|
69
|
+
}),
|
|
70
|
+
/* @__PURE__ */ jsx("pre", {
|
|
71
|
+
className: "mt-1 whitespace-pre-wrap break-words font-mono text-xs theme-text",
|
|
72
|
+
children: text
|
|
73
|
+
}),
|
|
74
|
+
onDismiss && /* @__PURE__ */ jsx("button", {
|
|
75
|
+
type: "button",
|
|
76
|
+
onClick: onDismiss,
|
|
77
|
+
"aria-label": dismissLabel,
|
|
78
|
+
title: dismissLabel,
|
|
79
|
+
className: "absolute top-1.5 right-1.5 rounded p-1 theme-muted-text hover:theme-hover",
|
|
80
|
+
children: /* @__PURE__ */ jsx(IoMdClose, { className: "h-3.5 w-3.5" })
|
|
81
|
+
})
|
|
82
|
+
]
|
|
83
|
+
});
|
|
84
|
+
}
|
|
59
85
|
/** An inline notice next to the composer. One with no dismiss states a fact
|
|
60
86
|
* that is still true; one with a dismiss is a message the reader can put away. */
|
|
61
87
|
function NoticeCard({ tone = "neutral", children, onDismiss, dismissLabel, testId }) {
|
|
@@ -76,4 +102,4 @@ function NoticeCard({ tone = "neutral", children, onDismiss, dismissLabel, testI
|
|
|
76
102
|
});
|
|
77
103
|
}
|
|
78
104
|
//#endregion
|
|
79
|
-
export { NoticeBar, NoticeCard };
|
|
105
|
+
export { NoticeBar, NoticeCard, OutputCard };
|
package/dist/ai/ui/sidebar.js
CHANGED
|
@@ -7,7 +7,7 @@ import { useState } from "react";
|
|
|
7
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
//#region src/ai/ui/sidebar.tsx
|
|
9
9
|
var SIDEBAR_RAIL_WIDTH_PX = 48;
|
|
10
|
-
var HIGHLIGHT_SELECTED = "bg-[color-mix(in_oklab,var(--theme-
|
|
10
|
+
var HIGHLIGHT_SELECTED = "bg-[color-mix(in_oklab,var(--theme-accent)_12%,transparent)]";
|
|
11
11
|
var HIGHLIGHT_HOVER = "hover:bg-[color-mix(in_oklab,var(--theme-panel)_5%,transparent)]";
|
|
12
12
|
/** The column a conversation list lives in: it animates between its width
|
|
13
13
|
* and a rail, unless a drag is setting the width right now. */
|
|
@@ -37,7 +37,7 @@ function SidebarToggle({ collapsed, onToggle, openLabel, closeLabel, className }
|
|
|
37
37
|
function SidebarGroupHeading({ children, tone = "default", testId }) {
|
|
38
38
|
return /* @__PURE__ */ jsx("h4", {
|
|
39
39
|
"data-testid": testId,
|
|
40
|
-
className: cx("mt-
|
|
40
|
+
className: cx("mt-5 mb-1 whitespace-nowrap px-2 text-xs font-medium", tone === "attention" ? "text-amber-700 dark:text-amber-300" : "theme-muted-text"),
|
|
41
41
|
children
|
|
42
42
|
});
|
|
43
43
|
}
|
|
@@ -46,7 +46,7 @@ function SidebarGroupHeading({ children, tone = "default", testId }) {
|
|
|
46
46
|
function SidebarRow({ selected, menuItems, openMenu, className, children }) {
|
|
47
47
|
const { active, openMenu: openRowMenu } = useRowMenuActive(openMenu);
|
|
48
48
|
return /* @__PURE__ */ jsxs("li", {
|
|
49
|
-
className: cx("group relative flex items-center justify-between rounded-lg px-2 py-
|
|
49
|
+
className: cx("group relative flex items-center justify-between rounded-lg px-2 py-[3px] transition-colors duration-150", selected || active ? HIGHLIGHT_SELECTED : HIGHLIGHT_HOVER, className),
|
|
50
50
|
...rowContextMenuProps(openRowMenu, menuItems),
|
|
51
51
|
children: [children, /* @__PURE__ */ jsx(MoreButton, {
|
|
52
52
|
active,
|
|
@@ -81,6 +81,9 @@ function isStorageDirectory(node) {
|
|
|
81
81
|
function getErrorMessage(error) {
|
|
82
82
|
return error instanceof Error ? error.message : String(error);
|
|
83
83
|
}
|
|
84
|
+
function isWithinRoots(roots) {
|
|
85
|
+
return (path) => roots.some(([, root]) => path.startsWith(root));
|
|
86
|
+
}
|
|
84
87
|
/** Directory paths from the storage root down to `path`. */
|
|
85
88
|
function getAncestorDirPaths(path) {
|
|
86
89
|
const isDir = path.endsWith("/");
|
|
@@ -189,6 +192,8 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
|
|
|
189
192
|
const [accessDrawerOpen, setAccessDrawerOpen] = useState(false);
|
|
190
193
|
const [connectionIssuesMessage, updateConnectionIssuesMessage] = useState(/* @__PURE__ */ new Map());
|
|
191
194
|
const [hasCorsIssue, setHasCorsIssue] = useState(/* @__PURE__ */ new Map());
|
|
195
|
+
const [previewNode, setPreviewNode] = useState(null);
|
|
196
|
+
const [previewOpen, setPreviewOpen] = useState(false);
|
|
192
197
|
const storageRoots = useMemo(() => {
|
|
193
198
|
const roots = /* @__PURE__ */ new Map();
|
|
194
199
|
for (const node of rootNodes) {
|
|
@@ -197,32 +202,52 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
|
|
|
197
202
|
}
|
|
198
203
|
return roots;
|
|
199
204
|
}, [rootNodes, parentToChildrenMap]);
|
|
205
|
+
const storageRootPaths = useMemo(() => new Map(storages.map((storage) => [storage.id, storage.rootPath])), [storages]);
|
|
200
206
|
const seenStorageRootsRef = useRef(storageRoots);
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
const
|
|
207
|
+
const seenRootPathsRef = useRef(storageRootPaths);
|
|
208
|
+
const dropStorageListings = (roots) => {
|
|
209
|
+
const isUnder = (path) => roots.some(([, root]) => path !== root && path.startsWith(root));
|
|
210
|
+
const isWithin = isWithinRoots(roots);
|
|
204
211
|
for (const path of [...fetchesInFlightRef.current.keys()]) if (isWithin(path)) fetchesInFlightRef.current.delete(path);
|
|
205
|
-
setListings((prev) =>
|
|
212
|
+
setListings((prev) => roots.reduce((next, [, root]) => clearListingsUnder(next, root), prev));
|
|
206
213
|
setLoadingPaths((prev) => new Set([...prev].filter((path) => !isWithin(path))));
|
|
207
214
|
setExpandedPaths((prev) => new Set([...prev].filter((path) => !isUnder(path))));
|
|
208
|
-
|
|
215
|
+
setCheckedItems((prev) => new Set([...prev].filter((path) => !isWithin(path))));
|
|
216
|
+
const withoutDropped = (prev) => {
|
|
209
217
|
const next = new Map(prev);
|
|
210
|
-
for (const [id] of
|
|
218
|
+
for (const [id] of roots) next.delete(id);
|
|
211
219
|
return next;
|
|
212
220
|
};
|
|
213
|
-
updateConnectionIssuesMessage(
|
|
214
|
-
setHasCorsIssue(
|
|
221
|
+
updateConnectionIssuesMessage(withoutDropped);
|
|
222
|
+
setHasCorsIssue(withoutDropped);
|
|
223
|
+
};
|
|
224
|
+
const forgetStorages = useEffectEvent((removed) => {
|
|
225
|
+
dropStorageListings(removed);
|
|
215
226
|
const selectedRoot = removed.find(([, root]) => normalizedSelectedPath.startsWith(root))?.[1];
|
|
216
227
|
if (selectedRoot) {
|
|
217
228
|
const parent = getParentPath(selectedRoot);
|
|
218
229
|
setSelectedPath(treeMap[parent] ? parent : "");
|
|
219
230
|
}
|
|
220
231
|
});
|
|
232
|
+
const rerootStorages = useEffectEvent((rerooted) => {
|
|
233
|
+
dropStorageListings(rerooted);
|
|
234
|
+
clearPreviewCache(isWithinRoots(rerooted));
|
|
235
|
+
if (previewNode && rerooted.some(([id]) => id === previewNode.storageId)) {
|
|
236
|
+
setPreviewOpen(false);
|
|
237
|
+
setPreviewNode(null);
|
|
238
|
+
}
|
|
239
|
+
const selectedRoot = rerooted.find(([, root]) => normalizedSelectedPath.startsWith(root))?.[1];
|
|
240
|
+
if (selectedRoot && selectedRoot !== normalizedSelectedPath) setSelectedPath(selectedRoot);
|
|
241
|
+
});
|
|
221
242
|
useEffect(() => {
|
|
243
|
+
const seenRootPaths = seenRootPathsRef.current;
|
|
222
244
|
const removed = [...seenStorageRootsRef.current].filter(([id]) => !storageRoots.has(id));
|
|
245
|
+
const rerooted = [...storageRoots].filter(([id]) => seenRootPaths.has(id) && seenRootPaths.get(id) !== storageRootPaths.get(id));
|
|
223
246
|
seenStorageRootsRef.current = storageRoots;
|
|
247
|
+
seenRootPathsRef.current = storageRootPaths;
|
|
224
248
|
if (removed.length > 0) forgetStorages(removed);
|
|
225
|
-
|
|
249
|
+
if (rerooted.length > 0) rerootStorages(rerooted);
|
|
250
|
+
}, [storageRoots, storageRootPaths]);
|
|
226
251
|
const [uploadModalOpen, setUploadModalOpen] = useState(false);
|
|
227
252
|
const [createFolderTarget, setCreateFolderTarget] = useState(null);
|
|
228
253
|
const [pendingUpload, setPendingUpload] = useState(null);
|
|
@@ -231,8 +256,6 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
|
|
|
231
256
|
const folderInputRef = useRef(null);
|
|
232
257
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
|
233
258
|
const [pendingDelete, setPendingDelete] = useState(null);
|
|
234
|
-
const [previewNode, setPreviewNode] = useState(null);
|
|
235
|
-
const [previewOpen, setPreviewOpen] = useState(false);
|
|
236
259
|
const [detailTab, setDetailTab] = useState("details");
|
|
237
260
|
useEffect(() => clearPreviewCache, []);
|
|
238
261
|
const { openMenu, contextMenu } = useRowMenu();
|
|
@@ -22,5 +22,5 @@ export declare function loadPreviewContent<T>(path: string, loader: (signal: Abo
|
|
|
22
22
|
export declare function fetchPreviewObject(path: string, url: string, signal: AbortSignal, headers?: HeadersInit): Promise<Response>;
|
|
23
23
|
export declare function peekPreviewUrl(path: string): UrlResult | undefined;
|
|
24
24
|
export declare function peekPreviewContent<T>(path: string): T | undefined;
|
|
25
|
-
export declare function clearPreviewCache(): void;
|
|
25
|
+
export declare function clearPreviewCache(isCleared?: (path: string) => boolean): void;
|
|
26
26
|
export {};
|
|
@@ -89,10 +89,12 @@ function peekPreviewUrl(path) {
|
|
|
89
89
|
function peekPreviewContent(path) {
|
|
90
90
|
return contentCache.get(path)?.value;
|
|
91
91
|
}
|
|
92
|
-
function clearPreviewCache() {
|
|
93
|
-
for (const record of contentCache
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
function clearPreviewCache(isCleared = () => true) {
|
|
93
|
+
for (const [path, record] of contentCache) if (isCleared(path)) {
|
|
94
|
+
disposeContent(record);
|
|
95
|
+
contentCache.delete(path);
|
|
96
|
+
}
|
|
97
|
+
for (const path of urlCache.keys()) if (isCleared(path)) urlCache.delete(path);
|
|
96
98
|
}
|
|
97
99
|
//#endregion
|
|
98
100
|
export { clearPreviewCache, fetchPreviewObject, loadPreviewContent, loadPreviewUrl, peekPreviewContent, peekPreviewUrl };
|
|
@@ -26,6 +26,7 @@ export type TStorage = {
|
|
|
26
26
|
canShare?: boolean | undefined;
|
|
27
27
|
canManageAccess?: boolean | undefined;
|
|
28
28
|
previewLimits?: Partial<PreviewLimits> | undefined;
|
|
29
|
+
rootPath?: string | undefined;
|
|
29
30
|
};
|
|
30
31
|
/** A file the host is asked to name or download: its storage, key and name. */
|
|
31
32
|
export interface ExplorerObject {
|