the watcher shows above the composer, named for what it watches
"watching" was a word in the metadata bar at the top, next to the token count. the thing the owner actually reads is the strip above the input, where background tasks live — and a watcher had nothing there at all, so a commander quietly doing its job looked like a commander doing nothing. WatcherStrip, in the same component and the same place as the task chips, because to the person reading it is the same KIND of thing: something running on their behalf that they want to see without scrolling. it is NOT a background task and cannot be derived like one — a watcher is a detached loop that outlives the turn that armed it, so it has no task id and never appears in `messages`. the platform knows it from a pid file, and only the screen holding the commander's metadata knows what it watches. so it is passed in. named for the subject rather than the fact: "Voice Memos", not "watcher armed". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -44,6 +44,14 @@ type EmbeddableChatProps = {
|
||||
* a commander opened in /chat had `context: 'chat'` and silently lost the control.
|
||||
*/
|
||||
isCommander?: boolean;
|
||||
/**
|
||||
* A detached loop this conversation armed, shown above the composer beside the background tasks.
|
||||
*
|
||||
* Passed in rather than derived, because a watcher is not a background task: it has no task id and is
|
||||
* not in `messages` at all — the platform knows about it from a pid file, and only the screen holding
|
||||
* the commander's metadata can say what it watches.
|
||||
*/
|
||||
watcher?: { label: string; running: boolean };
|
||||
/**
|
||||
* Refuse input entirely, with a reason the composer shows.
|
||||
*
|
||||
|
||||
@@ -52,6 +52,7 @@ type UseEmbeddableChatParams = {
|
||||
contextId?: string;
|
||||
/** This session is a commander, per `GET /chat/sessions/:id`. See the note where it is surfaced. */
|
||||
isCommander?: boolean;
|
||||
watcher?: { label: string; running: boolean };
|
||||
emptyState?: ReactNode;
|
||||
};
|
||||
|
||||
@@ -624,6 +625,7 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
|
||||
would lose the brand-new composer, which has no session yet.
|
||||
*/
|
||||
isCommander: params.context === 'command-hq' || params.isCommander === true,
|
||||
watcher: params.watcher,
|
||||
emptyState: params.emptyState,
|
||||
/*
|
||||
Claude's transcript id, which is what a commander is keyed on everywhere — the Command HQ row, the
|
||||
|
||||
@@ -243,6 +243,28 @@ const TaskStrip = ({ label, children, trailing, divided }: TaskStripProps) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* The commander's own watcher, docked in the same strip as the background tasks.
|
||||
*
|
||||
* A watcher is not a background TASK — it is a detached loop that outlives the turn that armed it, so it
|
||||
* has no task id and never appears in `messages`. It is the same KIND of thing to the person reading:
|
||||
* something running on their behalf that they want to see without scrolling. So it reads the same, sits
|
||||
* in the same place, and says what it is watching rather than that something is watching.
|
||||
*/
|
||||
export const WatcherStrip = ({ label, running }: { label: string; running: boolean }) => (
|
||||
<TaskStrip label={running ? 'watching' : 'not watching'}>
|
||||
<span
|
||||
className={`inline-flex min-w-0 shrink-0 items-center gap-1.5 rounded-full border px-2 py-0.5 text-xs ${
|
||||
running ? 'border-success/40 text-foreground' : 'border-border text-muted-foreground'
|
||||
}`}
|
||||
title={label}
|
||||
>
|
||||
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${running ? 'bg-success' : 'bg-muted-foreground/40'}`} />
|
||||
<span className="truncate">{label}</span>
|
||||
</span>
|
||||
</TaskStrip>
|
||||
);
|
||||
|
||||
// ── Chip ──
|
||||
|
||||
type TaskChipProps = {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useWorkspace } from '../../../components/Workspace';
|
||||
import { AttachmentList } from './AttachmentList';
|
||||
import { AttachButton } from './AttachButton';
|
||||
import { WebpageDialog } from './WebpageDialog';
|
||||
import { BackgroundTaskTray } from './BackgroundTaskTray';
|
||||
import { BackgroundTaskTray, WatcherStrip } from './BackgroundTaskTray';
|
||||
import { useTranslation } from '../../../locales';
|
||||
|
||||
type InputAreaProps = {
|
||||
@@ -119,6 +119,8 @@ export const InputArea = ({ manager, disabled }: InputAreaProps) => {
|
||||
<div className="mb-2 px-3 py-1.5 text-xs text-duck-teal bg-duck-teal/10 rounded-md">{commandFeedback}</div>
|
||||
)}
|
||||
|
||||
{manager.watcher && <WatcherStrip label={manager.watcher.label} running={manager.watcher.running} />}
|
||||
|
||||
<BackgroundTaskTray messages={messages} sessionId={sessionId} />
|
||||
|
||||
<QueuedList queued={queued} onRemove={unqueue} onSendNow={sendQueuedNow} />
|
||||
|
||||
@@ -654,6 +654,15 @@ type ResumeState = {
|
||||
* Same shape here.
|
||||
*/
|
||||
function ResumedCommander({ sessionId, resume }: { sessionId: string; resume: ResumeState }) {
|
||||
// The commander's own metadata, for the watcher strip above the composer. Same query the MetaBar above
|
||||
// already holds, so this is a cache read rather than a second request.
|
||||
const { commander } = useCommandHqCommander(sessionId);
|
||||
const reference = commander?.referencePaths?.[0];
|
||||
const watcher =
|
||||
commander?.watcherArmed && reference
|
||||
? { label: reference.split('/').filter(Boolean).pop() ?? reference, running: commander.watcherRunning === true }
|
||||
: undefined;
|
||||
|
||||
const { t, tUi } = useTranslation();
|
||||
const chat = useChat(undefined, resume.model ?? undefined, {
|
||||
replaceUrl: false,
|
||||
@@ -678,6 +687,9 @@ function ResumedCommander({ sessionId, resume }: { sessionId: string; resume: Re
|
||||
sessionId={undefined}
|
||||
context="command-hq"
|
||||
panelId={`command-hq:${sessionId}`}
|
||||
// Named for what it watches, not for the fact that it does. "Voice Memos" above the composer says
|
||||
// more than "watcher armed" ever did, and it is the only place the reference path is known.
|
||||
watcher={watcher}
|
||||
className="min-h-0 flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user