@steerable/agent-shell-web 0.6.27 → 0.6.28
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/package.json +4 -4
- package/src/components/AgentSidebar.tsx +24 -24
- package/src/components/BrandLockup.tsx +1 -1
- package/src/components/ChatHeader.tsx +5 -15
- package/src/components/LocalLlmSettingsModal.tsx +5 -5
- package/src/components/TerminalPanel.tsx +1 -1
- package/src/components/chat/ApprovalModal.tsx +5 -5
- package/src/components/chat/AskUserQuestionMenu.tsx +13 -13
- package/src/components/chat/AssistantMessage.tsx +41 -13
- package/src/components/chat/ChatInput.tsx +24 -24
- package/src/components/chat/ExecPolicyPicker.tsx +4 -4
- package/src/components/chat/ExecutedActionsCard.tsx +10 -3
- package/src/components/chat/FilePathCode.tsx +2 -1
- package/src/components/chat/LocalChatPanel.tsx +39 -4
- package/src/components/chat/Markdown.tsx +15 -14
- package/src/components/chat/MessageList.tsx +16 -3
- package/src/components/chat/ModelPicker.tsx +6 -6
- package/src/components/chat/SessionTodoList.tsx +137 -0
- package/src/components/chat/SessionTreeModal.tsx +3 -3
- package/src/components/chat/SuggestedReplies.tsx +5 -4
- package/src/components/chat/TaskPanelModal.tsx +3 -3
- package/src/components/chat/TaskProcessPanel.tsx +5 -4
- package/src/components/chat/TurnFilesCard.tsx +2 -2
- package/src/components/chat/TurnProcessGroup.tsx +226 -99
- package/src/components/chat/UserMessage.tsx +4 -4
- package/src/components/chat/next-steps-tags.ts +28 -0
- package/src/components/chat/process-status.ts +125 -53
- package/src/components/chat/todo-list-model.ts +167 -0
- package/src/components/chat/turn-timeline.ts +100 -11
- package/src/components/settings/AgentsSettingsPanel.tsx +2 -2
- package/src/components/settings/AppearanceSettingsPanel.tsx +3 -3
- package/src/components/settings/DiagnoseSettingsPanel.tsx +2 -2
- package/src/components/settings/InsightsSettingsPanel.tsx +4 -4
- package/src/components/settings/LlmSettingsPanel.tsx +10 -10
- package/src/components/settings/McpSettingsPanel.tsx +3 -3
- package/src/components/settings/ModelIdCombobox.tsx +2 -2
- package/src/components/settings/SecuritySettingsPanel.tsx +1 -1
- package/src/components/settings/SettingsSaveButton.tsx +1 -1
- package/src/components/settings/SkillsSettingsPanel.tsx +2 -2
- package/src/components/settings/TelemetrySettingsPanel.tsx +2 -2
- package/src/components/settings/UsagePanel.tsx +14 -10
- package/src/components/settings/WebSearchSettingsPanel.tsx +2 -2
- package/src/layouts/AgentLayout.tsx +3 -3
- package/src/lib/chat-transport.ts +68 -12
- package/src/lib/show-thinking-content.ts +4 -4
- package/src/pages/AgentPage.tsx +56 -6
- package/src/pages/ChatPanelPreviewPage.tsx +26 -4
- package/src/pages/SettingsPage.tsx +39 -39
- package/src/styles/index.css +12 -2
|
@@ -81,7 +81,7 @@ export type { ExecPolicy, McpToolItem, SkillItem };
|
|
|
81
81
|
* - Compact-mode responsive layout.
|
|
82
82
|
*/
|
|
83
83
|
|
|
84
|
-
const MIN_HEIGHT_PX =
|
|
84
|
+
const MIN_HEIGHT_PX = 36;
|
|
85
85
|
const MAX_HEIGHT_PX = 220;
|
|
86
86
|
const MENTION_PATTERN = /(@[^\s@]+)/g;
|
|
87
87
|
const MAX_MENTION_SUGGESTIONS = 8;
|
|
@@ -1512,7 +1512,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1512
1512
|
|
|
1513
1513
|
if (approvalPrompt?.current) {
|
|
1514
1514
|
return (
|
|
1515
|
-
<div className="chat-input-container @container relative px-
|
|
1515
|
+
<div className="chat-input-container @container relative px-2.5 pb-2 pt-0.5">
|
|
1516
1516
|
<ApprovalPromptMenu
|
|
1517
1517
|
key={approvalPrompt.current.requestId}
|
|
1518
1518
|
request={approvalPrompt.current}
|
|
@@ -1527,7 +1527,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1527
1527
|
const request = askUserPrompt.current;
|
|
1528
1528
|
return (
|
|
1529
1529
|
<div
|
|
1530
|
-
className="chat-input-container @container relative px-
|
|
1530
|
+
className="chat-input-container @container relative px-2.5 pb-2 pt-0.5"
|
|
1531
1531
|
data-testid="ask-user-composer"
|
|
1532
1532
|
>
|
|
1533
1533
|
<AskUserQuestionMenu
|
|
@@ -1549,7 +1549,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1549
1549
|
return (
|
|
1550
1550
|
// @container:底栏元素的显隐/宽度用容器查询(@sm = 384px)而不是
|
|
1551
1551
|
// 视口断点——侧边栏占宽后,视口够宽但输入框本身可能已经很窄。
|
|
1552
|
-
<div className="chat-input-container @container relative px-
|
|
1552
|
+
<div className="chat-input-container @container relative px-2.5 pb-2 pt-0.5">
|
|
1553
1553
|
{(leadingChrome || selectedAgent) && (
|
|
1554
1554
|
<div
|
|
1555
1555
|
className="relative mb-1 flex min-w-0 items-center gap-1.5"
|
|
@@ -1712,7 +1712,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1712
1712
|
onDrop={handleDrop}
|
|
1713
1713
|
>
|
|
1714
1714
|
{pendingFollowUps.length > 0 && (
|
|
1715
|
-
<div className="flex flex-col gap-1 border-b border-agent-border bg-agent-muted/30 px-
|
|
1715
|
+
<div className="flex flex-col gap-1 border-b border-agent-border bg-agent-muted/30 px-2.5 py-1.5">
|
|
1716
1716
|
<div className="text-[11px] font-medium text-agent-muted-foreground">
|
|
1717
1717
|
排队中({pendingFollowUps.length})· 本轮结束后自动发出 · 停止将丢弃
|
|
1718
1718
|
</div>
|
|
@@ -1739,14 +1739,14 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1739
1739
|
{steerNotice && (
|
|
1740
1740
|
<div
|
|
1741
1741
|
role="status"
|
|
1742
|
-
className="border-b border-agent-border bg-agent-muted/20 px-
|
|
1742
|
+
className="border-b border-agent-border bg-agent-muted/20 px-2.5 py-1 text-[11px] text-agent-muted-foreground"
|
|
1743
1743
|
>
|
|
1744
1744
|
{steerNotice}
|
|
1745
1745
|
</div>
|
|
1746
1746
|
)}
|
|
1747
1747
|
<div className="chat-input-surface relative overflow-hidden">
|
|
1748
1748
|
<div
|
|
1749
|
-
className="chat-input-placeholder pointer-events-none absolute inset-x-0 top-0 px-
|
|
1749
|
+
className="chat-input-placeholder pointer-events-none absolute inset-x-0 top-0 px-2.5 pt-2 text-xs text-agent-muted-foreground"
|
|
1750
1750
|
style={{ visibility: value ? 'hidden' : undefined }}
|
|
1751
1751
|
>
|
|
1752
1752
|
{placeholder}
|
|
@@ -1769,7 +1769,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1769
1769
|
onCompositionStart={handleCompositionStart}
|
|
1770
1770
|
onCompositionEnd={handleCompositionEnd}
|
|
1771
1771
|
onScroll={handleEditorScroll}
|
|
1772
|
-
className="chat-input-editor relative z-10 block w-full overflow-y-auto whitespace-pre-wrap break-words border-0 bg-transparent px-
|
|
1772
|
+
className="chat-input-editor relative z-10 block w-full overflow-y-auto whitespace-pre-wrap break-words border-0 bg-transparent px-2.5 pt-2 text-xs text-agent-foreground caret-agent-foreground outline-none selection:bg-agent-foreground/20"
|
|
1773
1773
|
style={{
|
|
1774
1774
|
minHeight: MIN_HEIGHT_PX,
|
|
1775
1775
|
maxHeight: MAX_HEIGHT_PX,
|
|
@@ -1777,7 +1777,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1777
1777
|
/>
|
|
1778
1778
|
</div>
|
|
1779
1779
|
{actualFiles.length > 0 && (
|
|
1780
|
-
<div className="flex flex-wrap gap-1.5 px-
|
|
1780
|
+
<div className="flex flex-wrap gap-1.5 px-2.5 pb-1.5 pt-0.5">
|
|
1781
1781
|
{actualFiles.map((file, idx) => (
|
|
1782
1782
|
<div
|
|
1783
1783
|
key={idx}
|
|
@@ -1802,7 +1802,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1802
1802
|
)}
|
|
1803
1803
|
<div
|
|
1804
1804
|
ref={toolbarRowRef}
|
|
1805
|
-
className="flex items-center justify-between gap-
|
|
1805
|
+
className="flex items-center justify-between gap-1.5 px-2 py-1 text-[12px] leading-[1.45]"
|
|
1806
1806
|
>
|
|
1807
1807
|
{/* Left toolbar: mode + exec sandbox + host extras + settings.
|
|
1808
1808
|
Agent picker lives in the meta row above the box, next to
|
|
@@ -1827,11 +1827,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1827
1827
|
type="button"
|
|
1828
1828
|
onClick={handlePickFiles}
|
|
1829
1829
|
disabled={disabled || isStreaming}
|
|
1830
|
-
className="flex h-
|
|
1830
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
1831
1831
|
title="上传文件(可多选)"
|
|
1832
1832
|
aria-label="上传文件"
|
|
1833
1833
|
>
|
|
1834
|
-
<LuPaperclip className="h-3
|
|
1834
|
+
<LuPaperclip className="h-3 w-3" />
|
|
1835
1835
|
</button>
|
|
1836
1836
|
{/* 隐藏的原生多选文件输入:点击回形针按钮触发系统选择框。 */}
|
|
1837
1837
|
<input
|
|
@@ -1847,12 +1847,12 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1847
1847
|
<button
|
|
1848
1848
|
type="button"
|
|
1849
1849
|
onClick={onOpenSettings}
|
|
1850
|
-
className="flex h-
|
|
1850
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
|
|
1851
1851
|
title="LLM 设置"
|
|
1852
1852
|
aria-label="LLM 设置"
|
|
1853
1853
|
data-testid="chat-llm-settings"
|
|
1854
1854
|
>
|
|
1855
|
-
<LuSettings className="h-3
|
|
1855
|
+
<LuSettings className="h-3 w-3" />
|
|
1856
1856
|
</button>
|
|
1857
1857
|
)}
|
|
1858
1858
|
</div>
|
|
@@ -1921,10 +1921,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1921
1921
|
disabled={!isStreaming && !canSend}
|
|
1922
1922
|
className={
|
|
1923
1923
|
isStreaming
|
|
1924
|
-
? 'flex h-
|
|
1924
|
+
? 'flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-agent-destructive text-white transition hover:opacity-90'
|
|
1925
1925
|
: canSend
|
|
1926
|
-
? 'flex h-
|
|
1927
|
-
: 'flex h-
|
|
1926
|
+
? 'flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-agent-foreground text-agent-canvas transition hover:opacity-90'
|
|
1927
|
+
: 'flex h-6 w-6 shrink-0 cursor-not-allowed items-center justify-center rounded-full bg-agent-muted text-agent-muted-foreground'
|
|
1928
1928
|
}
|
|
1929
1929
|
title={
|
|
1930
1930
|
isStreaming
|
|
@@ -1937,9 +1937,9 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1937
1937
|
data-testid="chat-send"
|
|
1938
1938
|
>
|
|
1939
1939
|
{isStreaming ? (
|
|
1940
|
-
<LuSquare className="h-3
|
|
1940
|
+
<LuSquare className="h-3 w-3 fill-current" />
|
|
1941
1941
|
) : (
|
|
1942
|
-
<LuArrowUp className="h-
|
|
1942
|
+
<LuArrowUp className="h-3.5 w-3.5" />
|
|
1943
1943
|
)}
|
|
1944
1944
|
</button>
|
|
1945
1945
|
</div>
|
|
@@ -1971,7 +1971,7 @@ function ModeToggle({
|
|
|
1971
1971
|
// shrink-0:分段控件被 flex 压缩会压扁文字;极窄容器(<@sm)退化为
|
|
1972
1972
|
// 纯图标(title 兜底语义),把收缩量让给可截断的选择器。
|
|
1973
1973
|
<div
|
|
1974
|
-
className="inline-flex h-
|
|
1974
|
+
className="inline-flex h-6 shrink-0 items-center rounded-full border border-agent-border bg-agent-canvas p-0.5"
|
|
1975
1975
|
role="radiogroup"
|
|
1976
1976
|
aria-label="对话模式"
|
|
1977
1977
|
data-testid="mode-toggle"
|
|
@@ -1985,13 +1985,13 @@ function ModeToggle({
|
|
|
1985
1985
|
title="Agent 模式:直接执行任务"
|
|
1986
1986
|
data-testid="mode-agent"
|
|
1987
1987
|
className={[
|
|
1988
|
-
'inline-flex h-
|
|
1988
|
+
'inline-flex h-5 items-center gap-1 rounded-full px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
|
|
1989
1989
|
mode === 'agent'
|
|
1990
1990
|
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
1991
1991
|
: 'text-agent-muted-foreground hover:text-agent-foreground',
|
|
1992
1992
|
].join(' ')}
|
|
1993
1993
|
>
|
|
1994
|
-
<LuInfinity className="h-3
|
|
1994
|
+
<LuInfinity className="h-3 w-3" />
|
|
1995
1995
|
<span className="@max-sm:hidden">Agent</span>
|
|
1996
1996
|
</button>
|
|
1997
1997
|
<button
|
|
@@ -2003,13 +2003,13 @@ function ModeToggle({
|
|
|
2003
2003
|
title="Plan 模式:先制定计划,只读不执行"
|
|
2004
2004
|
data-testid="mode-plan"
|
|
2005
2005
|
className={[
|
|
2006
|
-
'inline-flex h-
|
|
2006
|
+
'inline-flex h-5 items-center gap-1 rounded-full px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
|
|
2007
2007
|
mode === 'plan'
|
|
2008
2008
|
? 'bg-amber-400/20 text-amber-600 dark:text-amber-400'
|
|
2009
2009
|
: 'text-agent-muted-foreground hover:text-agent-foreground',
|
|
2010
2010
|
].join(' ')}
|
|
2011
2011
|
>
|
|
2012
|
-
<LuListChecks className="h-3
|
|
2012
|
+
<LuListChecks className="h-3 w-3" />
|
|
2013
2013
|
<span className="@max-sm:hidden">Plan</span>
|
|
2014
2014
|
</button>
|
|
2015
2015
|
</div>
|
|
@@ -64,16 +64,16 @@ export function ExecPolicyPicker({
|
|
|
64
64
|
title={current.description}
|
|
65
65
|
onClick={() => setOpen((next) => !next)}
|
|
66
66
|
className={[
|
|
67
|
-
'inline-flex h-
|
|
67
|
+
'inline-flex h-6 max-w-[140px] items-center gap-1 rounded-full border px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
|
|
68
68
|
isFull
|
|
69
69
|
? 'border-amber-400/50 bg-amber-400/10 text-amber-700 dark:text-amber-400'
|
|
70
70
|
: 'border-agent-border bg-agent-canvas text-agent-foreground hover:bg-agent-foreground/5',
|
|
71
71
|
].join(' ')}
|
|
72
72
|
>
|
|
73
73
|
{isFull ? (
|
|
74
|
-
<LuLockOpen className="h-3
|
|
74
|
+
<LuLockOpen className="h-3 w-3 shrink-0" />
|
|
75
75
|
) : (
|
|
76
|
-
<LuFolderLock className="h-3
|
|
76
|
+
<LuFolderLock className="h-3 w-3 shrink-0" />
|
|
77
77
|
)}
|
|
78
78
|
<span className="truncate">{current.label}</span>
|
|
79
79
|
<LuChevronDown className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
|
|
@@ -104,7 +104,7 @@ export function ExecPolicyPicker({
|
|
|
104
104
|
: 'hover:bg-agent-foreground/5',
|
|
105
105
|
].join(' ')}
|
|
106
106
|
>
|
|
107
|
-
<span className="text-
|
|
107
|
+
<span className="text-[12px] font-medium leading-[1.45] text-agent-foreground">
|
|
108
108
|
{option.label}
|
|
109
109
|
</span>
|
|
110
110
|
<span className="text-[11px] leading-snug text-agent-muted-foreground">
|
|
@@ -42,6 +42,8 @@ interface ExecutedActionsCardProps {
|
|
|
42
42
|
actions: ExecutedAction[];
|
|
43
43
|
/** Drop the outer card chrome when nested in a turn-process group. */
|
|
44
44
|
compact?: boolean;
|
|
45
|
+
/** Show args/output instead of a one-line summary. Defaults to collapsed. */
|
|
46
|
+
defaultExpanded?: boolean;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
function deriveStatus(action: ExecutedAction): ToolExecutionPayload['status'] {
|
|
@@ -145,7 +147,11 @@ function renderActionOutput(output: unknown): ReactNode {
|
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
/** Inline tool rows for a mixed think→act timeline (no summary banner). */
|
|
148
|
-
export function ToolsFlow({
|
|
150
|
+
export function ToolsFlow({
|
|
151
|
+
actions,
|
|
152
|
+
compact = false,
|
|
153
|
+
defaultExpanded = false,
|
|
154
|
+
}: ExecutedActionsCardProps) {
|
|
149
155
|
if (!actions || actions.length === 0) return null;
|
|
150
156
|
const expanded = expandRunCodeActions(actions);
|
|
151
157
|
return (
|
|
@@ -161,6 +167,7 @@ export function ToolsFlow({ actions, compact = false }: ExecutedActionsCardProps
|
|
|
161
167
|
<ToolExecutionCard
|
|
162
168
|
key={action.id ?? `${action.tool}-${i}`}
|
|
163
169
|
payload={actionToTool(action, i)}
|
|
170
|
+
defaultExpanded={defaultExpanded}
|
|
164
171
|
renderOutput={renderActionOutput}
|
|
165
172
|
className="rounded-none border-0 border-t border-agent-border first:border-t-0"
|
|
166
173
|
/>
|
|
@@ -179,8 +186,8 @@ export function ExecutedActionsCard({ actions }: ExecutedActionsCardProps) {
|
|
|
179
186
|
const runningCount = expanded.filter((a) => deriveStatus(a) === 'running').length;
|
|
180
187
|
|
|
181
188
|
return (
|
|
182
|
-
<div className="my-
|
|
183
|
-
<div className="flex items-center justify-between border-b border-agent-border bg-agent-muted/30 px-
|
|
189
|
+
<div className="my-1.5 overflow-hidden rounded-agent-md border border-agent-border bg-agent-canvas">
|
|
190
|
+
<div className="flex items-center justify-between border-b border-agent-border bg-agent-muted/30 px-2.5 py-1">
|
|
184
191
|
<div className="flex items-center gap-1.5 text-xs">
|
|
185
192
|
{failureCount === 0 ? (
|
|
186
193
|
<LuCircleCheck className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400" />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState, type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
2
|
import { openLocalPath, type ResolvedLocalPath } from '@/lib/local-api';
|
|
3
3
|
import { peekResolvedPath, subscribeResolvedPath } from './path-mentions';
|
|
4
|
+
import { splitTurnFilePath } from './turn-files';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 行内代码里的文件路径:后端确认存在后变成可点击,点击用系统默认应用打开。
|
|
@@ -59,7 +60,7 @@ export function FilePathCode({ candidate, chatId, children, ...rest }: FilePathC
|
|
|
59
60
|
openError ? 'text-red-600 dark:text-red-400' : ''
|
|
60
61
|
}`}
|
|
61
62
|
>
|
|
62
|
-
{children}
|
|
63
|
+
{splitTurnFilePath(resolved.path).name || children}
|
|
63
64
|
</button>
|
|
64
65
|
);
|
|
65
66
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
2
2
|
import type { ChatMessage } from '@steerable/agent-protocol';
|
|
3
3
|
import { ChatPanel } from '@steerable/agent-ui';
|
|
4
4
|
import type { SteerOutcome } from '@steerable/agent-ui';
|
|
@@ -16,6 +16,9 @@ import type { ExecutedAction } from './ExecutedActionsCard';
|
|
|
16
16
|
import type { ChildInfo } from './OrchestrationChildrenCard';
|
|
17
17
|
import type { TurnBlock } from './turn-timeline';
|
|
18
18
|
import type { TurnFile } from './turn-files';
|
|
19
|
+
import type { LlmSpeedSnapshot } from './process-status';
|
|
20
|
+
import { SessionTodoList } from './SessionTodoList';
|
|
21
|
+
import { resolveLatestSessionTodos } from './todo-list-model';
|
|
19
22
|
import {
|
|
20
23
|
isImageFile,
|
|
21
24
|
saveChatAttachments,
|
|
@@ -98,6 +101,8 @@ export interface LocalChatPanelProps {
|
|
|
98
101
|
currentTurnTimeline?: TurnBlock[];
|
|
99
102
|
currentTurnStartedAtMs?: number;
|
|
100
103
|
durationByMessageId?: Record<string, number>;
|
|
104
|
+
currentLlmSpeed?: LlmSpeedSnapshot;
|
|
105
|
+
llmSpeedByMessageId?: Record<string, LlmSpeedSnapshot>;
|
|
101
106
|
/** 回合产物文件列表(按落库消息 id 键控),转发给 MessageList。 */
|
|
102
107
|
turnFilesByMessageId?: Record<string, TurnFile[]>;
|
|
103
108
|
/** 当轮产物文件(流尾声到达、尚未归档到落库 id 的尾部消息用)。 */
|
|
@@ -176,6 +181,8 @@ export function LocalChatPanel({
|
|
|
176
181
|
currentTurnTimeline,
|
|
177
182
|
currentTurnStartedAtMs,
|
|
178
183
|
durationByMessageId,
|
|
184
|
+
currentLlmSpeed,
|
|
185
|
+
llmSpeedByMessageId,
|
|
179
186
|
turnFilesByMessageId,
|
|
180
187
|
currentTurnFiles,
|
|
181
188
|
currentTurnChildren,
|
|
@@ -285,6 +292,30 @@ export function LocalChatPanel({
|
|
|
285
292
|
// 空会话 hero:messages 为空且未在流式时整面板切到居中首屏布局。
|
|
286
293
|
const showEmptyHero = Boolean(emptyHero) && messages.length === 0 && !isStreaming;
|
|
287
294
|
|
|
295
|
+
const sessionTodos = useMemo(
|
|
296
|
+
() =>
|
|
297
|
+
resolveLatestSessionTodos({
|
|
298
|
+
messages,
|
|
299
|
+
executedActionsByMessageId,
|
|
300
|
+
currentTurnActions,
|
|
301
|
+
timelineByMessageId,
|
|
302
|
+
currentTurnTimeline,
|
|
303
|
+
}),
|
|
304
|
+
[
|
|
305
|
+
messages,
|
|
306
|
+
executedActionsByMessageId,
|
|
307
|
+
currentTurnActions,
|
|
308
|
+
timelineByMessageId,
|
|
309
|
+
currentTurnTimeline,
|
|
310
|
+
],
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const sessionTodoBanner = sessionTodos ? (
|
|
314
|
+
<div className="px-2.5">
|
|
315
|
+
<SessionTodoList todos={sessionTodos} />
|
|
316
|
+
</div>
|
|
317
|
+
) : null;
|
|
318
|
+
|
|
288
319
|
// Sync `chat-input-box-width` CSS var so bubbles match the input width
|
|
289
320
|
// (cloud product uses 95% of the input box; we mirror that here so message
|
|
290
321
|
// bubbles + input visually align in a column).
|
|
@@ -356,20 +387,21 @@ export function LocalChatPanel({
|
|
|
356
387
|
>
|
|
357
388
|
<ChatPanel.Root className="flex h-full flex-col overflow-hidden" unstyled>
|
|
358
389
|
{showEmptyHero ? (
|
|
359
|
-
<div className="flex min-h-0 flex-1 items-center justify-center p-
|
|
360
|
-
<div className="flex w-full max-w-2xl flex-col items-center gap-
|
|
390
|
+
<div className="flex min-h-0 flex-1 items-center justify-center p-3 sm:p-5">
|
|
391
|
+
<div className="flex w-full max-w-2xl flex-col items-center gap-4">
|
|
361
392
|
<div className="text-center">
|
|
362
393
|
<h1 className="text-xl font-semibold tracking-tight text-agent-foreground">
|
|
363
394
|
{emptyHero?.title}
|
|
364
395
|
</h1>
|
|
365
396
|
{emptyHero?.subtitle && (
|
|
366
|
-
<p className="mt-1.5 text-
|
|
397
|
+
<p className="mt-1.5 text-xs text-agent-muted-foreground">
|
|
367
398
|
{emptyHero.subtitle}
|
|
368
399
|
</p>
|
|
369
400
|
)}
|
|
370
401
|
</div>
|
|
371
402
|
<div className="w-full">
|
|
372
403
|
{inputBanner}
|
|
404
|
+
{sessionTodoBanner}
|
|
373
405
|
{chatInputNode}
|
|
374
406
|
</div>
|
|
375
407
|
</div>
|
|
@@ -396,6 +428,8 @@ export function LocalChatPanel({
|
|
|
396
428
|
currentTurnTimeline={currentTurnTimeline}
|
|
397
429
|
currentTurnStartedAtMs={currentTurnStartedAtMs}
|
|
398
430
|
durationByMessageId={durationByMessageId}
|
|
431
|
+
currentLlmSpeed={currentLlmSpeed}
|
|
432
|
+
llmSpeedByMessageId={llmSpeedByMessageId}
|
|
399
433
|
turnFilesByMessageId={turnFilesByMessageId}
|
|
400
434
|
currentTurnFiles={currentTurnFiles}
|
|
401
435
|
currentTurnChildren={currentTurnChildren}
|
|
@@ -415,6 +449,7 @@ export function LocalChatPanel({
|
|
|
415
449
|
onSelectSuggestion={onSelectSuggestion}
|
|
416
450
|
/>
|
|
417
451
|
{inputBanner}
|
|
452
|
+
{sessionTodoBanner}
|
|
418
453
|
{chatInputNode}
|
|
419
454
|
</div>
|
|
420
455
|
</>
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { PlanTodoList } from './PlanTodoList';
|
|
21
21
|
import { FilePathCode } from './FilePathCode';
|
|
22
22
|
import { looksLikeFilePath } from './path-mentions';
|
|
23
|
+
import { stripNextStepsTags } from './next-steps-tags';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Markdown renderer shared by `UserMessage` and `AssistantMessage`.
|
|
@@ -59,25 +60,25 @@ const components: Components = {
|
|
|
59
60
|
h1: (props: ComponentPropsWithoutRef<'h1'>) => (
|
|
60
61
|
<h1
|
|
61
62
|
{...props}
|
|
62
|
-
className="mb-
|
|
63
|
+
className="mb-0.5 mt-1.5 text-xs font-semibold text-agent-foreground first:mt-0"
|
|
63
64
|
/>
|
|
64
65
|
),
|
|
65
66
|
h2: (props: ComponentPropsWithoutRef<'h2'>) => (
|
|
66
67
|
<h2
|
|
67
68
|
{...props}
|
|
68
|
-
className="mb-
|
|
69
|
+
className="mb-0.5 mt-1.5 text-xs font-semibold text-agent-foreground first:mt-0"
|
|
69
70
|
/>
|
|
70
71
|
),
|
|
71
72
|
h3: (props: ComponentPropsWithoutRef<'h3'>) => (
|
|
72
73
|
<h3
|
|
73
74
|
{...props}
|
|
74
|
-
className="mb-
|
|
75
|
+
className="mb-0.5 mt-1.5 text-xs font-semibold text-agent-foreground first:mt-0"
|
|
75
76
|
/>
|
|
76
77
|
),
|
|
77
78
|
h4: (props: ComponentPropsWithoutRef<'h4'>) => (
|
|
78
79
|
<h4
|
|
79
80
|
{...props}
|
|
80
|
-
className="mb-
|
|
81
|
+
className="mb-0.5 mt-1.5 text-xs font-semibold text-agent-foreground first:mt-0"
|
|
81
82
|
/>
|
|
82
83
|
),
|
|
83
84
|
|
|
@@ -85,16 +86,16 @@ const components: Components = {
|
|
|
85
86
|
<p
|
|
86
87
|
style={{ whiteSpace: 'pre-line' }}
|
|
87
88
|
{...props}
|
|
88
|
-
className="my-
|
|
89
|
+
className="my-0.5 leading-relaxed"
|
|
89
90
|
/>
|
|
90
91
|
),
|
|
91
92
|
|
|
92
93
|
// ── Lists ─────────────────────────────────────────────────────────────
|
|
93
94
|
ul: (props: ComponentPropsWithoutRef<'ul'>) => (
|
|
94
|
-
<ul {...props} className="my-1
|
|
95
|
+
<ul {...props} className="my-1 list-disc space-y-0.5 pl-4" />
|
|
95
96
|
),
|
|
96
97
|
ol: (props: ComponentPropsWithoutRef<'ol'>) => (
|
|
97
|
-
<ol {...props} className="my-1
|
|
98
|
+
<ol {...props} className="my-1 list-decimal space-y-0.5 pl-4" />
|
|
98
99
|
),
|
|
99
100
|
li: (props: ComponentPropsWithoutRef<'li'>) => (
|
|
100
101
|
<li
|
|
@@ -115,18 +116,18 @@ const components: Components = {
|
|
|
115
116
|
blockquote: (props: ComponentPropsWithoutRef<'blockquote'>) => (
|
|
116
117
|
<blockquote
|
|
117
118
|
{...props}
|
|
118
|
-
className="my-1
|
|
119
|
+
className="my-1 border-l-2 border-agent-border bg-agent-muted/40 px-2 py-0.5 text-xs text-agent-muted-foreground [&>p]:my-0"
|
|
119
120
|
/>
|
|
120
121
|
),
|
|
121
122
|
|
|
122
123
|
// ── Horizontal rule ──────────────────────────────────────────────────
|
|
123
124
|
hr: (props: ComponentPropsWithoutRef<'hr'>) => (
|
|
124
|
-
<hr {...props} className="my-
|
|
125
|
+
<hr {...props} className="my-2 border-agent-border" />
|
|
125
126
|
),
|
|
126
127
|
|
|
127
128
|
// ── Tables ───────────────────────────────────────────────────────────
|
|
128
129
|
table: (props: ComponentPropsWithoutRef<'table'>) => (
|
|
129
|
-
<div className="my-
|
|
130
|
+
<div className="my-1.5 w-full overflow-x-auto rounded-md border border-agent-border">
|
|
130
131
|
<table {...props} className="w-full border-collapse text-[12px]" />
|
|
131
132
|
</div>
|
|
132
133
|
),
|
|
@@ -136,13 +137,13 @@ const components: Components = {
|
|
|
136
137
|
th: (props: ComponentPropsWithoutRef<'th'>) => (
|
|
137
138
|
<th
|
|
138
139
|
{...props}
|
|
139
|
-
className="border-b border-agent-border px-2 py-1
|
|
140
|
+
className="border-b border-agent-border px-2 py-1 text-left font-medium text-agent-foreground"
|
|
140
141
|
/>
|
|
141
142
|
),
|
|
142
143
|
td: (props: ComponentPropsWithoutRef<'td'>) => (
|
|
143
144
|
<td
|
|
144
145
|
{...props}
|
|
145
|
-
className="border-b border-agent-border px-2 py-1
|
|
146
|
+
className="border-b border-agent-border px-2 py-1 align-top break-words"
|
|
146
147
|
style={{ maxWidth: '300px' }}
|
|
147
148
|
/>
|
|
148
149
|
),
|
|
@@ -159,7 +160,7 @@ const components: Components = {
|
|
|
159
160
|
return (
|
|
160
161
|
<pre
|
|
161
162
|
{...props}
|
|
162
|
-
className="my-
|
|
163
|
+
className="my-1.5 overflow-x-auto rounded-md border border-agent-border bg-agent-muted/60 p-2 text-[12px] leading-relaxed text-agent-foreground"
|
|
163
164
|
/>
|
|
164
165
|
);
|
|
165
166
|
},
|
|
@@ -453,7 +454,7 @@ export function Markdown({
|
|
|
453
454
|
rehypePlugins={[rehypeRaw]}
|
|
454
455
|
components={finalComponents}
|
|
455
456
|
>
|
|
456
|
-
{children}
|
|
457
|
+
{stripNextStepsTags(children)}
|
|
457
458
|
</ReactMarkdown>
|
|
458
459
|
);
|
|
459
460
|
}
|
|
@@ -20,6 +20,7 @@ import type { ChatMode } from './ChatInput';
|
|
|
20
20
|
import type { TurnBlock } from './turn-timeline';
|
|
21
21
|
import type { TurnFile } from './turn-files';
|
|
22
22
|
import { inferDurationMs, readPersistedDurationMs } from './elapsed';
|
|
23
|
+
import type { LlmSpeedSnapshot } from './process-status';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* MessageList — scrolling viewport that renders user/assistant message
|
|
@@ -80,6 +81,10 @@ interface MessageListProps {
|
|
|
80
81
|
currentTurnStartedAtMs?: number;
|
|
81
82
|
/** Frozen duration keyed by assistant message id (live freeze + history). */
|
|
82
83
|
durationByMessageId?: Record<string, number>;
|
|
84
|
+
/** Live model-request speed for the in-flight assistant turn. */
|
|
85
|
+
currentLlmSpeed?: LlmSpeedSnapshot;
|
|
86
|
+
/** Frozen model-request speed keyed by assistant message id. */
|
|
87
|
+
llmSpeedByMessageId?: Record<string, LlmSpeedSnapshot>;
|
|
83
88
|
/**
|
|
84
89
|
* 回合产物文件列表,按落库消息 id 键控(live 回合在 message_id 事件时
|
|
85
90
|
* 归档;历史回合从 messageMetadata.turnFiles 水合)。
|
|
@@ -155,6 +160,8 @@ export function MessageList({
|
|
|
155
160
|
currentTurnTimeline,
|
|
156
161
|
currentTurnStartedAtMs,
|
|
157
162
|
durationByMessageId,
|
|
163
|
+
currentLlmSpeed,
|
|
164
|
+
llmSpeedByMessageId,
|
|
158
165
|
turnFilesByMessageId,
|
|
159
166
|
currentTurnFiles,
|
|
160
167
|
currentTurnChildren,
|
|
@@ -241,12 +248,12 @@ export function MessageList({
|
|
|
241
248
|
<div className="relative flex-1 overflow-hidden">
|
|
242
249
|
<div
|
|
243
250
|
ref={containerRef}
|
|
244
|
-
className="h-full overflow-y-auto overflow-anchor-none px-
|
|
251
|
+
className="h-full overflow-y-auto overflow-anchor-none px-2.5 py-2"
|
|
245
252
|
>
|
|
246
253
|
{visibleMessages.length === 0 ? (
|
|
247
254
|
emptyState ?? null
|
|
248
255
|
) : (
|
|
249
|
-
<div className="mx-auto w-full space-y-1
|
|
256
|
+
<div className="mx-auto w-full space-y-1">
|
|
250
257
|
{visibleMessages.map((message, index) => {
|
|
251
258
|
const isLast = index === visibleMessages.length - 1;
|
|
252
259
|
if (message.role === 'user') {
|
|
@@ -311,6 +318,11 @@ export function MessageList({
|
|
|
311
318
|
: durationByMessageId?.[message.id]
|
|
312
319
|
?? readPersistedDurationMs(metadataJson)
|
|
313
320
|
?? inferDurationMs(previousUserCreatedAt, message.createdAt);
|
|
321
|
+
const persistedSpeed = llmSpeedByMessageId?.[message.id];
|
|
322
|
+
const isSpeedTail = isLast && currentLlmSpeed !== undefined;
|
|
323
|
+
const llmSpeed =
|
|
324
|
+
persistedSpeed
|
|
325
|
+
?? (isStreamingTail || isSpeedTail ? currentLlmSpeed : undefined);
|
|
314
326
|
|
|
315
327
|
return (
|
|
316
328
|
<div key={message.id}>
|
|
@@ -329,6 +341,7 @@ export function MessageList({
|
|
|
329
341
|
onRegenerate={onRegenerate}
|
|
330
342
|
startedAtMs={isStreamingTail ? currentTurnStartedAtMs : undefined}
|
|
331
343
|
durationMs={durationMs}
|
|
344
|
+
llmSpeed={llmSpeed}
|
|
332
345
|
turnFiles={turnFiles}
|
|
333
346
|
onShare={message.id === lastAssistantId ? onShare : undefined}
|
|
334
347
|
/>
|
|
@@ -376,7 +389,7 @@ export function MessageList({
|
|
|
376
389
|
setIsAtBottom(true);
|
|
377
390
|
scrollToBottom('smooth');
|
|
378
391
|
}}
|
|
379
|
-
className="absolute bottom-
|
|
392
|
+
className="absolute bottom-2 right-2 flex h-7 w-7 items-center justify-center rounded-full border border-agent-border bg-agent-canvas text-agent-foreground shadow-md transition-colors hover:bg-agent-foreground/5"
|
|
380
393
|
title="回到底部"
|
|
381
394
|
aria-label="回到底部"
|
|
382
395
|
>
|
|
@@ -117,7 +117,7 @@ export function ModelPicker({
|
|
|
117
117
|
if (next && status !== 'live' && !loading) void refresh();
|
|
118
118
|
}}
|
|
119
119
|
disabled={disabled}
|
|
120
|
-
className="inline-flex h-
|
|
120
|
+
className="inline-flex h-6 min-w-0 max-w-[160px] items-center gap-1 rounded-full border border-agent-border bg-agent-canvas px-1.5 text-[12px] leading-[1.45] text-agent-foreground transition-colors hover:bg-agent-foreground/5 disabled:cursor-not-allowed disabled:opacity-70 @sm:max-w-[220px]"
|
|
121
121
|
title={
|
|
122
122
|
status === 'offline'
|
|
123
123
|
? `模型目录不可用${catalog?.error ? `:${catalog.error}` : ''}(仍可手动指定模型)`
|
|
@@ -165,7 +165,7 @@ export function ModelPicker({
|
|
|
165
165
|
setModelMenuOpen(false);
|
|
166
166
|
}}
|
|
167
167
|
className={[
|
|
168
|
-
'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-
|
|
168
|
+
'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-[12px] leading-[1.45] transition-colors',
|
|
169
169
|
model == null
|
|
170
170
|
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
171
171
|
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
@@ -192,7 +192,7 @@ export function ModelPicker({
|
|
|
192
192
|
setModelMenuOpen(false);
|
|
193
193
|
}}
|
|
194
194
|
className={[
|
|
195
|
-
'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-
|
|
195
|
+
'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-[12px] leading-[1.45] transition-colors',
|
|
196
196
|
isActive
|
|
197
197
|
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
198
198
|
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
@@ -232,7 +232,7 @@ export function ModelPicker({
|
|
|
232
232
|
setModelMenuOpen(false);
|
|
233
233
|
}}
|
|
234
234
|
disabled={disabled}
|
|
235
|
-
className="inline-flex h-
|
|
235
|
+
className="inline-flex h-6 items-center gap-1 rounded-full border border-agent-border bg-agent-canvas px-1.5 text-[12px] leading-[1.45] text-agent-foreground transition-colors hover:bg-agent-foreground/5 disabled:cursor-not-allowed disabled:opacity-70"
|
|
236
236
|
title="推理档位(随每轮请求下发;模型不支持会直接报错)"
|
|
237
237
|
aria-haspopup="menu"
|
|
238
238
|
aria-expanded={effortMenuOpen}
|
|
@@ -253,7 +253,7 @@ export function ModelPicker({
|
|
|
253
253
|
setEffortMenuOpen(false);
|
|
254
254
|
}}
|
|
255
255
|
className={[
|
|
256
|
-
'flex w-full rounded px-2 py-1.5 text-left text-
|
|
256
|
+
'flex w-full rounded px-2 py-1.5 text-left text-[12px] leading-[1.45] transition-colors',
|
|
257
257
|
reasoningEffort == null
|
|
258
258
|
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
259
259
|
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
@@ -271,7 +271,7 @@ export function ModelPicker({
|
|
|
271
271
|
setEffortMenuOpen(false);
|
|
272
272
|
}}
|
|
273
273
|
className={[
|
|
274
|
-
'flex w-full rounded px-2 py-1.5 text-left text-
|
|
274
|
+
'flex w-full rounded px-2 py-1.5 text-left text-[12px] leading-[1.45] transition-colors',
|
|
275
275
|
reasoningEffort === level
|
|
276
276
|
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
277
277
|
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|