@iloveagents/foundry-web-ui 0.14.2 → 0.16.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/components/assistant-chat.js +1 -1
- package/dist/components/chat-message-parts.d.ts +12 -7
- package/dist/components/chat-message-parts.js +52 -17
- package/dist/components/message-disclosure.d.ts +6 -18
- package/dist/components/message-disclosure.js +24 -9
- package/dist/components/reasoning-effort-picker.js +38 -2
- package/dist/components/reasoning-part.d.ts +8 -5
- package/dist/components/reasoning-part.js +15 -11
- package/dist/components/sidebar.js +22 -20
- package/dist/components/turn-activity.d.ts +33 -0
- package/dist/components/turn-activity.js +110 -0
- package/dist/lib/nav-config.d.ts +16 -0
- package/package.json +3 -3
- package/dist/components/tool-run-group.d.ts +0 -29
- package/dist/components/tool-run-group.js +0 -56
|
@@ -120,7 +120,7 @@ export function ChatContent({ starterSuggestions = DEFAULT_STARTER_SUGGESTIONS,
|
|
|
120
120
|
const collapseChat = useChatBubbleStore((s) => s.collapse);
|
|
121
121
|
const showPagePanel = useChatBubbleStore((s) => s.showPagePanel);
|
|
122
122
|
const togglePagePanel = useChatBubbleStore((s) => s.togglePagePanel);
|
|
123
|
-
return (_jsxs(ThreadPrimitive.Root, { className: cn("flex flex-col flex-1", "bg-background text-foreground", "overflow-hidden"), children: [isExpanded && (_jsxs("div", { className: "shrink-0 flex items-center justify-between px-4 py-2 border-b border-border", children: [_jsxs("span", { className: "text-sm font-medium text-foreground flex items-center gap-2", children: [_jsx(Bot, { className: "size-4 text-primary" }), "Chat"] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(TooltipIconButton, { tooltip: showPagePanel ? "Hide page panel" : "Show page panel", size: "icon", onClick: togglePagePanel, className: "hidden xl:inline-flex", children: showPagePanel ? (_jsx(PanelRightClose, { className: "size-4" })) : (_jsx(PanelRightOpen, { className: "size-4" })) }), _jsx(TooltipIconButton, { tooltip: "Minimize to bubble", size: "icon", onClick: collapseChat, children: _jsx(Minimize2, { className: "size-4" }) })] })] })), _jsxs(DropZone, { children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsxs(ThreadPrimitive.Viewport, { className: "absolute inset-0 overflow-y-auto bg-background", children: [_jsx(ThreadPrimitive.Empty, { children: _jsxs("div", { className: cn("flex min-h-full flex-col", "items-center justify-center", "px-4"), children: [_jsx(Bot, { className: "size-16 mb-6 text-primary/70" }), _jsx("h1", { className: "text-[2rem] font-normal text-foreground text-center mb-6", children: "How can I help you today?" }), starterSuggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-2 max-w-lg", children: starterSuggestions.map((prompt) => (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, asChild: true, children: _jsx(Button, { variant: "outline", className: "text-sm", children: prompt }) }, prompt))) }))] }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "mx-auto max-w-3xl px-4 pt-8", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
123
|
+
return (_jsxs(ThreadPrimitive.Root, { className: cn("flex flex-col flex-1", "bg-background text-foreground", "overflow-hidden"), children: [isExpanded && (_jsxs("div", { className: "shrink-0 flex items-center justify-between px-4 py-2 border-b border-border", children: [_jsxs("span", { className: "text-sm font-medium text-foreground flex items-center gap-2", children: [_jsx(Bot, { className: "size-4 text-primary" }), "Chat"] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(TooltipIconButton, { tooltip: showPagePanel ? "Hide page panel" : "Show page panel", size: "icon", onClick: togglePagePanel, className: "hidden xl:inline-flex", children: showPagePanel ? (_jsx(PanelRightClose, { className: "size-4" })) : (_jsx(PanelRightOpen, { className: "size-4" })) }), _jsx(TooltipIconButton, { tooltip: "Minimize to bubble", size: "icon", onClick: collapseChat, children: _jsx(Minimize2, { className: "size-4" }) })] })] })), _jsxs(DropZone, { children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsxs(ThreadPrimitive.Viewport, { className: "absolute inset-0 overflow-y-auto bg-background", children: [_jsx(ThreadPrimitive.Empty, { children: _jsxs("div", { className: cn("flex min-h-full flex-col", "items-center justify-center", "px-4"), children: [_jsx(Bot, { className: "size-16 mb-6 text-primary/70" }), _jsx("h1", { className: "text-[2rem] font-normal text-foreground text-center mb-6", children: "How can I help you today?" }), starterSuggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-2 max-w-lg", children: starterSuggestions.map((prompt) => (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, asChild: true, children: _jsx(Button, { variant: "outline", className: "text-sm", children: prompt }) }, prompt))) }))] }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "mx-auto max-w-3xl px-4 pt-8 pb-4", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
124
124
|
UserMessage,
|
|
125
125
|
EditComposer,
|
|
126
126
|
AssistantMessage,
|
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
* hand-rolling a parts config is what keeps them honest — there is now only
|
|
11
11
|
* one place to drift from.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* over the cards, collapsed; see that component
|
|
13
|
+
* Each stretch of machinery folds into one {@link TurnActivity} — a single
|
|
14
|
+
* summary row over the cards and the thinking, collapsed; see that component
|
|
15
|
+
* for why.
|
|
15
16
|
*
|
|
16
|
-
* Reading order is
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Reading order is CHRONOLOGICAL, throughout. It briefly was not: text
|
|
18
|
+
* carried a flex `order` that floated every prose part to the bottom of the
|
|
19
|
+
* message, to keep a reasoning row from sitting above a one-line answer.
|
|
20
|
+
* With the machinery folded into a quiet 20px row that argument is spent,
|
|
21
|
+
* and the hoist actively hurt — in a multi-step turn it stripped the
|
|
22
|
+
* narration out from between the work and stacked the bare rows into the
|
|
23
|
+
* ladder this component exists to prevent. It also glued consecutive prose
|
|
24
|
+
* parts into one run-on paragraph. Parts now render where the model put
|
|
25
|
+
* them.
|
|
21
26
|
*/
|
|
22
27
|
export declare function ChatMessageParts(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -3,12 +3,21 @@ import { MessagePrimitive, groupPartByType } from "@assistant-ui/react";
|
|
|
3
3
|
import { MarkdownText } from "./markdown-text.js";
|
|
4
4
|
import { ReasoningMessagePartComponent } from "./reasoning-part.js";
|
|
5
5
|
import { ToolFallback } from "./tool-fallback.js";
|
|
6
|
-
import {
|
|
6
|
+
import { TurnActivity } from "./turn-activity.js";
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* A turn's machinery nests under a `group-activity` node; inside it, adjacent
|
|
9
|
+
* tool calls still coalesce into `group-tool`.
|
|
10
|
+
*
|
|
11
|
+
* Text is deliberately absent from this map. The model's prose stays
|
|
12
|
+
* ungrouped, so it can never end up inside the fold — and because grouping
|
|
13
|
+
* coalesces ADJACENT parts, each stretch of prose also ends one activity node
|
|
14
|
+
* and starts the next. That is the intent: a multi-step turn reads as work,
|
|
15
|
+
* what it learned, work, what it learned, answer.
|
|
10
16
|
*/
|
|
11
|
-
const groupBy = groupPartByType({
|
|
17
|
+
const groupBy = groupPartByType({
|
|
18
|
+
"tool-call": ["group-activity", "group-tool"],
|
|
19
|
+
reasoning: ["group-activity"],
|
|
20
|
+
});
|
|
12
21
|
/**
|
|
13
22
|
* The parts of an assistant message — ONE renderer, used by every chat
|
|
14
23
|
* surface.
|
|
@@ -21,27 +30,53 @@ const groupBy = groupPartByType({ "tool-call": ["group-tool"] });
|
|
|
21
30
|
* hand-rolling a parts config is what keeps them honest — there is now only
|
|
22
31
|
* one place to drift from.
|
|
23
32
|
*
|
|
24
|
-
*
|
|
25
|
-
* over the cards, collapsed; see that component
|
|
33
|
+
* Each stretch of machinery folds into one {@link TurnActivity} — a single
|
|
34
|
+
* summary row over the cards and the thinking, collapsed; see that component
|
|
35
|
+
* for why.
|
|
26
36
|
*
|
|
27
|
-
* Reading order is
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
37
|
+
* Reading order is CHRONOLOGICAL, throughout. It briefly was not: text
|
|
38
|
+
* carried a flex `order` that floated every prose part to the bottom of the
|
|
39
|
+
* message, to keep a reasoning row from sitting above a one-line answer.
|
|
40
|
+
* With the machinery folded into a quiet 20px row that argument is spent,
|
|
41
|
+
* and the hoist actively hurt — in a multi-step turn it stripped the
|
|
42
|
+
* narration out from between the work and stacked the bare rows into the
|
|
43
|
+
* ladder this component exists to prevent. It also glued consecutive prose
|
|
44
|
+
* parts into one run-on paragraph. Parts now render where the model put
|
|
45
|
+
* them.
|
|
32
46
|
*/
|
|
33
47
|
export function ChatMessageParts() {
|
|
34
48
|
return (_jsx(MessagePrimitive.GroupedParts, { groupBy: groupBy, indicator: "never", children: ({ part, children }) => {
|
|
35
49
|
switch (part.type) {
|
|
50
|
+
case "group-activity":
|
|
51
|
+
return (_jsx(TurnActivity, { indices: part.indices, isRunning: part.status?.type === "running", children: children }));
|
|
36
52
|
case "group-tool":
|
|
37
|
-
|
|
53
|
+
// Inside the activity fold, so NOT another collapsed row —
|
|
54
|
+
// opening one disclosure only to meet a second is a maze. The
|
|
55
|
+
// cards render directly; the count already showed on the row
|
|
56
|
+
// above.
|
|
57
|
+
return _jsx("div", { className: "flex flex-col gap-2", children: children });
|
|
38
58
|
case "text":
|
|
39
|
-
// The
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
59
|
+
// The wrapper is load-bearing, though it carries no classes.
|
|
60
|
+
// `MarkdownText` renders `<ReactMarkdown>` directly, which emits
|
|
61
|
+
// its top-level blocks into a FRAGMENT — without an element to
|
|
62
|
+
// hold them, every paragraph, list and code block becomes a
|
|
63
|
+
// direct child of the message's `gap-4` flex container and picks
|
|
64
|
+
// up 16px of message-part spacing between them, on top of their
|
|
65
|
+
// own margins. It used to be the `order-2` div; the ordering went
|
|
66
|
+
// away, the wrapper must not.
|
|
67
|
+
return (_jsx("div", { children: _jsx(MarkdownText, {}) }));
|
|
43
68
|
case "reasoning":
|
|
44
|
-
|
|
69
|
+
// Plain inside the fold, rather than behind a second disclosure.
|
|
70
|
+
//
|
|
71
|
+
// Its POSITION is only as chronological as the parts array it is
|
|
72
|
+
// given. Replayed history preserves the model's interleaving;
|
|
73
|
+
// the live adapter does not — it accumulates all reasoning into a
|
|
74
|
+
// single part emitted ahead of every tool call, so a live
|
|
75
|
+
// reasoning A → tool A → reasoning B → tool B arrives as
|
|
76
|
+
// reasoning A+B → tool A → tool B. That is the adapter's to fix,
|
|
77
|
+
// and rendering in place is right either way; this note is here
|
|
78
|
+
// so the next reader does not take the ordering on faith.
|
|
79
|
+
return _jsx(ReasoningMessagePartComponent, { text: part.text, nested: true });
|
|
45
80
|
case "tool-call":
|
|
46
81
|
return part.toolUI ?? _jsx(ToolFallback, { ...part });
|
|
47
82
|
default:
|
|
@@ -9,10 +9,11 @@ import type { ReactNode } from "react";
|
|
|
9
9
|
* different paddings, different hover treatments, different default states.
|
|
10
10
|
* Read together in a transcript they looked like two unrelated widgets.
|
|
11
11
|
*
|
|
12
|
-
* So the shape lives here once. A row is:
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* So the shape lives here once. A row is: the part's own icon, its label,
|
|
13
|
+
* then the disclosure caret — on a single baseline, with identical metrics.
|
|
14
|
+
* The icon leads because it is the glyph that carries meaning (what kind of
|
|
15
|
+
* thing is folded away, and whether it went well); the caret trails, where
|
|
16
|
+
* the eye lands after the words and where every chat client puts it.
|
|
16
17
|
*
|
|
17
18
|
* Everything is understated on purpose. These rows sit between the user's
|
|
18
19
|
* question and the answer, and they are machinery, not content: muted until
|
|
@@ -20,7 +21,7 @@ import type { ReactNode } from "react";
|
|
|
20
21
|
* hairline rule, so an open row reads as a margin note rather than a second
|
|
21
22
|
* message.
|
|
22
23
|
*/
|
|
23
|
-
export declare function MessageDisclosure({ icon, label, open, onOpenChange, tone,
|
|
24
|
+
export declare function MessageDisclosure({ icon, label, open, onOpenChange, tone, ariaLabel, children, ...rest }: {
|
|
24
25
|
/** The part's own icon — a brain, a wrench, a status glyph. */
|
|
25
26
|
icon: ReactNode;
|
|
26
27
|
label: ReactNode;
|
|
@@ -28,19 +29,6 @@ export declare function MessageDisclosure({ icon, label, open, onOpenChange, ton
|
|
|
28
29
|
onOpenChange: (open: boolean) => void;
|
|
29
30
|
/** ``destructive`` is for a run where everything failed; nothing else. */
|
|
30
31
|
tone?: "muted" | "destructive";
|
|
31
|
-
/**
|
|
32
|
-
* ``after-tools`` sits the row between the tool runs and the answer,
|
|
33
|
-
* wherever the model actually emitted it — reasoning belongs under the
|
|
34
|
-
* work it explains and above the conclusion it produced. (The message
|
|
35
|
-
* text is ordered after it; see `ChatMessageParts`.)
|
|
36
|
-
*
|
|
37
|
-
* It is a flex `order`, so the row moves VISUALLY while staying put in the
|
|
38
|
-
* DOM. That is exactly why such a row carries a different attribute: the
|
|
39
|
-
* compaction below is a DOM-sibling rule, and a row that is first in the
|
|
40
|
-
* DOM but second on screen would otherwise hand its negative margin to
|
|
41
|
-
* whichever row really is on top.
|
|
42
|
-
*/
|
|
43
|
-
placement?: "inline" | "after-tools";
|
|
44
32
|
ariaLabel?: string;
|
|
45
33
|
children: ReactNode;
|
|
46
34
|
} & Record<`data-${string}`, unknown>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
3
3
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
4
4
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
5
5
|
/**
|
|
@@ -12,10 +12,11 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/colla
|
|
|
12
12
|
* different paddings, different hover treatments, different default states.
|
|
13
13
|
* Read together in a transcript they looked like two unrelated widgets.
|
|
14
14
|
*
|
|
15
|
-
* So the shape lives here once. A row is:
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* So the shape lives here once. A row is: the part's own icon, its label,
|
|
16
|
+
* then the disclosure caret — on a single baseline, with identical metrics.
|
|
17
|
+
* The icon leads because it is the glyph that carries meaning (what kind of
|
|
18
|
+
* thing is folded away, and whether it went well); the caret trails, where
|
|
19
|
+
* the eye lands after the words and where every chat client puts it.
|
|
19
20
|
*
|
|
20
21
|
* Everything is understated on purpose. These rows sit between the user's
|
|
21
22
|
* question and the answer, and they are machinery, not content: muted until
|
|
@@ -23,8 +24,22 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/colla
|
|
|
23
24
|
* hairline rule, so an open row reads as a margin note rather than a second
|
|
24
25
|
* message.
|
|
25
26
|
*/
|
|
26
|
-
export function MessageDisclosure({ icon, label, open, onOpenChange, tone = "muted",
|
|
27
|
-
return (_jsxs(Collapsible, { open: open, onOpenChange: onOpenChange,
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
export function MessageDisclosure({ icon, label, open, onOpenChange, tone = "muted", ariaLabel, children, ...rest }) {
|
|
28
|
+
return (_jsxs(Collapsible, { open: open, onOpenChange: onOpenChange, "data-message-disclosure": true,
|
|
29
|
+
// Rows render where the model put them. There was once an
|
|
30
|
+
// ``after-tools`` placement that moved a row with flex `order` — and a
|
|
31
|
+
// separate data attribute, because the compaction below is a
|
|
32
|
+
// DOM-SIBLING rule and a row that is first in the DOM but second on
|
|
33
|
+
// screen would hand its negative margin to the wrong neighbour. Both
|
|
34
|
+
// are gone: the reordering existed to keep a reasoning row from sitting
|
|
35
|
+
// above a short answer, which folding the machinery into one quiet row
|
|
36
|
+
// already solves.
|
|
37
|
+
className: "w-full [[data-message-disclosure]_+_&]:-mt-3", ...rest, children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-disclosure-trigger": true, "aria-label": ariaLabel, className: cn(
|
|
38
|
+
// `w-fit` with `-ml-1.5`: the row is a pill that hugs its own
|
|
39
|
+
// words, and the negative margin cancels the padding so the
|
|
40
|
+
// ICON lines up with the message text's left edge rather than
|
|
41
|
+
// sitting 6px inside it. Full-width was worse on both counts —
|
|
42
|
+
// it made a hover target the width of the message, and it left
|
|
43
|
+
// the caret stranded at the far right once the caret moved.
|
|
44
|
+
"group/disclosure -ml-1.5 flex w-fit max-w-full items-center gap-1.5 rounded-md", "px-1.5 py-0.5 text-left text-xs transition-colors", "text-muted-foreground hover:bg-muted hover:text-foreground", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", tone === "destructive" && "text-destructive hover:text-destructive"), children: [_jsx("span", { className: "flex size-3.5 shrink-0 items-center justify-center", children: icon }), _jsx("span", { className: "truncate", children: label }), _jsx(ChevronDown, { "aria-hidden": "true", className: cn("size-3.5 shrink-0 opacity-60 transition-transform duration-150", "group-hover/disclosure:opacity-100", open && "rotate-180") })] }) }), _jsx(CollapsibleContent, { children: _jsx("div", { className: "mt-1.5 ml-3 border-l border-border pl-3", children: children }) })] }));
|
|
30
45
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useStore } from "zustand";
|
|
3
3
|
import { Check, ChevronDown } from "lucide-react";
|
|
4
4
|
import { reasoningDefaultStore, reasoningEffortStore, REASONING_EFFORT_LABELS, } from "@iloveagents/foundry-agent";
|
|
@@ -11,6 +11,40 @@ const OPTIONS = [
|
|
|
11
11
|
{ value: "high", hint: "Slower, better on hard problems" },
|
|
12
12
|
{ value: "xhigh", hint: "Slowest, for the hardest questions" },
|
|
13
13
|
];
|
|
14
|
+
/** Bar count per level, lowest to highest. */
|
|
15
|
+
const EFFORT_RANK = {
|
|
16
|
+
low: 1,
|
|
17
|
+
medium: 2,
|
|
18
|
+
high: 3,
|
|
19
|
+
xhigh: 4,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The level as four rising bars.
|
|
23
|
+
*
|
|
24
|
+
* The trigger's job is to answer "how hard is it thinking?" at a glance, and
|
|
25
|
+
* in a narrow composer — the chat bubble, a side panel — the words cost more
|
|
26
|
+
* width than that question is worth. Bars cost ~14px and answer it without
|
|
27
|
+
* reading, so in that case the words drop and this stands in for them.
|
|
28
|
+
*
|
|
29
|
+
* Takes a CONCRETE level, never `null`. A gauge with nothing lit is not a
|
|
30
|
+
* gauge, it is four faint marks; the caller keeps the words instead when
|
|
31
|
+
* there is no level to encode, which makes the empty state unrepresentable
|
|
32
|
+
* here rather than something this has to render politely.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately shows LEVEL only, never whether the level is pinned or
|
|
35
|
+
* inherited from `Auto`. The level is what changes cost and latency; the mode
|
|
36
|
+
* is the half this trigger already drops first when space runs short.
|
|
37
|
+
*
|
|
38
|
+
* `bg-current` throughout, so the bars inherit the button's own muted →
|
|
39
|
+
* foreground hover transition instead of needing their own colour states.
|
|
40
|
+
*/
|
|
41
|
+
function EffortGauge({ level }) {
|
|
42
|
+
const rank = EFFORT_RANK[level];
|
|
43
|
+
return (_jsx("span", { "aria-hidden": "true", className: "flex h-3.5 shrink-0 items-end gap-px", children: [1, 2, 3, 4].map((step) => (_jsx("span", { className: cn("w-0.5 rounded-full bg-current transition-opacity", step === 1 && "h-1", step === 2 && "h-1.5", step === 3 && "h-2", step === 4 && "h-2.5",
|
|
44
|
+
// The unlit bars stay visible: the gauge has to read as "2 of 4",
|
|
45
|
+
// not as a bare pair of marks whose ceiling you cannot see.
|
|
46
|
+
step <= rank ? "opacity-100" : "opacity-25") }, step))) }));
|
|
47
|
+
}
|
|
14
48
|
/**
|
|
15
49
|
* What `Auto` is currently doing, in words.
|
|
16
50
|
*
|
|
@@ -54,5 +88,7 @@ export function ReasoningEffortPicker({ className }) {
|
|
|
54
88
|
? REASONING_EFFORT_LABELS[resolved]
|
|
55
89
|
: REASONING_EFFORT_LABELS[effort];
|
|
56
90
|
const triggerLabel = showsMode ? `${modeLabel} · ${levelLabel}` : levelLabel;
|
|
57
|
-
|
|
91
|
+
// `default` with nothing resolved is the one case with no level to draw.
|
|
92
|
+
const gaugeLevel = effort === "default" ? resolved : effort;
|
|
93
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-reasoning-effort-trigger": true, className: cn("flex items-center gap-1 rounded-md px-2 py-1", "text-xs text-muted-foreground hover:text-foreground hover:bg-muted", "transition-colors", className), "aria-label": `Intelligence: ${triggerLabel}`, title: `Intelligence: ${triggerLabel}`, children: [gaugeLevel !== null && (_jsx("span", { className: "hidden @max-[24rem]:flex", children: _jsx(EffortGauge, { level: gaugeLevel }) })), _jsxs("span", { className: cn("whitespace-nowrap", gaugeLevel !== null && "@max-[24rem]:hidden"), children: [showsMode && `${modeLabel} · `, levelLabel] }), _jsx(ChevronDown, { className: "size-3.5 shrink-0" })] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-56", children: [_jsx(DropdownMenuLabel, { className: "text-xs font-normal text-muted-foreground", children: "Intelligence" }), OPTIONS.map((opt) => (_jsxs(DropdownMenuItem, { onSelect: () => setEffort(opt.value), className: "flex items-start gap-2", children: [_jsx(Check, { className: cn("size-4 mt-0.5 shrink-0", effort === opt.value ? "opacity-100" : "opacity-0") }), _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "text-sm", children: REASONING_EFFORT_LABELS[opt.value] }), _jsx("span", { className: "text-xs text-muted-foreground", children: opt.value === "default" ? autoHint(resolved, scope) : opt.hint })] })] }, opt.value)))] })] }));
|
|
58
94
|
}
|
|
@@ -6,14 +6,17 @@
|
|
|
6
6
|
* a curious reader, not the response. It stays visible after the run so a
|
|
7
7
|
* reader can check *why* an answer came out the way it did.
|
|
8
8
|
*
|
|
9
|
-
* The row shape comes from `MessageDisclosure`, shared with the
|
|
10
|
-
*
|
|
11
|
-
* separately, which is how they ended up with their carets on opposite
|
|
9
|
+
* The row shape comes from `MessageDisclosure`, shared with the turn's
|
|
10
|
+
* activity row — these sit next to each other in a message and used to be
|
|
11
|
+
* built separately, which is how they ended up with their carets on opposite
|
|
12
|
+
* sides.
|
|
12
13
|
*/
|
|
13
|
-
export declare function ReasoningPart({ text }: {
|
|
14
|
+
export declare function ReasoningPart({ text, nested }: {
|
|
14
15
|
text: string;
|
|
16
|
+
nested?: boolean;
|
|
15
17
|
}): import("react/jsx-runtime").JSX.Element | null;
|
|
16
18
|
/** Adapter for assistant-ui's `Reasoning` part slot. */
|
|
17
|
-
export declare const ReasoningMessagePartComponent: ({ text }: {
|
|
19
|
+
export declare const ReasoningMessagePartComponent: ({ text, nested, }: {
|
|
18
20
|
text: string;
|
|
21
|
+
nested?: boolean;
|
|
19
22
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { Brain } from "lucide-react";
|
|
4
4
|
import { MessageDisclosure } from "./message-disclosure.js";
|
|
@@ -10,19 +10,23 @@ import { MessageDisclosure } from "./message-disclosure.js";
|
|
|
10
10
|
* a curious reader, not the response. It stays visible after the run so a
|
|
11
11
|
* reader can check *why* an answer came out the way it did.
|
|
12
12
|
*
|
|
13
|
-
* The row shape comes from `MessageDisclosure`, shared with the
|
|
14
|
-
*
|
|
15
|
-
* separately, which is how they ended up with their carets on opposite
|
|
13
|
+
* The row shape comes from `MessageDisclosure`, shared with the turn's
|
|
14
|
+
* activity row — these sit next to each other in a message and used to be
|
|
15
|
+
* built separately, which is how they ended up with their carets on opposite
|
|
16
|
+
* sides.
|
|
16
17
|
*/
|
|
17
|
-
export function ReasoningPart({ text }) {
|
|
18
|
+
export function ReasoningPart({ text, nested = false }) {
|
|
18
19
|
const [isOpen, setIsOpen] = useState(false);
|
|
19
20
|
if (!text.trim())
|
|
20
21
|
return null;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
// Inside the turn's activity fold there is no second fold: the reader
|
|
23
|
+
// already opened one to get here, and a disclosure containing disclosures
|
|
24
|
+
// is a maze. Just the thinking, captioned, in the slot the model produced
|
|
25
|
+
// it in.
|
|
26
|
+
if (nested) {
|
|
27
|
+
return (_jsxs("div", { className: "flex flex-col gap-1", "data-reasoning-part": true, children: [_jsxs("span", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [_jsx(Brain, { className: "size-3.5" }), "Reasoning"] }), _jsx("div", { className: "whitespace-pre-wrap text-xs text-muted-foreground", children: text })] }));
|
|
28
|
+
}
|
|
29
|
+
return (_jsx(MessageDisclosure, { icon: _jsx(Brain, { className: "size-3.5" }), label: "Reasoning", open: isOpen, onOpenChange: setIsOpen, ariaLabel: isOpen ? "Hide reasoning" : "Show reasoning", children: _jsx("div", { className: "whitespace-pre-wrap text-xs text-muted-foreground", children: text }) }));
|
|
26
30
|
}
|
|
27
31
|
/** Adapter for assistant-ui's `Reasoning` part slot. */
|
|
28
|
-
export const ReasoningMessagePartComponent = ({ text }) =>
|
|
32
|
+
export const ReasoningMessagePartComponent = ({ text, nested = false, }) => _jsx(ReasoningPart, { text: text, nested: nested });
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { NavLink, useLocation, useNavigate } from "react-router";
|
|
4
|
-
import { Ellipsis, PanelLeft, PanelLeftOpen, SquarePen, ChevronDown, ChevronRight, GripVertical, Plus, Upload } from "lucide-react";
|
|
4
|
+
import { Ellipsis, PanelLeft, PanelLeftOpen, SquarePen, ChevronDown, ChevronRight, GripVertical, Plus, Upload, } from "lucide-react";
|
|
5
5
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
6
6
|
import { useSidebarStore, RAIL_WIDTH, SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH, } from "../lib/sidebar-store.js";
|
|
7
7
|
import { useToolPanelStore } from "../lib/tool-panel-store.js";
|
|
8
8
|
import { useNewConversation } from "../lib/use-new-conversation.js";
|
|
9
9
|
import { useNavStore } from "../lib/nav-store.js";
|
|
10
10
|
import { useAppStore } from "../lib/app-store.js";
|
|
11
|
-
import { getContainerDropZoneLabel, hasAnyDropTarget, hasExternalFilesData } from "../lib/nav-dnd.js";
|
|
11
|
+
import { getContainerDropZoneLabel, hasAnyDropTarget, hasExternalFilesData, } from "../lib/nav-dnd.js";
|
|
12
12
|
import { TooltipIconButton } from "./tooltip-icon-button.js";
|
|
13
13
|
import { Button } from "@iloveagents/foundry-web-primitives";
|
|
14
|
-
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu.js";
|
|
14
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu.js";
|
|
15
15
|
import { AppBrand } from "./app-brand.js";
|
|
16
16
|
const ACTIVE_NAV_ROW_CLASS = "border border-transparent bg-sidebar-selected shadow-none";
|
|
17
17
|
const ACTIVE_NAV_TEXT_CLASS = "font-semibold text-sidebar-foreground";
|
|
@@ -127,10 +127,7 @@ function useActiveNavRowScroll(isActive) {
|
|
|
127
127
|
return;
|
|
128
128
|
const rect = element.getBoundingClientRect();
|
|
129
129
|
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
130
|
-
const isOnScreen = rect.width > 0 &&
|
|
131
|
-
rect.height > 0 &&
|
|
132
|
-
rect.right > 0 &&
|
|
133
|
-
rect.left < viewportWidth;
|
|
130
|
+
const isOnScreen = rect.width > 0 && rect.height > 0 && rect.right > 0 && rect.left < viewportWidth;
|
|
134
131
|
if (!isOnScreen)
|
|
135
132
|
return;
|
|
136
133
|
const nav = element.closest("nav");
|
|
@@ -427,10 +424,10 @@ function ContainerDropZone({ dnd }) {
|
|
|
427
424
|
* row underneath; the branch chevron keeps its own, since positioned
|
|
428
425
|
* elements paint above the static button and win the hit test.
|
|
429
426
|
*/
|
|
430
|
-
function NavTwisty({ hasChildren, isExpanded, onToggle, label, isActive, depth, inTree = true, }) {
|
|
427
|
+
function NavTwisty({ hasChildren, childrenUnloaded = false, isExpanded, onToggle, label, isActive, depth, inTree = true, }) {
|
|
431
428
|
const cell = cn(NAV_TWISTY_CLASS, getNavDepthOffsets(depth).column);
|
|
432
429
|
if (!hasChildren) {
|
|
433
|
-
return (_jsx("span", { "data-nav-twisty-cell": true, "aria-hidden": "true", className: cn(cell, "pointer-events-none"), children: inTree && (_jsx("span", { className: cn("size-1 rounded-full", isActive ? "bg-primary/70" : "bg-sidebar-foreground/30") })) }));
|
|
430
|
+
return (_jsx("span", { "data-nav-twisty-cell": true, "aria-hidden": "true", className: cn(cell, "pointer-events-none"), children: inTree && !childrenUnloaded && (_jsx("span", { "data-nav-leaf-dot": true, className: cn("size-1 rounded-full", isActive ? "bg-primary/70" : "bg-sidebar-foreground/30") })) }));
|
|
434
431
|
}
|
|
435
432
|
return (_jsx("span", { "data-nav-twisty-cell": true, className: cell, children: _jsx("button", { type: "button", "data-nav-twisty": true, "aria-expanded": isExpanded, "aria-label": isExpanded ? `Collapse ${label}` : `Expand ${label}`, onClick: (e) => {
|
|
436
433
|
e.preventDefault();
|
|
@@ -480,11 +477,13 @@ function NavLeafItem({ item, isActive, onClick, depth = 0, }) {
|
|
|
480
477
|
!isDisabled &&
|
|
481
478
|
"ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
|
|
482
479
|
!isDisabled &&
|
|
483
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: false, label: item.label, isActive: isActive && !isDisabled, depth: depth }), _jsxs("button", { type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, className: cn("flex items-center gap-2 flex-1 min-w-0 py-1.5 text-sm text-left", bodyPl, "pr-3 transition-[padding] duration-150", NAV_ROW_ACTIONS_CLEARANCE, isDisabled
|
|
480
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, label: item.label, isActive: isActive && !isDisabled, depth: depth }), _jsxs("button", { type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, className: cn("flex items-center gap-2 flex-1 min-w-0 py-1.5 text-sm text-left", bodyPl, "pr-3 transition-[padding] duration-150", NAV_ROW_ACTIONS_CLEARANCE, isDisabled
|
|
484
481
|
? "text-sidebar-foreground/45 cursor-default"
|
|
485
482
|
: isActive
|
|
486
483
|
? ACTIVE_NAV_TEXT_CLASS
|
|
487
|
-
: "text-sidebar-foreground/80"), children: [_jsx(item.icon, { className: cn("size-3.5 shrink-0", isActive && !isDisabled ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
|
|
484
|
+
: "text-sidebar-foreground/80"), children: [_jsx(item.icon, { className: cn("size-3.5 shrink-0", isActive && !isDisabled ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
|
|
485
|
+
? ACTIVE_NAV_BADGE_CLASS
|
|
486
|
+
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }));
|
|
488
487
|
}
|
|
489
488
|
return (_jsxs("button", { ref: activeButtonRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, ...(isDisabled ? {} : draggableProps), ...(isDisabled ? {} : dropTargetProps), className: cn("relative flex items-center gap-2 w-full", "rounded-lg border border-transparent pr-3 py-1.5 text-sm text-left", bodyPl, isDisabled
|
|
490
489
|
? "opacity-40 cursor-default"
|
|
@@ -493,7 +492,7 @@ function NavLeafItem({ item, isActive, onClick, depth = 0, }) {
|
|
|
493
492
|
!isDisabled &&
|
|
494
493
|
"ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
|
|
495
494
|
!isDisabled &&
|
|
496
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: false, label: item.label, isActive: isActive && !isDisabled, depth: depth }), _jsx(item.icon, { className: cn("size-3.5 shrink-0", isActive && !isDisabled ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-foreground/60") }), _jsx("span", { className: cn("truncate", isActive && !isDisabled && ACTIVE_NAV_TEXT_CLASS), children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive ? ACTIVE_NAV_BADGE_CLASS : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "ml-auto size-3.5 shrink-0 text-primary pointer-events-none" }))] }));
|
|
495
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, label: item.label, isActive: isActive && !isDisabled, depth: depth }), _jsx(item.icon, { className: cn("size-3.5 shrink-0", isActive && !isDisabled ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-foreground/60") }), _jsx("span", { className: cn("truncate", isActive && !isDisabled && ACTIVE_NAV_TEXT_CLASS), children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive ? ACTIVE_NAV_BADGE_CLASS : "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "ml-auto size-3.5 shrink-0 text-primary pointer-events-none" }))] }));
|
|
497
496
|
}
|
|
498
497
|
function SubNavItems({ items, parentPath, depth = 1, }) {
|
|
499
498
|
const navigate = useNavigate();
|
|
@@ -587,7 +586,9 @@ function NestedFolderItem({ item, depth }) {
|
|
|
587
586
|
? ACTIVE_NAV_ICON_CLASS
|
|
588
587
|
: isAncestorActive
|
|
589
588
|
? "text-sidebar-foreground/80"
|
|
590
|
-
: "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
|
|
589
|
+
: "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
|
|
590
|
+
? ACTIVE_NAV_BADGE_CLASS
|
|
591
|
+
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }), isExpanded && item.children && (_jsxs("div", { children: [item.children.map((child) => {
|
|
591
592
|
const hasNestedChildren = child.children && child.children.length > 0;
|
|
592
593
|
if (hasNestedChildren) {
|
|
593
594
|
return (_jsx(NestedFolderItem, { item: child, depth: depth + 1 }, child.to + child.label));
|
|
@@ -634,7 +635,7 @@ function CollapsibleNavItem({ item, inTree = true }) {
|
|
|
634
635
|
!isDisabled &&
|
|
635
636
|
"ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
|
|
636
637
|
!isDisabled &&
|
|
637
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: hasChildren, inTree: inTree, isExpanded: isExpanded, onToggle: hasChildren ? () => setManualOverride(!isExpanded) : undefined, label: item.label, isActive: (selected || ancestor) && !isDisabled, depth: 0 }), _jsxs("button", { type: "button", onClick: () => {
|
|
638
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: hasChildren, childrenUnloaded: item.childrenUnloaded, inTree: inTree, isExpanded: isExpanded, onToggle: hasChildren ? () => setManualOverride(!isExpanded) : undefined, label: item.label, isActive: (selected || ancestor) && !isDisabled, depth: 0 }), _jsxs("button", { type: "button", onClick: () => {
|
|
638
639
|
if (isDisabled)
|
|
639
640
|
return;
|
|
640
641
|
setNavContext({
|
|
@@ -659,13 +660,15 @@ function CollapsibleNavItem({ item, inTree = true }) {
|
|
|
659
660
|
? ACTIVE_NAV_ICON_CLASS
|
|
660
661
|
: ancestor
|
|
661
662
|
? "text-sidebar-foreground/80"
|
|
662
|
-
: "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", selected
|
|
663
|
+
: "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", selected
|
|
664
|
+
? ACTIVE_NAV_BADGE_CLASS
|
|
665
|
+
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }));
|
|
663
666
|
return (_jsxs("div", { ...(isDisabled ? {} : dropTargetProps), ...(isDisabled ? {} : draggableProps), children: [inner(isActive, isAncestorActive), hasChildren && isExpanded && (_jsxs(_Fragment, { children: [_jsx(SubNavItems, { items: item.children, parentPath: item.to }), item.dnd?.canDrop() && _jsx(ContainerDropZone, { dnd: item.dnd })] }))] }));
|
|
664
667
|
}
|
|
665
668
|
if (isDisabled) {
|
|
666
669
|
return (_jsxs("div", { "data-nav-depth": 0, className: cn("relative flex items-center gap-2 w-full opacity-40", inTree ? getNavDepthOffsets(0).body : NAV_FLAT_BODY_CLASS,
|
|
667
670
|
// Transparent border for box-model parity — see the NavLink variant.
|
|
668
|
-
"rounded-lg border border-transparent pr-3 py-2 text-left text-sm text-sidebar-foreground/45"), children: [_jsx(NavTwisty, { hasChildren: false, inTree: inTree, label: item.label, depth: 0 }), _jsx(item.icon, { className: "size-4 shrink-0 text-sidebar-foreground/45" }), _jsx("span", { className: "truncate", children: item.label })] }));
|
|
671
|
+
"rounded-lg border border-transparent pr-3 py-2 text-left text-sm text-sidebar-foreground/45"), children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, inTree: inTree, label: item.label, depth: 0 }), _jsx(item.icon, { className: "size-4 shrink-0 text-sidebar-foreground/45" }), _jsx("span", { className: "truncate", children: item.label })] }));
|
|
669
672
|
}
|
|
670
673
|
return (_jsx(NavLink, { ref: activeLinkRef, "data-active-nav-row": currentPath === item.to ? "true" : undefined, "data-nav-depth": 0, to: item.to, ...draggableProps, ...dropTargetProps, className: ({ isActive }) => cn("relative flex items-center gap-2 w-full", inTree ? getNavDepthOffsets(0).body : NAV_FLAT_BODY_CLASS,
|
|
671
674
|
// The transparent border is NOT decoration: it is what makes this
|
|
@@ -675,7 +678,8 @@ function CollapsibleNavItem({ item, inTree = true }) {
|
|
|
675
678
|
// siblings' and the column goes ragged.
|
|
676
679
|
"rounded-xl border border-transparent pr-3 py-2 text-sm text-left", "text-sidebar-foreground/85 transition-colors hover:bg-sidebar-accent/70", isActive && cn(ACTIVE_NAV_ROW_CLASS, ACTIVE_NAV_TEXT_CLASS), isDragOver &&
|
|
677
680
|
!isFileDragOver &&
|
|
678
|
-
"bg-sidebar-selected ring-2 ring-inset ring-sidebar-ring/50", isFileDragOver &&
|
|
681
|
+
"bg-sidebar-selected ring-2 ring-inset ring-sidebar-ring/50", isFileDragOver &&
|
|
682
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && "opacity-50"), children: ({ isActive }) => (_jsxs(_Fragment, { children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, inTree: inTree, label: item.label, isActive: isActive, depth: 0 }), _jsx(item.icon, { className: cn("size-4 shrink-0", isActive ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-foreground/60") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
|
|
679
683
|
? ACTIVE_NAV_BADGE_CLASS
|
|
680
684
|
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot }), isFileDragOver && (_jsx(Upload, { "aria-hidden": "true", className: "ml-auto size-3.5 shrink-0 text-primary pointer-events-none" }))] })) }));
|
|
681
685
|
}
|
|
@@ -802,9 +806,7 @@ function useGroupCollapsed(label, defaultCollapsed) {
|
|
|
802
806
|
// that happens to collide with a prototype key (e.g. ``toString``,
|
|
803
807
|
// ``__proto__``, ``constructor``) is not treated as a persisted
|
|
804
808
|
// entry. ``map`` is parsed JSON so its prototype is ``Object``'s.
|
|
805
|
-
return Object.prototype.hasOwnProperty.call(map, label)
|
|
806
|
-
? map[label]
|
|
807
|
-
: Boolean(defaultCollapsed);
|
|
809
|
+
return Object.prototype.hasOwnProperty.call(map, label) ? map[label] : Boolean(defaultCollapsed);
|
|
808
810
|
}
|
|
809
811
|
function toggleGroupCollapsed(label, current) {
|
|
810
812
|
writeCollapseMap({ ...readCollapseMap(), [label]: !current });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* ONE row for everything a turn did before it answered.
|
|
4
|
+
*
|
|
5
|
+
* A turn that searched a corpus eleven times used to render eleven summary
|
|
6
|
+
* rows — one per adjacent run of tool calls — plus a reasoning row of its
|
|
7
|
+
* own, and the answer somewhere underneath. Each row was individually
|
|
8
|
+
* reasonable; stacked, they were a ladder the reader had to climb past to
|
|
9
|
+
* reach the point.
|
|
10
|
+
*
|
|
11
|
+
* So the machinery folds: "Worked · 20 tools · reasoning". The model's PROSE
|
|
12
|
+
* never goes inside — it is what the reader came for and it stays in the
|
|
13
|
+
* open, which also means each stretch of it ends one of these rows and
|
|
14
|
+
* starts the next. A turn that narrates its steps gets one row per step
|
|
15
|
+
* rather than one per tool run; a turn that just works and answers gets
|
|
16
|
+
* exactly one.
|
|
17
|
+
*
|
|
18
|
+
* Order is CHRONOLOGICAL, inside the fold and outside it. Reasoning explains
|
|
19
|
+
* why the next call happened ("I need the termination clause from both
|
|
20
|
+
* agreements" → three searches); moving it away from the calls it explains
|
|
21
|
+
* turns a narrative into a pile. Outside, a multi-step turn reads as work,
|
|
22
|
+
* what it learned, work, what it learned, answer — the model's prose ends
|
|
23
|
+
* one of these rows and starts the next.
|
|
24
|
+
*
|
|
25
|
+
* Collapsed by default, for the reason the per-run fold this replaces had
|
|
26
|
+
* already found: in a real transcript the machinery dominates the answer it
|
|
27
|
+
* was meant to support, and the reader scrolls past it to reach the point.
|
|
28
|
+
*/
|
|
29
|
+
export declare function TurnActivity({ indices, isRunning, children, }: {
|
|
30
|
+
indices: readonly number[];
|
|
31
|
+
isRunning: boolean;
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useAuiState } from "@assistant-ui/react";
|
|
4
|
+
import { Loader2, CheckCircle2, XCircle, AlertCircle } from "lucide-react";
|
|
5
|
+
import { MessageDisclosure } from "./message-disclosure.js";
|
|
6
|
+
import { _detectErrorResult } from "./tool-fallback.js";
|
|
7
|
+
/**
|
|
8
|
+
* ONE row for everything a turn did before it answered.
|
|
9
|
+
*
|
|
10
|
+
* A turn that searched a corpus eleven times used to render eleven summary
|
|
11
|
+
* rows — one per adjacent run of tool calls — plus a reasoning row of its
|
|
12
|
+
* own, and the answer somewhere underneath. Each row was individually
|
|
13
|
+
* reasonable; stacked, they were a ladder the reader had to climb past to
|
|
14
|
+
* reach the point.
|
|
15
|
+
*
|
|
16
|
+
* So the machinery folds: "Worked · 20 tools · reasoning". The model's PROSE
|
|
17
|
+
* never goes inside — it is what the reader came for and it stays in the
|
|
18
|
+
* open, which also means each stretch of it ends one of these rows and
|
|
19
|
+
* starts the next. A turn that narrates its steps gets one row per step
|
|
20
|
+
* rather than one per tool run; a turn that just works and answers gets
|
|
21
|
+
* exactly one.
|
|
22
|
+
*
|
|
23
|
+
* Order is CHRONOLOGICAL, inside the fold and outside it. Reasoning explains
|
|
24
|
+
* why the next call happened ("I need the termination clause from both
|
|
25
|
+
* agreements" → three searches); moving it away from the calls it explains
|
|
26
|
+
* turns a narrative into a pile. Outside, a multi-step turn reads as work,
|
|
27
|
+
* what it learned, work, what it learned, answer — the model's prose ends
|
|
28
|
+
* one of these rows and starts the next.
|
|
29
|
+
*
|
|
30
|
+
* Collapsed by default, for the reason the per-run fold this replaces had
|
|
31
|
+
* already found: in a real transcript the machinery dominates the answer it
|
|
32
|
+
* was meant to support, and the reader scrolls past it to reach the point.
|
|
33
|
+
*/
|
|
34
|
+
export function TurnActivity({ indices, isRunning, children, }) {
|
|
35
|
+
const { tools, failed, hasReasoning } = useTurnOutcome(indices);
|
|
36
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
37
|
+
const summary = summarise({ tools, failed, hasReasoning, isRunning });
|
|
38
|
+
// Red only when EVERY call failed AND the turn has SETTLED. One failure
|
|
39
|
+
// among twenty is a turn that recovered, and painting it red would report a
|
|
40
|
+
// broken answer where there is a working one — but so would painting it red
|
|
41
|
+
// mid-run, when the only calls so far happen to have failed and the model is
|
|
42
|
+
// still reasoning its way to one that works. A partial failure still says
|
|
43
|
+
// so, in the count, without the alarm.
|
|
44
|
+
const allFailed = !isRunning && tools > 0 && failed === tools;
|
|
45
|
+
return (_jsx(MessageDisclosure, { open: isOpen, onOpenChange: setIsOpen, tone: allFailed ? "destructive" : "muted", icon: isRunning ? (_jsx(Loader2, { className: "size-3.5 animate-spin text-primary" })) : allFailed ? (_jsx(XCircle, { className: "size-3.5 text-destructive" })) : failed > 0 ? (_jsx(AlertCircle, { className: "size-3.5 text-muted-foreground" })) : (_jsx(CheckCircle2, { className: "size-3.5 text-primary" })), label: summary,
|
|
46
|
+
// The summary goes in the accessible name, not just the visible one.
|
|
47
|
+
// An `aria-label` REPLACES the name a button would otherwise derive
|
|
48
|
+
// from its contents, so a bare "Show what this turn did" would have
|
|
49
|
+
// announced the row while hiding the very thing it summarises — the
|
|
50
|
+
// call count, whether anything failed, whether there is thinking to
|
|
51
|
+
// read. A screen-reader user got strictly less than a sighted one.
|
|
52
|
+
ariaLabel: `${summary} — ${isOpen ? "hide" : "show"} details`, "data-turn-activity": true, children: _jsx("div", { className: "flex flex-col gap-2", children: children }) }));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The row's one line of text.
|
|
56
|
+
*
|
|
57
|
+
* Says what happened, in the order a reader asks it: is it still going, how
|
|
58
|
+
* much work was there, did any of it fail, and was there thinking to read.
|
|
59
|
+
* "Worked" alone when a turn only thought — claiming tools it never called
|
|
60
|
+
* would be a lie in the summary line, which is the one part nobody expands
|
|
61
|
+
* to check.
|
|
62
|
+
*
|
|
63
|
+
* This row describes its CONTENTS; it does not narrate the run. The phase a
|
|
64
|
+
* run is in belongs to the status indicator above the composer, which knows
|
|
65
|
+
* things this row cannot — which tool is in flight, whether the stream has
|
|
66
|
+
* stalled. A turn with reasoning and no calls yet therefore says "Reasoning"
|
|
67
|
+
* rather than "Thinking…", which is what the indicator is already saying two
|
|
68
|
+
* lines below; the same word twice on one screen reads as a stutter, not as
|
|
69
|
+
* two facts.
|
|
70
|
+
*
|
|
71
|
+
* No ellipses either: the spinner in the row's own icon slot carries "still
|
|
72
|
+
* going", and it does it without competing for the label.
|
|
73
|
+
*/
|
|
74
|
+
function summarise({ tools, failed, hasReasoning, isRunning, }) {
|
|
75
|
+
const toolText = tools === 1 ? "1 tool" : `${tools} tools`;
|
|
76
|
+
// 0 tools while running means the group holds nothing but reasoning —
|
|
77
|
+
// `useTurnOutcome` counts every non-reasoning part as a call — so naming it
|
|
78
|
+
// is accurate, not a guess.
|
|
79
|
+
if (isRunning)
|
|
80
|
+
return tools > 0 ? `Working · ${toolText}` : "Reasoning";
|
|
81
|
+
const parts = ["Worked"];
|
|
82
|
+
if (tools > 0)
|
|
83
|
+
parts.push(failed > 0 ? `${toolText} · ${failed} failed` : toolText);
|
|
84
|
+
if (hasReasoning)
|
|
85
|
+
parts.push("reasoning");
|
|
86
|
+
return parts.join(" · ");
|
|
87
|
+
}
|
|
88
|
+
/** What the turn actually contains, read from the message's own parts. */
|
|
89
|
+
function useTurnOutcome(indices) {
|
|
90
|
+
const parts = useAuiState((s) => s.message.parts);
|
|
91
|
+
let tools = 0;
|
|
92
|
+
let failed = 0;
|
|
93
|
+
let hasReasoning = false;
|
|
94
|
+
for (const index of indices) {
|
|
95
|
+
const part = parts?.[index];
|
|
96
|
+
if (!part)
|
|
97
|
+
continue;
|
|
98
|
+
if (part.type === "reasoning") {
|
|
99
|
+
hasReasoning = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Anything else under this node is a call the turn made. Counted by
|
|
103
|
+
// exclusion rather than by an allow-list of type names, so a new tool
|
|
104
|
+
// part type shows up in the count instead of vanishing from it.
|
|
105
|
+
tools++;
|
|
106
|
+
if (part.status?.type === "incomplete" || _detectErrorResult(part.result))
|
|
107
|
+
failed++;
|
|
108
|
+
}
|
|
109
|
+
return { tools, failed, hasReasoning };
|
|
110
|
+
}
|
package/dist/lib/nav-config.d.ts
CHANGED
|
@@ -67,6 +67,22 @@ export interface NavItem {
|
|
|
67
67
|
contextInstructions?: string;
|
|
68
68
|
/** Sub-navigation items (rendered indented under parent) */
|
|
69
69
|
children?: NavItem[];
|
|
70
|
+
/**
|
|
71
|
+
* The row's children have not been fetched yet, so `children` being empty
|
|
72
|
+
* says nothing about whether it has any.
|
|
73
|
+
*
|
|
74
|
+
* A tree row normally shows one of two markers: a chevron if it branches, a
|
|
75
|
+
* dot if it is a leaf. That is two states for a question with THREE answers
|
|
76
|
+
* — has children, has none, and nobody has looked. A caller that loads
|
|
77
|
+
* subtrees lazily (only the active one, say) leaves every other row with an
|
|
78
|
+
* empty `children`, and the row then claims to be a leaf on the strength of
|
|
79
|
+
* a fetch that never happened.
|
|
80
|
+
*
|
|
81
|
+
* Set this while the answer is unknown and the row shows neither marker: no
|
|
82
|
+
* chevron to click into nothing, no dot asserting an emptiness no one
|
|
83
|
+
* checked. Clear it once the children are known, empty or not.
|
|
84
|
+
*/
|
|
85
|
+
childrenUnloaded?: boolean;
|
|
70
86
|
/** Inline action buttons rendered next to the nav item (e.g., + document, + folder) */
|
|
71
87
|
actions?: NavItemAction[];
|
|
72
88
|
/** Optional drag-and-drop behavior (provided by feature modules) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React agent UI core for Foundry UI — chat, composer, AG-UI adapter for assistant-ui, tool cards, panels, sidebar, theme runtime, and UI stores.",
|
|
6
6
|
"keywords": [
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
"tailwind-merge": "^3.5.0",
|
|
71
71
|
"react-markdown": "^10.0.0",
|
|
72
72
|
"remark-gfm": "^4.0.0",
|
|
73
|
-
"@iloveagents/foundry-agent": "^0.
|
|
74
|
-
"@iloveagents/foundry-web-primitives": "^0.
|
|
73
|
+
"@iloveagents/foundry-agent": "^0.16.0",
|
|
74
|
+
"@iloveagents/foundry-web-primitives": "^0.16.0"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@ag-ui/client": "^0.0.52",
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
/**
|
|
3
|
-
* One row standing in for a run of adjacent tool calls, with the cards under
|
|
4
|
-
* it behind a disclosure.
|
|
5
|
-
*
|
|
6
|
-
* A turn that calls eight tools used to render eight bordered cards with no
|
|
7
|
-
* way to fold them away. The run is now one summary line — "8 tools · done" —
|
|
8
|
-
* over a group the reader can collapse when the machinery is in the way.
|
|
9
|
-
*
|
|
10
|
-
* COLLAPSED by default. This was open at first, on the reasoning that a
|
|
11
|
-
* developer watching an agent work wants the detail — but in a real
|
|
12
|
-
* transcript the cards dominate the answer they were supposed to support,
|
|
13
|
-
* and the reader scrolls past machinery to reach the point. The summary line
|
|
14
|
-
* already carries what matters at a glance (how many calls, and whether they
|
|
15
|
-
* worked); opening it is the deliberate act.
|
|
16
|
-
*
|
|
17
|
-
* The row shape is shared with the reasoning part via `MessageDisclosure`, so
|
|
18
|
-
* the two foldaways in a message read as one family instead of two widgets.
|
|
19
|
-
*
|
|
20
|
-
* RED only when the whole run failed. One failure among eight successful
|
|
21
|
-
* calls is not a red turn — the agent recovered, and painting the summary red
|
|
22
|
-
* would report a broken run where there was a working one. A partial failure
|
|
23
|
-
* still says so, in the count, without the alarm.
|
|
24
|
-
*/
|
|
25
|
-
export declare function ToolRunGroup({ indices, isRunning, children, }: {
|
|
26
|
-
indices: readonly number[];
|
|
27
|
-
isRunning: boolean;
|
|
28
|
-
children: ReactNode;
|
|
29
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import { useAuiState } from "@assistant-ui/react";
|
|
4
|
-
import { Loader2, CheckCircle2, XCircle, AlertCircle } from "lucide-react";
|
|
5
|
-
import { MessageDisclosure } from "./message-disclosure.js";
|
|
6
|
-
import { _detectErrorResult } from "./tool-fallback.js";
|
|
7
|
-
/**
|
|
8
|
-
* One row standing in for a run of adjacent tool calls, with the cards under
|
|
9
|
-
* it behind a disclosure.
|
|
10
|
-
*
|
|
11
|
-
* A turn that calls eight tools used to render eight bordered cards with no
|
|
12
|
-
* way to fold them away. The run is now one summary line — "8 tools · done" —
|
|
13
|
-
* over a group the reader can collapse when the machinery is in the way.
|
|
14
|
-
*
|
|
15
|
-
* COLLAPSED by default. This was open at first, on the reasoning that a
|
|
16
|
-
* developer watching an agent work wants the detail — but in a real
|
|
17
|
-
* transcript the cards dominate the answer they were supposed to support,
|
|
18
|
-
* and the reader scrolls past machinery to reach the point. The summary line
|
|
19
|
-
* already carries what matters at a glance (how many calls, and whether they
|
|
20
|
-
* worked); opening it is the deliberate act.
|
|
21
|
-
*
|
|
22
|
-
* The row shape is shared with the reasoning part via `MessageDisclosure`, so
|
|
23
|
-
* the two foldaways in a message read as one family instead of two widgets.
|
|
24
|
-
*
|
|
25
|
-
* RED only when the whole run failed. One failure among eight successful
|
|
26
|
-
* calls is not a red turn — the agent recovered, and painting the summary red
|
|
27
|
-
* would report a broken run where there was a working one. A partial failure
|
|
28
|
-
* still says so, in the count, without the alarm.
|
|
29
|
-
*/
|
|
30
|
-
export function ToolRunGroup({ indices, isRunning, children, }) {
|
|
31
|
-
const { total, failed } = useToolRunOutcome(indices);
|
|
32
|
-
// Every call in the run failed — the only state that earns the alarm colour.
|
|
33
|
-
const allFailed = total > 0 && failed === total;
|
|
34
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
35
|
-
const label = isRunning
|
|
36
|
-
? `Using ${total === 1 ? "1 tool" : `${total} tools`}…`
|
|
37
|
-
: failed > 0
|
|
38
|
-
? `${total === 1 ? "1 tool" : `${total} tools`} · ${failed} failed`
|
|
39
|
-
: `${total === 1 ? "1 tool" : `${total} tools`} · done`;
|
|
40
|
-
return (_jsx(MessageDisclosure, { open: isOpen, onOpenChange: setIsOpen, tone: allFailed ? "destructive" : "muted", icon: isRunning ? (_jsx(Loader2, { className: "size-3.5 animate-spin text-primary" })) : allFailed ? (_jsx(XCircle, { className: "size-3.5 text-destructive" })) : failed > 0 ? (
|
|
41
|
-
// Partial: the run produced a result, some of it the hard way.
|
|
42
|
-
_jsx(AlertCircle, { className: "size-3.5 text-muted-foreground" })) : (_jsx(CheckCircle2, { className: "size-3.5 text-primary" })), label: label, "data-tool-run-group": true, children: _jsx("div", { className: "flex flex-col gap-2", children: children }) }));
|
|
43
|
-
}
|
|
44
|
-
function useToolRunOutcome(indices) {
|
|
45
|
-
const parts = useAuiState((s) => s.message.parts);
|
|
46
|
-
let failed = 0;
|
|
47
|
-
for (const index of indices) {
|
|
48
|
-
const part = parts?.[index];
|
|
49
|
-
if (!part)
|
|
50
|
-
continue;
|
|
51
|
-
const status = part.status;
|
|
52
|
-
if (status?.type === "incomplete" || _detectErrorResult(part.result))
|
|
53
|
-
failed++;
|
|
54
|
-
}
|
|
55
|
-
return { total: indices.length, failed };
|
|
56
|
-
}
|