@brainpilot/web 0.0.6 → 0.0.8
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/assets/index-162Pskp8.js +438 -0
- package/dist/assets/index-DWOsU22G.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +3 -3
- package/src/__tests__/chatScrollBehavior.test.ts +48 -0
- package/src/__tests__/composerSendTools.test.tsx +37 -0
- package/src/__tests__/demoConversation.test.ts +25 -2
- package/src/__tests__/internalToolStrip.test.ts +108 -0
- package/src/__tests__/sidebarResize.test.ts +46 -0
- package/src/components/chat/ComposerSendTools.tsx +31 -0
- package/src/components/chat/MessageStream.tsx +7 -0
- package/src/components/chat/PromptComposer.tsx +65 -134
- package/src/components/demo/DemoView.tsx +11 -4
- package/src/components/shell/DesktopShell.tsx +49 -12
- package/src/components/shell/sidebarResize.ts +49 -0
- package/src/components/sidebar/SessionList.tsx +127 -0
- package/src/components/sidebar/Sidebar.tsx +92 -100
- package/src/contexts/SessionContext.tsx +44 -1
- package/src/contexts/messageGroups.ts +56 -0
- package/src/contexts/messageReducer.ts +4 -0
- package/src/i18n/messages/shell.ts +2 -0
- package/src/styles/global.css +85 -15
- package/dist/assets/index-Br55rkHb.css +0 -1
- package/dist/assets/index-CeUzk-ej.js +0 -445
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
MessageCircle,
|
|
4
|
-
MessageSquarePlus,
|
|
2
|
+
MessagesSquare,
|
|
5
3
|
MonitorPlay,
|
|
6
4
|
PanelLeft,
|
|
7
5
|
PenLine,
|
|
8
|
-
Search,
|
|
9
6
|
Settings,
|
|
10
|
-
Trash2,
|
|
11
|
-
X,
|
|
12
7
|
} from "lucide-react";
|
|
13
|
-
import {
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
14
9
|
import { useSessions } from "../../contexts/SessionContext";
|
|
15
10
|
import { useT } from "../../i18n/useT";
|
|
16
11
|
import { IconButton } from "../primitives/IconButton";
|
|
12
|
+
import { SessionList } from "./SessionList";
|
|
17
13
|
|
|
18
14
|
type SidebarProps = {
|
|
19
15
|
isCollapsed: boolean;
|
|
@@ -37,20 +33,46 @@ export function Sidebar({ isCollapsed, activePage, onOpenDemo, onGoWorkspace, on
|
|
|
37
33
|
deleteSession,
|
|
38
34
|
} = useSessions();
|
|
39
35
|
const t = useT();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
// #131 — when collapsed to the icon rail, the session list moves into a
|
|
37
|
+
// floating popover opened from a single icon, so it no longer competes for
|
|
38
|
+
// horizontal space yet stays one click away.
|
|
39
|
+
const [isSessionsPopoverOpen, setIsSessionsPopoverOpen] = useState(false);
|
|
40
|
+
const sessionsPopoverRef = useRef<HTMLDivElement | null>(null);
|
|
43
41
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
setEditingId(null);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
await updateSessionTitle(editingId, editingTitle.trim());
|
|
51
|
-
setEditingId(null);
|
|
42
|
+
const newConversation = () => {
|
|
43
|
+
onGoWorkspace();
|
|
44
|
+
startDraftSession();
|
|
52
45
|
};
|
|
53
46
|
|
|
47
|
+
const selectAndGo = (sessionId: string) => {
|
|
48
|
+
onGoWorkspace();
|
|
49
|
+
selectSession(sessionId);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Collapsing the rail (manually or at narrow widths) closes a stale popover.
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!isCollapsed) setIsSessionsPopoverOpen(false);
|
|
55
|
+
}, [isCollapsed]);
|
|
56
|
+
|
|
57
|
+
// Dismiss the popover on outside click / Escape, like a standard menu.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!isSessionsPopoverOpen) return;
|
|
60
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
61
|
+
if (!sessionsPopoverRef.current?.contains(event.target as Node)) {
|
|
62
|
+
setIsSessionsPopoverOpen(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
66
|
+
if (event.key === "Escape") setIsSessionsPopoverOpen(false);
|
|
67
|
+
};
|
|
68
|
+
window.addEventListener("pointerdown", onPointerDown);
|
|
69
|
+
window.addEventListener("keydown", onKeyDown);
|
|
70
|
+
return () => {
|
|
71
|
+
window.removeEventListener("pointerdown", onPointerDown);
|
|
72
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
73
|
+
};
|
|
74
|
+
}, [isSessionsPopoverOpen]);
|
|
75
|
+
|
|
54
76
|
return (
|
|
55
77
|
<aside className="sidebar" aria-label={t("sidebar.aria.nav")}>
|
|
56
78
|
<div
|
|
@@ -74,27 +96,54 @@ export function Sidebar({ isCollapsed, activePage, onOpenDemo, onGoWorkspace, on
|
|
|
74
96
|
</div>
|
|
75
97
|
|
|
76
98
|
<nav className="sidebar__nav" aria-label={t("sidebar.aria.primary")}>
|
|
77
|
-
<button className="nav-item nav-item--strong" onClick={
|
|
99
|
+
<button className="nav-item nav-item--strong" onClick={newConversation} type="button" title={t("sidebar.newChat")}>
|
|
78
100
|
<PenLine size={16} />
|
|
79
101
|
<span>{t("sidebar.newChat")}</span>
|
|
80
102
|
</button>
|
|
81
103
|
{/*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Plug / Clock3 lucide imports when restoring these.
|
|
86
|
-
<button className="nav-item" type="button">
|
|
87
|
-
<Plug size={16} />
|
|
88
|
-
<span>{t("sidebar.plugins")}</span>
|
|
89
|
-
</button>
|
|
90
|
-
<button className="nav-item" type="button">
|
|
91
|
-
<Clock3 size={16} />
|
|
92
|
-
<span>{t("sidebar.automations")}</span>
|
|
93
|
-
</button>
|
|
104
|
+
#131 — collapsed icon rail: a single Sessions icon opens the session
|
|
105
|
+
list in a popover (the inline list below is hidden when collapsed).
|
|
106
|
+
Rendered only in the rail so the expanded sidebar keeps its full list.
|
|
94
107
|
*/}
|
|
108
|
+
{isCollapsed ? (
|
|
109
|
+
<div className="sidebar__sessions-popover-anchor" ref={sessionsPopoverRef}>
|
|
110
|
+
<button
|
|
111
|
+
aria-expanded={isSessionsPopoverOpen}
|
|
112
|
+
aria-haspopup="menu"
|
|
113
|
+
className={`nav-item ${isSessionsPopoverOpen ? "is-active" : ""}`}
|
|
114
|
+
onClick={() => setIsSessionsPopoverOpen((open) => !open)}
|
|
115
|
+
title={t("sidebar.conversations")}
|
|
116
|
+
type="button"
|
|
117
|
+
>
|
|
118
|
+
<MessagesSquare size={16} />
|
|
119
|
+
<span>{t("sidebar.conversations")}</span>
|
|
120
|
+
</button>
|
|
121
|
+
{isSessionsPopoverOpen ? (
|
|
122
|
+
<div className="sidebar__sessions-popover" role="menu" aria-label={t("sidebar.conversations")}>
|
|
123
|
+
<div className="sidebar__sessions-popover-head">
|
|
124
|
+
<h2>{t("sidebar.conversations")}</h2>
|
|
125
|
+
<button className="nav-item nav-item--strong" onClick={() => { newConversation(); setIsSessionsPopoverOpen(false); }} type="button">
|
|
126
|
+
<PenLine size={14} />
|
|
127
|
+
<span>{t("sidebar.newChat")}</span>
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
<SessionList
|
|
131
|
+
sessions={sessions}
|
|
132
|
+
currentId={currentSession?.id}
|
|
133
|
+
isLoading={isLoading}
|
|
134
|
+
onSelect={(id) => { selectAndGo(id); setIsSessionsPopoverOpen(false); }}
|
|
135
|
+
onRename={updateSessionTitle}
|
|
136
|
+
onDelete={deleteSession}
|
|
137
|
+
onOpenSearch={() => { onOpenSearch(); setIsSessionsPopoverOpen(false); }}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
) : null}
|
|
141
|
+
</div>
|
|
142
|
+
) : null}
|
|
95
143
|
<button
|
|
96
144
|
className={`nav-item ${activePage === "demo" ? "is-active" : ""}`}
|
|
97
145
|
onClick={onOpenDemo}
|
|
146
|
+
title={t("sidebar.demo")}
|
|
98
147
|
type="button"
|
|
99
148
|
>
|
|
100
149
|
<MonitorPlay size={16} />
|
|
@@ -106,82 +155,25 @@ export function Sidebar({ isCollapsed, activePage, onOpenDemo, onGoWorkspace, on
|
|
|
106
155
|
<div className="section-heading">
|
|
107
156
|
<h2 id="conversations-heading">{t("sidebar.conversations")}</h2>
|
|
108
157
|
<div className="section-heading__actions">
|
|
109
|
-
<IconButton label={t("sidebar.aria.newConversation")} onClick={
|
|
110
|
-
<
|
|
158
|
+
<IconButton label={t("sidebar.aria.newConversation")} onClick={newConversation}>
|
|
159
|
+
<PenLine size={13} />
|
|
111
160
|
</IconButton>
|
|
112
161
|
</div>
|
|
113
162
|
</div>
|
|
114
163
|
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{
|
|
123
|
-
|
|
124
|
-
const isConfirming = confirmDeleteId === session.id;
|
|
125
|
-
return (
|
|
126
|
-
<div className={`conversation-item ${currentSession?.id === session.id ? "is-active" : ""}`} key={session.id}>
|
|
127
|
-
{isEditing ? (
|
|
128
|
-
<form className="conversation-edit" onSubmit={submitRename}>
|
|
129
|
-
<input
|
|
130
|
-
autoFocus
|
|
131
|
-
onChange={(event) => setEditingTitle(event.target.value)}
|
|
132
|
-
value={editingTitle}
|
|
133
|
-
/>
|
|
134
|
-
<IconButton label={t("sidebar.aria.saveTitle")} type="submit">
|
|
135
|
-
<Check size={14} />
|
|
136
|
-
</IconButton>
|
|
137
|
-
<IconButton label={t("sidebar.aria.cancelRename")} onClick={() => setEditingId(null)}>
|
|
138
|
-
<X size={14} />
|
|
139
|
-
</IconButton>
|
|
140
|
-
</form>
|
|
141
|
-
) : (
|
|
142
|
-
<>
|
|
143
|
-
<button className="conversation-row" onClick={() => { onGoWorkspace(); selectSession(session.id); }} type="button">
|
|
144
|
-
<MessageCircle size={16} />
|
|
145
|
-
<span>{session.title}</span>
|
|
146
|
-
<small>{new Date(session.updatedAt).toLocaleDateString()}</small>
|
|
147
|
-
</button>
|
|
148
|
-
<div className="conversation-actions">
|
|
149
|
-
{isConfirming ? (
|
|
150
|
-
<>
|
|
151
|
-
<IconButton label={t("sidebar.aria.confirmDelete")} onClick={() => void deleteSession(session.id)}>
|
|
152
|
-
<Check size={14} />
|
|
153
|
-
</IconButton>
|
|
154
|
-
<IconButton label={t("sidebar.aria.cancelDelete")} onClick={() => setConfirmDeleteId(null)}>
|
|
155
|
-
<X size={14} />
|
|
156
|
-
</IconButton>
|
|
157
|
-
</>
|
|
158
|
-
) : (
|
|
159
|
-
<>
|
|
160
|
-
<IconButton
|
|
161
|
-
label={t("sidebar.aria.rename")}
|
|
162
|
-
onClick={() => {
|
|
163
|
-
setEditingId(session.id);
|
|
164
|
-
setEditingTitle(session.title);
|
|
165
|
-
}}
|
|
166
|
-
>
|
|
167
|
-
<PenLine size={14} />
|
|
168
|
-
</IconButton>
|
|
169
|
-
<IconButton label={t("sidebar.aria.delete")} onClick={() => setConfirmDeleteId(session.id)}>
|
|
170
|
-
<Trash2 size={14} />
|
|
171
|
-
</IconButton>
|
|
172
|
-
</>
|
|
173
|
-
)}
|
|
174
|
-
</div>
|
|
175
|
-
</>
|
|
176
|
-
)}
|
|
177
|
-
</div>
|
|
178
|
-
);
|
|
179
|
-
})}
|
|
180
|
-
</div>
|
|
164
|
+
<SessionList
|
|
165
|
+
sessions={sessions}
|
|
166
|
+
currentId={currentSession?.id}
|
|
167
|
+
isLoading={isLoading}
|
|
168
|
+
onSelect={selectAndGo}
|
|
169
|
+
onRename={updateSessionTitle}
|
|
170
|
+
onDelete={deleteSession}
|
|
171
|
+
onOpenSearch={onOpenSearch}
|
|
172
|
+
/>
|
|
181
173
|
</section>
|
|
182
174
|
|
|
183
175
|
<div className="sidebar__footer">
|
|
184
|
-
<button className="nav-item" onClick={onOpenSettings} type="button">
|
|
176
|
+
<button className="nav-item" onClick={onOpenSettings} type="button" title={t("sidebar.settings")}>
|
|
185
177
|
<Settings size={16} />
|
|
186
178
|
<span>{t("sidebar.settings")}</span>
|
|
187
179
|
</button>
|
|
@@ -55,6 +55,14 @@ interface SessionContextValue {
|
|
|
55
55
|
agentFilters: Record<string, AgentMessageFilter>;
|
|
56
56
|
/** Live Graph of Trace for the current session (#79), or null if none/unloaded. */
|
|
57
57
|
currentTrace: TraceGraph | null;
|
|
58
|
+
/**
|
|
59
|
+
* #134 — whether the current session has trace updates the user hasn't seen
|
|
60
|
+
* since last opening the Trace view. Drives a quiet dot on the Trace tab so
|
|
61
|
+
* trace stays a transparency layer instead of noisy chat output. Per-session:
|
|
62
|
+
* switching sessions reflects that session's unread state. False on initial
|
|
63
|
+
* hydration (only live post-open trace_node events set it).
|
|
64
|
+
*/
|
|
65
|
+
traceUnread: boolean;
|
|
58
66
|
/** Re-seed the trace graph from the HTTP route (manual refresh). */
|
|
59
67
|
refreshTrace: (sessionId: string) => Promise<void>;
|
|
60
68
|
selectSession: (sessionId: string) => void;
|
|
@@ -185,6 +193,14 @@ export function SessionProvider({ children }: { children: ReactNode }) {
|
|
|
185
193
|
const [isRefreshingMessages, setIsRefreshingMessages] = useState(false);
|
|
186
194
|
const [error, setError] = useState<string | null>(null);
|
|
187
195
|
const [currentView, setCurrentView] = useState<"chat" | "agents" | "trace">("chat");
|
|
196
|
+
// #134 — read currentView inside the SSE queue-drain effect (keyed on
|
|
197
|
+
// session/tick, not view) to decide whether an incoming trace update should
|
|
198
|
+
// raise the unread dot. A live trace_node that arrives while the user is
|
|
199
|
+
// already on the Trace view is "seen", so it must not flag unread.
|
|
200
|
+
const currentViewRef = useRef<"chat" | "agents" | "trace">("chat");
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
currentViewRef.current = currentView;
|
|
203
|
+
}, [currentView]);
|
|
188
204
|
// Unsent textarea drafts live in a module-level store (see contexts/draftStore.ts)
|
|
189
205
|
// so keystrokes don't re-render the whole chat subtree. Drafts are keyed by
|
|
190
206
|
// session id and survive PromptComposer unmount (tab switches).
|
|
@@ -201,6 +217,11 @@ export function SessionProvider({ children }: { children: ReactNode }) {
|
|
|
201
217
|
// #79: live Graph of Trace per session. Seeded by a fetch on session change,
|
|
202
218
|
// then kept live by CUSTOM:trace_node SSE events (see the queue drain below).
|
|
203
219
|
const [traceBySession, setTraceBySession] = useState<Record<string, TraceGraph>>({});
|
|
220
|
+
// #134 — per-session "trace changed since you last looked" flag. Set only by
|
|
221
|
+
// live CUSTOM:trace_node events while the user is NOT on the Trace view;
|
|
222
|
+
// cleared when they open Trace for that session. Hydration/seed paths never
|
|
223
|
+
// set it, so a freshly-opened session with existing trace shows no false dot.
|
|
224
|
+
const [traceUnreadBySession, setTraceUnreadBySession] = useState<Record<string, boolean>>({});
|
|
204
225
|
|
|
205
226
|
|
|
206
227
|
const currentSession = useMemo(
|
|
@@ -802,7 +823,17 @@ export function SessionProvider({ children }: { children: ReactNode }) {
|
|
|
802
823
|
for (const event of queue) {
|
|
803
824
|
graph = reduceTraceForEvent(graph, event, sid);
|
|
804
825
|
}
|
|
805
|
-
|
|
826
|
+
if (graph && graph !== start) {
|
|
827
|
+
// #134 — a live trace update landed. Raise the per-session unread dot
|
|
828
|
+
// unless the user is already looking at this session's Trace view (then
|
|
829
|
+
// it's seen). Hydration/seed go through other code paths, so this only
|
|
830
|
+
// ever fires for genuine post-open SSE trace_node events.
|
|
831
|
+
if (currentViewRef.current !== "trace") {
|
|
832
|
+
setTraceUnreadBySession((u) => (u[sid] ? u : { ...u, [sid]: true }));
|
|
833
|
+
}
|
|
834
|
+
return { ...current, [sid]: graph };
|
|
835
|
+
}
|
|
836
|
+
return current;
|
|
806
837
|
});
|
|
807
838
|
|
|
808
839
|
// Process all events through the message reducer.
|
|
@@ -842,6 +873,16 @@ export function SessionProvider({ children }: { children: ReactNode }) {
|
|
|
842
873
|
[currentSessionId, traceBySession],
|
|
843
874
|
);
|
|
844
875
|
|
|
876
|
+
// #134 — opening the Trace view for a session marks its trace as seen, so the
|
|
877
|
+
// tab dot clears. Keyed on (session, view) so it also clears when a trace
|
|
878
|
+
// update arrives while already on the view (the drain guards that case too).
|
|
879
|
+
useEffect(() => {
|
|
880
|
+
if (currentView !== "trace" || !currentSessionId) return;
|
|
881
|
+
setTraceUnreadBySession((u) => (u[currentSessionId] ? { ...u, [currentSessionId]: false } : u));
|
|
882
|
+
}, [currentView, currentSessionId]);
|
|
883
|
+
|
|
884
|
+
const traceUnread = currentSessionId ? (traceUnreadBySession[currentSessionId] ?? false) : false;
|
|
885
|
+
|
|
845
886
|
const value = useMemo(
|
|
846
887
|
() => ({
|
|
847
888
|
sessions,
|
|
@@ -859,6 +900,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
|
|
|
859
900
|
tokenUsage,
|
|
860
901
|
agentFilters,
|
|
861
902
|
currentTrace,
|
|
903
|
+
traceUnread,
|
|
862
904
|
refreshTrace,
|
|
863
905
|
selectSession,
|
|
864
906
|
createSession,
|
|
@@ -890,6 +932,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
|
|
|
890
932
|
tokenUsage,
|
|
891
933
|
agentFilters,
|
|
892
934
|
currentTrace,
|
|
935
|
+
traceUnread,
|
|
893
936
|
refreshTrace,
|
|
894
937
|
selectSession,
|
|
895
938
|
createSession,
|
|
@@ -9,6 +9,60 @@ export type RenderItem =
|
|
|
9
9
|
| { type: "single"; message: ChatMessage }
|
|
10
10
|
| { type: "activity"; id: string; steps: ChatMessage[]; streaming: boolean };
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* #134 — tool visibility model. Internal tools are part of the agent's plumbing
|
|
14
|
+
* (trace bookkeeping) rather than the user-facing conversation: the model still
|
|
15
|
+
* sees their calls and results, but the chat UI hides both so an implementation
|
|
16
|
+
* detail (`trace event dispatched`) never surfaces above the Principal's reply.
|
|
17
|
+
* Trace changes are surfaced quietly via the Trace tab badge instead.
|
|
18
|
+
*
|
|
19
|
+
* Hard-coded for the current internal toolset; promote to a richer
|
|
20
|
+
* `ToolVisibility = "user" | "debug" | "internal"` map if/when more land.
|
|
21
|
+
*/
|
|
22
|
+
const INTERNAL_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
23
|
+
"record_trace",
|
|
24
|
+
"create_trace_node",
|
|
25
|
+
"update_trace_node",
|
|
26
|
+
"add_trace_relation",
|
|
27
|
+
"get_trace_graph",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/** A tool name is internal if it matches bare or mcp-namespaced (server__tool). */
|
|
31
|
+
export function isInternalToolName(name: string | undefined): boolean {
|
|
32
|
+
if (!name) return false;
|
|
33
|
+
if (INTERNAL_TOOL_NAMES.has(name)) return true;
|
|
34
|
+
const bare = name.includes("__") ? name.slice(name.lastIndexOf("__") + 2) : name;
|
|
35
|
+
return INTERNAL_TOOL_NAMES.has(bare);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Drop internal-tool calls AND their matching results from the chat stream.
|
|
40
|
+
*
|
|
41
|
+
* A TOOL_CALL_START carries the tool name; the later TOOL_CALL_RESULT carries
|
|
42
|
+
* only a `toolCallId` linking back to it. So we first collect the call ids of
|
|
43
|
+
* every internal tool, then filter out both the call message and any tool
|
|
44
|
+
* message whose `toolCallId` (the result) points at one. Presentation-only:
|
|
45
|
+
* the underlying message list and the model's view are untouched.
|
|
46
|
+
*/
|
|
47
|
+
export function stripInternalToolMessages(messages: ChatMessage[]): ChatMessage[] {
|
|
48
|
+
const internalCallIds = new Set<string>();
|
|
49
|
+
for (const m of messages) {
|
|
50
|
+
if (m.kind === "tool" && isInternalToolName(m.toolName)) {
|
|
51
|
+
internalCallIds.add(m.id);
|
|
52
|
+
if (m.toolCallId) internalCallIds.add(m.toolCallId);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (internalCallIds.size === 0) return messages;
|
|
56
|
+
return messages.filter((m) => {
|
|
57
|
+
if (m.kind !== "tool") return true;
|
|
58
|
+
// The call itself (internal tool name) → drop.
|
|
59
|
+
if (isInternalToolName(m.toolName)) return false;
|
|
60
|
+
// The result, linked by toolCallId to an internal call → drop.
|
|
61
|
+
if (m.toolCallId && internalCallIds.has(m.toolCallId)) return false;
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
12
66
|
/**
|
|
13
67
|
* Standalone kinds render as their own visible card. All assistant text
|
|
14
68
|
* messages — whether they come from the Principal or an Expert agent, and
|
|
@@ -49,6 +103,8 @@ export function buildRenderItems(
|
|
|
49
103
|
runningAgents?: ReadonlySet<string>,
|
|
50
104
|
): RenderItem[] {
|
|
51
105
|
const items: RenderItem[] = [];
|
|
106
|
+
// #134 — internal tools (trace bookkeeping) are hidden from the chat UI.
|
|
107
|
+
messages = stripInternalToolMessages(messages);
|
|
52
108
|
let buffer: ChatMessage[] = [];
|
|
53
109
|
// A step keeps its block "in progress" while its owning agent's run is active.
|
|
54
110
|
// Steps default to the principal agent when unattributed, matching how the
|
|
@@ -258,6 +258,10 @@ export function reduceMessagesForEvent(existing: ChatMessage[], event: WebSocket
|
|
|
258
258
|
agent,
|
|
259
259
|
kind: "tool",
|
|
260
260
|
toolResult: content,
|
|
261
|
+
// #134 — keep the link back to the originating TOOL_CALL_START so the
|
|
262
|
+
// UI can suppress results of internal tools (record_trace) whose name
|
|
263
|
+
// only rode on the call event, not on this result.
|
|
264
|
+
toolCallId: event.toolCallId,
|
|
261
265
|
},
|
|
262
266
|
];
|
|
263
267
|
}
|
|
@@ -11,6 +11,7 @@ export default defineMessages(
|
|
|
11
11
|
"shell.view.chat": "对话",
|
|
12
12
|
"shell.view.agents": "智能体",
|
|
13
13
|
"shell.view.trace": "轨迹",
|
|
14
|
+
"shell.view.traceUpdated": "轨迹有新更新",
|
|
14
15
|
"shell.aria.refreshMessages": "刷新消息",
|
|
15
16
|
"shell.terminal.minimize": "最小化终端",
|
|
16
17
|
"shell.terminal.open": "打开终端",
|
|
@@ -28,6 +29,7 @@ export default defineMessages(
|
|
|
28
29
|
"shell.view.chat": "Chat",
|
|
29
30
|
"shell.view.agents": "Agents",
|
|
30
31
|
"shell.view.trace": "Trace",
|
|
32
|
+
"shell.view.traceUpdated": "Trace has new updates",
|
|
31
33
|
"shell.aria.refreshMessages": "Refresh messages",
|
|
32
34
|
"shell.terminal.minimize": "Minimize terminal",
|
|
33
35
|
"shell.terminal.open": "Open terminal",
|
package/src/styles/global.css
CHANGED
|
@@ -678,6 +678,24 @@ button {
|
|
|
678
678
|
padding: 0;
|
|
679
679
|
}
|
|
680
680
|
|
|
681
|
+
/* #134: a tab that can carry a quiet unread dot (Trace). Positioned so the dot
|
|
682
|
+
sits at the top-right corner of the icon without affecting layout. */
|
|
683
|
+
.workspace-view-tab--badged {
|
|
684
|
+
position: relative;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.workspace-view-tab__badge {
|
|
688
|
+
position: absolute;
|
|
689
|
+
top: 3px;
|
|
690
|
+
right: 3px;
|
|
691
|
+
width: 6px;
|
|
692
|
+
height: 6px;
|
|
693
|
+
border-radius: 999px;
|
|
694
|
+
background: var(--color-danger, #e5484d);
|
|
695
|
+
box-shadow: 0 0 0 2px var(--color-surface-soft);
|
|
696
|
+
pointer-events: none;
|
|
697
|
+
}
|
|
698
|
+
|
|
681
699
|
.session-title {
|
|
682
700
|
display: flex;
|
|
683
701
|
min-width: 0;
|
|
@@ -1849,20 +1867,11 @@ button {
|
|
|
1849
1867
|
}
|
|
1850
1868
|
|
|
1851
1869
|
@media (max-width: 860px) {
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
gap: 16px;
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
.section-heading,
|
|
1861
|
-
.conversation-stack,
|
|
1862
|
-
.nav-item span {
|
|
1863
|
-
display: none;
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1870
|
+
/* #131 — the rail layout at narrow widths is now driven by React
|
|
1871
|
+
(DesktopShell auto-collapses below this breakpoint and applies
|
|
1872
|
+
.desktop-shell--sidebar-collapsed), so the sidebar styling lives in those
|
|
1873
|
+
class rules and the session list is reachable via the rail popover instead
|
|
1874
|
+
of being hidden outright. Only non-sidebar responsive tweaks remain here. */
|
|
1866
1875
|
.prompt-home {
|
|
1867
1876
|
padding-inline: 18px;
|
|
1868
1877
|
}
|
|
@@ -2091,7 +2100,12 @@ button {
|
|
|
2091
2100
|
overscroll-behavior: contain;
|
|
2092
2101
|
padding: 4px 14px 20px 4px;
|
|
2093
2102
|
scrollbar-gutter: stable;
|
|
2094
|
-
scroll-behavior: smooth
|
|
2103
|
+
/* #133: NO `scroll-behavior: smooth` here. The chat stream restores its
|
|
2104
|
+
position imperatively (`scrollTop = …`) on tab-switch remount and on
|
|
2105
|
+
pinned-bottom live append; a global smooth behavior turns those instant
|
|
2106
|
+
jumps into a visible top-to-bottom replay through the history. Smooth
|
|
2107
|
+
scrolling, if ever wanted, must be opt-in around an explicit
|
|
2108
|
+
user-triggered jump, never the default for the container. */
|
|
2095
2109
|
scrollbar-color: var(--color-border) transparent;
|
|
2096
2110
|
scrollbar-width: thin;
|
|
2097
2111
|
}
|
|
@@ -3013,6 +3027,62 @@ button {
|
|
|
3013
3027
|
opacity: 0;
|
|
3014
3028
|
}
|
|
3015
3029
|
|
|
3030
|
+
/* #131 — collapsed icon-rail session popover. The rail hides the inline session
|
|
3031
|
+
section (rule above) and squeezes nav labels to opacity:0; the popover is a
|
|
3032
|
+
floating panel anchored to the Sessions icon that restores the full list. */
|
|
3033
|
+
.sidebar__sessions-popover-anchor {
|
|
3034
|
+
position: relative;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
.sidebar__sessions-popover {
|
|
3038
|
+
position: absolute;
|
|
3039
|
+
top: 0;
|
|
3040
|
+
left: calc(100% + 10px);
|
|
3041
|
+
z-index: 40;
|
|
3042
|
+
display: flex;
|
|
3043
|
+
width: 300px;
|
|
3044
|
+
max-height: min(70vh, 560px);
|
|
3045
|
+
flex-direction: column;
|
|
3046
|
+
gap: 8px;
|
|
3047
|
+
padding: 12px;
|
|
3048
|
+
border: 1px solid var(--color-border);
|
|
3049
|
+
border-radius: var(--radius-md);
|
|
3050
|
+
background: var(--color-surface-raised, var(--color-surface));
|
|
3051
|
+
box-shadow: var(--shadow-md, 0 12px 32px rgba(0, 0, 0, 0.18));
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
.sidebar__sessions-popover .conversation-stack {
|
|
3055
|
+
display: grid;
|
|
3056
|
+
min-height: 0;
|
|
3057
|
+
overflow-y: auto;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
.sidebar__sessions-popover-head {
|
|
3061
|
+
display: flex;
|
|
3062
|
+
align-items: center;
|
|
3063
|
+
justify-content: space-between;
|
|
3064
|
+
gap: 8px;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
.sidebar__sessions-popover-head h2 {
|
|
3068
|
+
margin: 0;
|
|
3069
|
+
color: var(--color-text-subtle);
|
|
3070
|
+
font-size: 13px;
|
|
3071
|
+
font-weight: 500;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
/* The popover lives inside the collapsed rail, so undo the rail's label
|
|
3075
|
+
squeeze/hide for everything inside it — its content is fully expanded. */
|
|
3076
|
+
.desktop-shell--sidebar-collapsed .sidebar__sessions-popover .nav-item span,
|
|
3077
|
+
.desktop-shell--sidebar-collapsed .sidebar__sessions-popover .conversation-row span,
|
|
3078
|
+
.desktop-shell--sidebar-collapsed .sidebar__sessions-popover .conversation-row small {
|
|
3079
|
+
opacity: 1;
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
.desktop-shell--sidebar-collapsed .sidebar__sessions-popover .conversation-stack {
|
|
3083
|
+
display: grid;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3016
3086
|
.sandbox-status {
|
|
3017
3087
|
position: relative;
|
|
3018
3088
|
display: inline-flex;
|