Skip to content

Commit 47365e8

Browse files
committed
update files
1 parent 2a4dabb commit 47365e8

24 files changed

Lines changed: 854 additions & 152 deletions

‎desktop/src/api/backgroundShell.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,19 +94,25 @@ function parseSignal(raw: unknown): AgentMirrorEvent | null {
9494
if (typeof type !== 'string') return null
9595
const id = typeof obj.id === 'string' ? obj.id : ''
9696
if (!id) return null
97+
const sessionId =
98+
typeof obj.sessionId === 'string'
99+
? obj.sessionId
100+
: typeof obj.session_id === 'string'
101+
? obj.session_id
102+
: null
97103
switch (type) {
98104
case 'spawned': {
99105
const command = typeof obj.command === 'string' ? obj.command : ''
100-
return { type: 'spawned', id, command }
106+
return { type: 'spawned', id, command, sessionId }
101107
}
102108
case 'chunk': {
103109
const stream = obj.stream === 'stderr' ? 'stderr' : 'stdout'
104110
const line = typeof obj.line === 'string' ? obj.line : ''
105-
return { type: 'chunk', id, stream, line }
111+
return { type: 'chunk', id, stream, line, sessionId }
106112
}
107113
case 'heartbeat': {
108114
const elapsed = typeof obj.elapsedSecs === 'number' ? obj.elapsedSecs : 0
109-
return { type: 'heartbeat', id, elapsedSecs: elapsed }
115+
return { type: 'heartbeat', id, elapsedSecs: elapsed, sessionId }
110116
}
111117
case 'exited': {
112118
const elapsed = typeof obj.elapsedSecs === 'number' ? obj.elapsedSecs : 0
@@ -116,7 +122,7 @@ function parseSignal(raw: unknown): AgentMirrorEvent | null {
116122
: obj.exitCode === null
117123
? null
118124
: null
119-
return { type: 'exited', id, elapsedSecs: elapsed, exitCode }
125+
return { type: 'exited', id, elapsedSecs: elapsed, exitCode, sessionId }
120126
}
121127
default:
122128
return null

‎desktop/src/components/chat/EmbeddedBrowserPanel.tsx‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
clampRectToHost,
1919
dockFocusActive,
2020
dockHide,
21-
dockOpen,
2221
dockPark,
2322
dockPresentSession,
2423
dockResync,
@@ -170,10 +169,10 @@ export function EmbeddedBrowserPanel() {
170169
}
171170
void (async () => {
172171
try {
172+
await dockSetRect(rect)
173173
await dockPresentSession(sessionId)
174-
await dockOpen(rect, null, sessionId)
175174
} catch (err) {
176-
console.warn('[browserDock] dock present/open on session switch failed', err)
175+
console.warn('[browserDock] dock present on session switch failed', err)
177176
}
178177
})()
179178
}, [sessionId])
@@ -574,10 +573,10 @@ export function EmbeddedBrowserPanel() {
574573
}
575574
void (async () => {
576575
try {
576+
await dockSetRect(rect)
577577
await dockPresentSession(sessionId)
578-
await dockOpen(rect, null, sessionId)
579578
} catch (err) {
580-
console.warn('[browserDock] dock present/open on visibility change failed', err)
579+
console.warn('[browserDock] dock present on visibility change failed', err)
581580
}
582581
})()
583582
}, [sessionId, visible, ownsDock, hasContent])

‎desktop/src/components/chat/MessageList.tsx‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ type RenderItem =
5454
| { kind: 'explored'; id: string; items: UIMessage[]; summary: ExploredSummary }
5555
| { kind: 'message'; message: UIMessage }
5656

57+
const TODO_LIST_TOOL_NAMES = new Set([
58+
'todo_write',
59+
'TodoWrite',
60+
'todowrite',
61+
'tasks_write',
62+
'TasksWrite',
63+
])
64+
5765
const USER_FACING_ERROR_CODES = new Set([
5866
'NO_MODEL_CONFIGURED',
5967
'CONFIG_ERROR',
@@ -278,6 +286,27 @@ export function buildRenderModel(messages: UIMessage[]): RenderModel {
278286
}
279287
}
280288

289+
const todoItemIdxs: number[] = []
290+
for (let i = 0; i < items.length; i++) {
291+
const it = items[i]!
292+
if (
293+
it.kind === 'message' &&
294+
it.message.type === 'tool_use' &&
295+
TODO_LIST_TOOL_NAMES.has(it.message.toolName)
296+
) {
297+
todoItemIdxs.push(i)
298+
}
299+
}
300+
if (todoItemIdxs.length > 1) {
301+
for (let k = 0; k < todoItemIdxs.length - 1; k++) {
302+
const idx = todoItemIdxs[k]!
303+
const it = items[idx]!
304+
if (it.kind === 'message' && !it.message.superseded) {
305+
items[idx] = { ...it, message: { ...it.message, superseded: true } }
306+
}
307+
}
308+
}
309+
281310
return { renderItems: items, toolResultMap, childToolCallsByParent }
282311
}
283312

‎desktop/src/components/chat/tools/ExecToolView.tsx‎

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,102 @@
22
// Copyright (c) 2025-2026 SenWeaverCoding
33
// Licensed under the MIT License.
44

5+
import { useEffect, useState } from 'react'
56
import type { ToolViewProps } from './ToolViewProps'
67
import { TerminalChrome } from '../TerminalChrome'
78
import { CodeViewer } from '../CodeViewer'
89
import { extractCommand, extractTextContent, firstWord, truncate } from '../../../utils/toolFormatters'
910
import { useTerminalPanelStore } from '../../../stores/terminalPanelStore'
1011
import { useTabStore } from '../../../stores/tabStore'
12+
import { useChatStore } from '../../../stores/chatStore'
1113
import { useTranslation } from '../../../i18n'
1214

1315
const EXEC_MAX_LINES = 32
1416

15-
export function ExecHeader({ input }: ToolViewProps) {
17+
function formatElapsed(ms: number): string {
18+
const totalSec = Math.max(0, Math.floor(ms / 1000))
19+
if (totalSec < 60) return `${totalSec}s`
20+
const m = Math.floor(totalSec / 60)
21+
const s = totalSec % 60
22+
return `${m}m${s.toString().padStart(2, '0')}s`
23+
}
24+
25+
function RunningTimer({ start }: { start?: number }) {
26+
const [now, setNow] = useState(() => Date.now())
27+
useEffect(() => {
28+
const id = setInterval(() => setNow(Date.now()), 1000)
29+
return () => clearInterval(id)
30+
}, [])
31+
const base = typeof start === 'number' && start > 0 ? start : now
32+
return (
33+
<span className="shrink-0 font-[var(--font-mono)] text-[10px] tabular-nums text-[var(--color-text-tertiary)]">
34+
{formatElapsed(now - base)}
35+
</span>
36+
)
37+
}
38+
39+
export function ExecHeader({
40+
input,
41+
isStreaming,
42+
toolTimestamp,
43+
parentSessionId,
44+
toolUseId,
45+
}: ToolViewProps) {
46+
const t = useTranslation()
47+
const cancelTool = useChatStore((s) => s.cancelTool)
1648
const command = extractCommand(input)
49+
const stopLabel = (t('execTool.stop' as never) as string) || 'Stop this command'
50+
51+
const stopControl =
52+
isStreaming && parentSessionId ? (
53+
<span
54+
role="button"
55+
tabIndex={0}
56+
title={stopLabel}
57+
aria-label={stopLabel}
58+
onClick={(e) => {
59+
e.stopPropagation()
60+
e.preventDefault()
61+
cancelTool(parentSessionId, toolUseId)
62+
}}
63+
onKeyDown={(e) => {
64+
if (e.key === 'Enter' || e.key === ' ') {
65+
e.stopPropagation()
66+
e.preventDefault()
67+
cancelTool(parentSessionId, toolUseId)
68+
}
69+
}}
70+
className="shrink-0 inline-flex h-[18px] w-[18px] items-center justify-center rounded-full border border-[var(--color-error)]/40 text-[var(--color-error)] transition-colors hover:bg-[var(--color-error)]/12"
71+
>
72+
<span className="material-symbols-outlined text-[12px]">stop</span>
73+
</span>
74+
) : null
75+
1776
if (!command) {
1877
return (
19-
<span className="min-w-0 flex-1 truncate text-[11px] text-[var(--color-text-tertiary)]">
20-
(no command)
78+
<span className="flex min-w-0 flex-1 items-center gap-2 text-[11px] text-[var(--color-text-tertiary)]">
79+
<span className="min-w-0 flex-1 truncate">(no command)</span>
80+
{isStreaming && <RunningTimer start={toolTimestamp} />}
81+
{stopControl}
2182
</span>
2283
)
2384
}
2485
const head = firstWord(command)
2586
return (
2687
<span
27-
className="flex min-w-0 flex-1 items-baseline gap-2 truncate text-[12px] text-[var(--color-text-secondary)]"
88+
className="flex min-w-0 flex-1 items-baseline gap-2 text-[12px] text-[var(--color-text-secondary)]"
2889
title={command}
2990
>
3091
{head && (
31-
<span className="font-[var(--font-mono)] text-[12px] text-[var(--color-text-primary)]">
92+
<span className="shrink-0 font-[var(--font-mono)] text-[12px] text-[var(--color-text-primary)]">
3293
{head}
3394
</span>
3495
)}
35-
<span className="min-w-0 truncate font-[var(--font-mono)] text-[11px] text-[var(--color-text-tertiary)]">
96+
<span className="min-w-0 flex-1 truncate font-[var(--font-mono)] text-[11px] text-[var(--color-text-tertiary)]">
3697
{truncate(command.replace(/\s+/g, ' '), 120)}
3798
</span>
99+
{isStreaming && <RunningTimer start={toolTimestamp} />}
100+
{stopControl}
38101
</span>
39102
)
40103
}

‎desktop/src/components/chat/tools/ToolCard.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ export function ToolCard({
130130
isStreaming = false,
131131
compact = false,
132132
defaultExpanded,
133+
parentSessionId,
134+
toolTimestamp,
133135
childCalls,
134136
childResults,
135137
}: Props) {
@@ -181,6 +183,8 @@ export function ToolCard({
181183
result: result ?? null,
182184
isStreaming,
183185
compact,
186+
parentSessionId,
187+
toolTimestamp,
184188
childCalls,
185189
childResults,
186190
}

‎desktop/src/i18n/locales/en.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,7 @@ export const en = {
14551455
'modeBlocked.toolNotAllowed.body': '{tools} is not in the allowed-tools whitelist of the current coding mode. The call was refused. Switch to a mode that allows it, or pick a different tool to accomplish the task.',
14561456

14571457
'execTool.openFullLog': 'View full log in terminal panel',
1458+
'execTool.stop': 'Stop this command',
14581459

14591460
'thinking.wordThought': 'Thought',
14601461
'thinking.wordThinking': 'Thinking',

‎desktop/src/i18n/locales/zh.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,6 +1441,7 @@ export const zh: Record<TranslationKey, string> = {
14411441
'modeBlocked.toolNotAllowed.body': '{tools} 不在当前编程模式的工具白名单内。请求已被拦截。请切换到包含该工具的模式,或选择其他工具完成任务。',
14421442

14431443
'execTool.openFullLog': '在终端面板查看完整日志',
1444+
'execTool.stop': '停止该命令',
14441445

14451446
'thinking.wordThought': '思考',
14461447
'thinking.wordThinking': '思考中',

‎desktop/src/stores/browserPanelStore.ts‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
dockRequestState,
2222
dockReload,
2323
dockSetPickMode,
24+
dockSetRect,
2425
dockSetZoom,
2526
type BrowserDockEvent,
2627
type BrowserDockRect,
@@ -175,6 +176,8 @@ export type BrowserPanelState = {
175176
type ToggleOptions = {
176177
source?: BrowserPanelSource
177178
url?: string | null
179+
180+
presentOnly?: boolean
178181
}
179182

180183
type StoreState = {
@@ -627,14 +630,21 @@ export const useBrowserPanelStore = create<StoreState>((set, get) => ({
627630
})
628631
const rect = get().panels[sessionId]?.anchorRect ?? null
629632
try {
630-
await dockPresentSession(sessionId)
631-
if (rect) {
632-
await dockOpen(rect, seedUrl, sessionId)
633+
if (opts?.presentOnly) {
634+
if (rect) {
635+
await dockSetRect(rect)
636+
}
637+
await dockPresentSession(sessionId)
633638
} else {
634-
await dockOpen({ x: 0, y: 0, w: 1, h: 1 }, seedUrl, sessionId)
639+
await dockPresentSession(sessionId)
640+
if (rect) {
641+
await dockOpen(rect, seedUrl, sessionId)
642+
} else {
643+
await dockOpen({ x: 0, y: 0, w: 1, h: 1 }, seedUrl, sessionId)
644+
}
635645
}
636646
} catch (err) {
637-
console.warn('[browserDock] openForTool dockOpen failed', err)
647+
console.warn('[browserDock] openForTool present failed', err)
638648
}
639649
dockRequestState().catch((err) => {
640650
console.warn('[browserDock] dockRequestState failed', err)

‎desktop/src/stores/chatStore.ts‎

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
hasUsableModelForSession,
2626
isNoModelConfiguredError,
2727
} from '../utils/modelAvailability'
28-
import { ensureSessionRuntimeSynced } from '../utils/runtimeSync'
28+
import { ensureSessionRuntimeSynced, queueSessionRuntimeSync } from '../utils/runtimeSync'
2929
import { AGENT_LIFECYCLE_TYPES } from '../types/team'
3030
import type { MessageEntry, PendingRewindSummary } from '../types/session'
3131
import type { PermissionMode } from '../types/settings'
@@ -291,6 +291,8 @@ type ChatStore = {
291291
setSessionPermissionMode: (sessionId: string, mode: PermissionMode) => void
292292
setSessionCodingMode: (sessionId: string, mode: CodingModeId) => void
293293
stopGeneration: (sessionId: string) => void
294+
cancelTool: (sessionId: string, toolUseId?: string) => void
295+
reconcileStuckSession: (sessionId: string) => void
294296
loadHistory: (sessionId: string) => Promise<void>
295297
reloadHistory: (sessionId: string) => Promise<void>
296298
queueComposerPrefill: (
@@ -1979,10 +1981,8 @@ export const useChatStore = create<ChatStore>((set, get) => ({
19791981
return
19801982
}
19811983

1982-
void ensureSessionRuntimeSynced(sessionId, { persist: false })
1983-
.finally(() => {
1984-
wsManager.send(sessionId, { type: 'user_message', content, attachments })
1985-
})
1984+
queueSessionRuntimeSync(sessionId, { persist: false })
1985+
wsManager.send(sessionId, { type: 'user_message', content, attachments })
19861986
},
19871987

19881988
respondToPermission: (sessionId, requestId, allowed, options) => {
@@ -2066,6 +2066,35 @@ export const useChatStore = create<ChatStore>((set, get) => ({
20662066
wsManager.send(sessionId, { type: 'set_coding_mode', mode, scope: 'session' })
20672067
},
20682068

2069+
cancelTool: (sessionId, toolUseId) => {
2070+
wsManager.send(sessionId, { type: 'cancel_tool', sessionId, toolUseId })
2071+
},
2072+
2073+
reconcileStuckSession: (sessionId) => {
2074+
const session = get().sessions[sessionId]
2075+
if (!session) return
2076+
const isStuckActive =
2077+
session.chatState === 'thinking' ||
2078+
session.chatState === 'tool_executing' ||
2079+
session.chatState === 'streaming'
2080+
if (!isStuckActive) {
2081+
if (session.stopRequested) {
2082+
set((s) => ({
2083+
sessions: updateSessionIn(s.sessions, sessionId, () => ({
2084+
stopRequested: false,
2085+
})),
2086+
}))
2087+
}
2088+
return
2089+
}
2090+
set((s) => ({
2091+
sessions: updateSessionIn(s.sessions, sessionId, () => ({
2092+
stopRequested: false,
2093+
})),
2094+
}))
2095+
void get().reloadHistory(sessionId)
2096+
},
2097+
20692098
stopGeneration: (sessionId) => {
20702099
wsManager.send(sessionId, { type: 'stop_generation' })
20712100
if (hasPendingDelta(sessionId)) {
@@ -2672,6 +2701,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
26722701
void useBrowserPanelStore.getState().openForTool(sessionId, {
26732702
source: 'tool',
26742703
url: null,
2704+
presentOnly: true,
26752705
})
26762706
}
26772707
}
@@ -3039,6 +3069,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
30393069
void useBrowserPanelStore.getState().openForTool(sessionId, {
30403070
source: 'tool',
30413071
url: targetUrl,
3072+
presentOnly: true,
30423073
})
30433074
}
30443075
if (isSubagentParentTool(toolName) && toolUseId) {

0 commit comments

Comments
 (0)