Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/desktop/renderer/composer/RunComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
parseTransportError,
type CompleteAttachment,
type ComposerConnectorsPort,
type ContextPillView,
type ProviderKeysPort,
type RunStartRequest,
type StartRunError,
Expand Down Expand Up @@ -112,6 +113,14 @@ export interface RunComposerProps {
* frame. Host-bound (list + write); omitted ⇒ the zone is absent.
*/
readonly filing?: RunComposerFiling;
/**
* The context meter's view, built by the cockpit from the two `/context`
* reads and handed down the `renderComposer` ctx. `null` when nothing has
* been measured — the composer then renders no meter, which is the honest
* state. Mirrors the web binder's prop of the same name; the two hosts share
* the mapping, not the fetch.
*/
readonly context?: ContextPillView | null;
}

/**
Expand Down Expand Up @@ -149,6 +158,7 @@ export function RunComposer(props: RunComposerProps): ReactElement {
conversationId = null,
conversationModel = null,
filing,
context,
} = props;

// The last run-create failure, surfaced inline above the composer. The
Expand Down Expand Up @@ -347,6 +357,9 @@ export function RunComposer(props: RunComposerProps): ReactElement {
<AssistantComposer
connectors={{ servers: [...servers], loading: serversLoading }}
skills={{ skills: [...skills], loading: skillsLoading }}
// Context meter — built once by the cockpit, so this binder and the web
// one draw the same number for the same conversation.
context={context}
attachmentAdapter={attachmentAdapter}
dictationPort={desktopDictationPort}
filePicker={filePicker}
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/renderer/destinationBinders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
type ProjectDetail,
type ProjectDetailProfile,
type ProjectSummary,
type ContextPillView,
type RunEmptyComposerCtx,
type RunStartRequest,
type WorkspaceStageHost,
Expand Down Expand Up @@ -1557,6 +1558,9 @@ export function RunBinder({
// What this chat last ran with — the composer's model-pill seed of last
// resort, derived by the cockpit from the run list it already fetched.
readonly conversationModel?: string | null;
// The context meter, built ONCE by the cockpit from the two `/context`
// reads so both hosts draw the same number for the same conversation.
readonly context?: ContextPillView | null;
}) => (
<RunComposer
dispatch={ctx.dispatch}
Expand All @@ -1577,6 +1581,7 @@ export function RunBinder({
// sends through the cockpit's dispatch, which owns the run target.
conversationId={conversationId}
conversationModel={ctx.conversationModel}
context={ctx.context}
// Same binding as the empty composer's — one filing state per chat, so
// the pill reads the same before and after the first message.
filing={filing}
Expand Down
8 changes: 8 additions & 0 deletions apps/frontend/src/features/run/RunComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import {
AssistantComposer,
parseTransportError,
type ContextPillView,
type RunStartRequest,
type StartRunError,
} from "@0x-copilot/chat-surface";
Expand Down Expand Up @@ -68,6 +69,9 @@ export interface RunComposerCtx {
readonly onCancel: () => void;
/** Newly authenticated connector to enable in the run-scoped Tools state. */
readonly autoActivateConnectorId?: string | null;
/** The context meter's view, built by the cockpit from the two `/context`
* reads. `null` when nothing has been measured — render no meter. */
readonly context?: ContextPillView | null;
}

export interface RunComposerProps {
Expand Down Expand Up @@ -172,6 +176,10 @@ export function RunComposer({
onOpenSkillsSettings={noop}
onOpenMcpSettings={noop}
toolsTrigger={toolsTrigger}
// Context meter. The cockpit reads both `/context` endpoints once and
// hands the built view down the ctx, so this binder and the desktop one
// cannot fetch it differently. `undefined` renders no meter.
context={ctx.context}
models={models}
selectedModel={selectedModel}
onModelChange={onModelChange}
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/features/run/RunRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
RunDestination,
buildRunCreateBody,
useTransport,
type ContextPillView,
type RunEmptyComposerCtx,
type RunStartRequest,
} from "@0x-copilot/chat-surface";
Expand Down Expand Up @@ -355,6 +356,9 @@ export function RunRoute({
// WC-P3 — cockpit-owned run state + cancel; RunComposer swaps send↔Stop.
readonly running: boolean;
readonly onCancel: () => void;
// The context meter, built ONCE by the cockpit from the two `/context`
// reads so both hosts draw the same number for the same conversation.
readonly context?: ContextPillView | null;
}) => (
<RunComposer
ctx={ctx}
Expand Down
29 changes: 29 additions & 0 deletions packages/chat-surface/src/composer/AssistantComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
} from "./useWorkspaceFolderGrants";
import { WorkspaceFolderBar } from "./WorkspaceFolderBar";
import type { ThinkingDepth } from "./depth";
import { ContextPill } from "./ContextPill";
import type { ContextPillView } from "./contextPillView";
import { ModelPill } from "./ModelPill";
import type { ProviderKeysPort } from "../settings/data/providerKeys";
import type { KeyFormConnected } from "../onboarding/KeyForm";
Expand Down Expand Up @@ -186,6 +188,19 @@ export interface AssistantComposerProps {
* above the overflow-hidden composer frame.
*/
toolsTrigger?: ReactNode;
/**
* Context meter for the right cluster — "how full is the window, and who
* filled it". Built by `buildContextPillView` from the two read APIs; the
* host owns the fetch, this package owns the drawing.
*
* `null`/omitted renders NOTHING, which is the honest state for a
* conversation nobody has measured yet: no skeleton, no spinner, no zeroed
* meter. A zeroed meter is a claim.
*/
context?: ContextPillView | null;
/** Open the full context report (`by_call` / `by_subagent` / the per-turn
* occupancy series). Host-owned navigation; omitted renders no link. */
onOpenContextReport?: () => void;
/** PR 8.0.1 — display name of the active model, surfaced in the
* composer footer hint row. */
activeModelLabel?: string;
Expand Down Expand Up @@ -340,6 +355,8 @@ export const AssistantComposer = forwardRef<
// been migrated) but the composer no longer surfaces it — the model
// name lives in <ModelPill> only (Phase 9 dedup).
activeModelLabel: _activeModelLabel,
context,
onOpenContextReport,
models,
selectedModel,
onModelChange,
Expand Down Expand Up @@ -691,6 +708,18 @@ export const AssistantComposer = forwardRef<
{dictation.message}
</span>
) : null}
{/* Context meter, LEFT of the model pill — the model sets the
denominator, so reading rightward gives "60% free · of Opus 5's
window". It is also the one control here that answers a
question asked immediately before pressing send.
Absent (not disabled, not zeroed) until something is measured. */}
{context != null ? (
<ContextPill
view={context}
disabled={controlsDisabled}
onOpenReport={onOpenContextReport}
/>
) : null}
{models && selectedModel !== undefined && onModelChange ? (
<ModelPill
models={models}
Expand Down
Loading