mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-30 11:52:04 +00:00
feat(desktop): read-only spectator transcript for subagent watch windows
Subagent session pop-outs (`watch=1`) spectate a run driven elsewhere, so editing/steering the transcript from there makes no sense. Gate the composer and the user-bubble mutations on `isWatchWindow()`: - hide the composer (folds into `showChatBar`) - user prompts become a read-only button that toggles the 2-line clamp so long prompts stay fully readable, instead of opening the edit composer - drop the stop/restore actions and the checkpoint branch-picker Keyed off the narrow `isWatchWindow()` (not `isSecondaryWindow()`), so the new-session and cmd-click pop-outs are unaffected.
This commit is contained in:
parent
f1345290ed
commit
7cf6758e33
5 changed files with 55 additions and 16 deletions
|
|
@ -45,7 +45,7 @@ import {
|
|||
$sessions,
|
||||
sessionPinId
|
||||
} from '@/store/session'
|
||||
import { isSecondaryWindow } from '@/store/windows'
|
||||
import { isSecondaryWindow, isWatchWindow } from '@/store/windows'
|
||||
import type { ModelOptionsResponse } from '@/types/hermes'
|
||||
|
||||
import { routeSessionId } from '../routes'
|
||||
|
|
@ -342,8 +342,9 @@ export function ChatView({
|
|||
|
||||
const threadLoading = threadLoadingState(loadingSession, busy, awaitingResponse, lastVisibleIsUser)
|
||||
// Hide the composer in the exhausted error state too: there's no live runtime
|
||||
// to send to until a retry rebinds one.
|
||||
const showChatBar = !loadingSession && !resumeExhausted
|
||||
// to send to until a retry rebinds one. Watch windows are pure spectators of a
|
||||
// subagent run driven elsewhere — no composer, transcript is read-only.
|
||||
const showChatBar = !loadingSession && !resumeExhausted && !isWatchWindow()
|
||||
const threadKey = selectedSessionId || activeSessionId || (isRoutedSessionView ? location.pathname : 'new')
|
||||
|
||||
const modelOptionsQuery = useQuery<ModelOptionsResponse>({
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ import { $activeSessionAwaitingInput } from '@/store/prompts'
|
|||
import { $connection } from '@/store/session'
|
||||
import { notifyThreadEditClose, notifyThreadEditOpen } from '@/store/thread-scroll'
|
||||
import { $voicePlayback } from '@/store/voice-playback'
|
||||
import { isWatchWindow } from '@/store/windows'
|
||||
|
||||
type ThreadLoadingState = 'response' | 'session'
|
||||
interface RestoreMessageTarget {
|
||||
|
|
@ -1020,6 +1021,13 @@ const UserMessage: FC<{
|
|||
const lastClampHeightRef = useRef(-1)
|
||||
const lineHeightRef = useRef(0)
|
||||
|
||||
// Watch windows spectate a subagent run driven elsewhere — prompts can't be
|
||||
// edited, restored, or stopped from here. The bubble stays a button that
|
||||
// toggles the 2-line clamp so long prompts are still fully readable.
|
||||
const readOnly = isWatchWindow()
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const clampActive = !(readOnly && expanded)
|
||||
|
||||
const measureClamp = useCallback((entries: readonly ResizeObserverEntry[]) => {
|
||||
const inner = clampInnerRef.current
|
||||
const outer = inner?.parentElement
|
||||
|
|
@ -1070,11 +1078,11 @@ const UserMessage: FC<{
|
|||
|
||||
const hasBody = messageText.trim().length > 0
|
||||
const isLatestUser = messageId === latestUserId
|
||||
const showStop = isLatestUser && threadRunning && Boolean(onCancel)
|
||||
const showStop = !readOnly && isLatestUser && threadRunning && Boolean(onCancel)
|
||||
// Restore (re-run this exact prompt) is available everywhere the Stop button
|
||||
// isn't — including mid-stream on older prompts, since the action interrupts
|
||||
// the live turn before rewinding.
|
||||
const showRestore = !showStop && Boolean(onRequestRestoreConfirm) && hasBody
|
||||
const showRestore = !readOnly && !showStop && Boolean(onRequestRestoreConfirm) && hasBody
|
||||
|
||||
const bubbleClassName = cn(
|
||||
USER_BUBBLE_BASE_CLASS,
|
||||
|
|
@ -1086,7 +1094,10 @@ const UserMessage: FC<{
|
|||
// Render the user's text through a minimal markdown pipeline:
|
||||
// backtick `code` and ``` fenced ``` blocks, with directive chips
|
||||
// (`@file:` etc.) still resolved inside the plain-text spans.
|
||||
<div className="sticky-human-clamp" data-clamped={bodyClamped ? 'true' : undefined}>
|
||||
<div
|
||||
className={cn(clampActive && 'sticky-human-clamp')}
|
||||
data-clamped={clampActive && bodyClamped ? 'true' : undefined}
|
||||
>
|
||||
{/* Match the edit composer's collapsed line box (min-h-[1.25rem]) so
|
||||
clicking to edit can't grow the bubble by a sub-pixel and reflow the
|
||||
turn 1px. */}
|
||||
|
|
@ -1114,20 +1125,41 @@ const UserMessage: FC<{
|
|||
<ActionBarPrimitive.Root className="relative w-full max-w-full" data-slot="aui_user-bubble-actions">
|
||||
<div className="human-message-with-todos-wrapper flex w-full flex-col gap-0">
|
||||
<div className="relative w-full">
|
||||
{/* Always editable — clicking opens the edit composer even while a
|
||||
turn streams; sending the edit reverts (interrupt + rewind). */}
|
||||
<ActionBarPrimitive.Edit asChild>
|
||||
{readOnly ? (
|
||||
// Spectator transcript: clicking only toggles the clamp so the
|
||||
// full prompt is readable — never opens an edit composer.
|
||||
<button
|
||||
aria-label={copy.editMessage}
|
||||
className={bubbleClassName}
|
||||
onClick={() => triggerHaptic('selection')}
|
||||
onPointerDown={() => notifyThreadEditOpen()}
|
||||
title={copy.editMessage}
|
||||
aria-expanded={bodyClamped ? expanded : undefined}
|
||||
className={cn(bubbleClassName, !bodyClamped && 'cursor-default')}
|
||||
onClick={() => {
|
||||
if (!bodyClamped) {
|
||||
return
|
||||
}
|
||||
|
||||
triggerHaptic('selection')
|
||||
setExpanded(value => !value)
|
||||
}}
|
||||
title={bodyClamped ? (expanded ? t.common.collapse : copy.expandMessage) : undefined}
|
||||
type="button"
|
||||
>
|
||||
{bubbleContent}
|
||||
</button>
|
||||
</ActionBarPrimitive.Edit>
|
||||
) : (
|
||||
// Always editable — clicking opens the edit composer even while a
|
||||
// turn streams; sending the edit reverts (interrupt + rewind).
|
||||
<ActionBarPrimitive.Edit asChild>
|
||||
<button
|
||||
aria-label={copy.editMessage}
|
||||
className={bubbleClassName}
|
||||
onClick={() => triggerHaptic('selection')}
|
||||
onPointerDown={() => notifyThreadEditOpen()}
|
||||
title={copy.editMessage}
|
||||
type="button"
|
||||
>
|
||||
{bubbleContent}
|
||||
</button>
|
||||
</ActionBarPrimitive.Edit>
|
||||
)}
|
||||
{(showStop || showRestore) && (
|
||||
<div className="pointer-events-none absolute right-2 bottom-2 z-10 flex items-center justify-center opacity-0 transition-opacity group-hover/user-message:opacity-100 group-focus-within/user-message:opacity-100">
|
||||
{showStop ? (
|
||||
|
|
@ -1171,7 +1203,10 @@ const UserMessage: FC<{
|
|||
)}
|
||||
</div>
|
||||
<BranchPickerPrimitive.Root
|
||||
className="checkpoint-container flex items-center gap-1 pb-0 pt-1 pl-1.5 text-[0.75rem] leading-none text-(--ui-text-tertiary)"
|
||||
className={cn(
|
||||
'checkpoint-container flex items-center gap-1 pb-0 pt-1 pl-1.5 text-[0.75rem] leading-none text-(--ui-text-tertiary)',
|
||||
readOnly && 'hidden'
|
||||
)}
|
||||
hideWhenSingleBranch
|
||||
>
|
||||
<span aria-hidden className="checkpoint-icon size-1.5 rounded-full border border-current" />
|
||||
|
|
|
|||
|
|
@ -2010,6 +2010,7 @@ export const en: Translations = {
|
|||
stopReading: 'Stop reading',
|
||||
readAloud: 'Read aloud',
|
||||
editMessage: 'Edit message',
|
||||
expandMessage: 'Expand message',
|
||||
scrollToBottom: 'Scroll to bottom',
|
||||
stop: 'Stop',
|
||||
restorePrevious: 'Restore previous checkpoint',
|
||||
|
|
|
|||
|
|
@ -1657,6 +1657,7 @@ export interface Translations {
|
|||
stopReading: string
|
||||
readAloud: string
|
||||
editMessage: string
|
||||
expandMessage: string
|
||||
scrollToBottom: string
|
||||
stop: string
|
||||
restorePrevious: string
|
||||
|
|
|
|||
|
|
@ -2177,6 +2177,7 @@ export const zh: Translations = {
|
|||
stopReading: '停止朗读',
|
||||
readAloud: '朗读',
|
||||
editMessage: '编辑消息',
|
||||
expandMessage: '展开消息',
|
||||
scrollToBottom: '滚动到底部',
|
||||
stop: '停止',
|
||||
restorePrevious: '恢复上一个检查点',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue