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
17 changes: 15 additions & 2 deletions .pi/extensions/fm-calm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,16 @@ import {
} from "@earendil-works/pi-coding-agent";
import { Box, Container, getKeybindings, type Component } from "@earendil-works/pi-tui";
import type { TSchema } from "typebox";
import { installCalmAssistantLayout } from "./lib/fm-calm-assistant-layout.ts";
import { installCalmOperationalUserLayout } from "./lib/fm-calm-operational-user-layout.ts";
import {
installCalmAssistantLayout,
noteCalmTranscriptRunSettled,
noteCalmTranscriptRunStart,
resetCalmTranscriptOrigin,
} from "./lib/fm-calm-assistant-layout.ts";
import {
installCalmOperationalUserLayout,
installCalmTranscriptReplayWindow,
} from "./lib/fm-calm-operational-user-layout.ts";
import {
CALM_WORKING_SHIP_WIDGET_KEY,
createCalmWorkingShipAnimation,
Expand Down Expand Up @@ -98,6 +106,7 @@ function installCalmPresentationAdapter(name: string, install: () => void): void
export default function (pi: ExtensionAPI) {
installCalmPresentationAdapter("collapsed-thinking", installCalmAssistantLayout);
installCalmPresentationAdapter("operational-user-row", installCalmOperationalUserLayout);
installCalmPresentationAdapter("transcript-replay-window", installCalmTranscriptReplayWindow);

let exportRendering = false;
let removeTerminalInputHandler: (() => void) | undefined;
Expand Down Expand Up @@ -275,6 +284,7 @@ export default function (pi: ExtensionAPI) {
registerBuiltIn(createLsToolDefinition);

pi.on("session_start", (_event, ctx) => {
resetCalmTranscriptOrigin();
exportRendering = false;
setCalmPresentation(loadCalmPreference());
setCalmStockExportRendering(false);
Expand Down Expand Up @@ -314,17 +324,20 @@ export default function (pi: ExtensionAPI) {
});

pi.on("agent_start", (_event, ctx) => {
noteCalmTranscriptRunStart();
agentRunActive = true;
applyWorkingPresentation(ctx.ui);
});

// agent_settled is emitted from a finally block, so it also covers abort and failure.
pi.on("agent_settled", (_event, ctx) => {
noteCalmTranscriptRunSettled();
agentRunActive = false;
applyWorkingPresentation(ctx.ui);
});

pi.on("session_shutdown", (_event, ctx) => {
noteCalmTranscriptRunSettled();
agentRunActive = false;
applyWorkingPresentation(ctx.ui);
});
Expand Down
162 changes: 147 additions & 15 deletions .pi/extensions/lib/fm-calm-assistant-layout.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
// Verified against Pi 0.81.1 and 0.82.0, which export AssistantMessageComponent with an
// updateContent method. installCalmAssistantLayout() probes that exact method and throws
// if it is missing; fm-calm.ts catches that and skips only this adapter with a diagnostic
// instead of blocking Calm or Pi.
// Pi exports AssistantMessageComponent with an updateContent method.
// installCalmAssistantLayout() probes that exact method and throws if it is missing;
// fm-calm.ts catches that and skips only this adapter with a diagnostic instead of
// blocking Calm or Pi.
// The adapter owns both collapsed-thinking layout and the presentation-only exact
// operational acknowledgement rule.
// Acknowledgement origin is scoped to one agent run rather than to the most recent user
// row: a run counts as operational only while every Firstmate input it carries is
// canonically operational, so a wake steered into a still-running captain turn keeps that
// run's replies visible.
// Pi opens a run with agent_start before the run's initiating user message_start, drains
// steered and queued inputs into that same run, and settles it from a finally block. The
// accumulator records the first input of a run whichever order those two arrive in, so a
// future Pi that emits them the other way round still accumulates instead of overwriting.
// Pi also rebuilds the whole transcript through InteractiveMode.renderSessionItems,
// including the rebuild it performs when it auto-compacts inside a run. Rows replayed in
// the window the separately probed transcript-replay adapter marks are scored per row
// against their own preceding input and never disturb the run scope. Without that
// adapter no window is ever opened and replayed rows fall back to run scoping. Every
// unresolved case resolves to visible.
import type { AssistantMessageComponent as PiAssistantMessageComponent } from "@earendil-works/pi-coding-agent";
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
import { calmPresentationHides } from "./fm-calm-visibility.ts";
Expand All @@ -15,27 +31,128 @@ type AssistantMessagePresentationState = {
};

type CalmAssistantLayoutPatch = {
assistantOperationalOrigins: WeakMap<object, boolean>;
replayDepth: number;
replayOriginIsOperational: boolean;
runIsActive: boolean;
runOriginIsOperational: boolean;
runOriginRecorded: boolean;
hidesOperationalAcknowledgement: () => boolean;
hidesThinking: () => boolean;
};

// Keep the introduction-version symbol stable so a compatible upgrade cannot
// double-patch a live process.
// The symbol changes only when the patch shape changes, so a compatible upgrade cannot
// double-patch a live process and an incompatible one cannot keep a stale closure
// installed under the same key.
const CALM_ASSISTANT_LAYOUT_PATCH = Symbol.for(
"firstmate:calm-assistant-layout:pi-0.81.1",
"firstmate:calm-assistant-layout:operational-ack-v2",
);
const FIRSTMATE_NO_ACTION_ACKNOWLEDGEMENT = "Captain, shipshape.";

export function installCalmAssistantLayout(): void {
const registry = globalThis as typeof globalThis & {
function registry(): typeof globalThis & {
[key: symbol]: CalmAssistantLayoutPatch | undefined;
} {
return globalThis as typeof globalThis & {
[key: symbol]: CalmAssistantLayoutPatch | undefined;
};
}

export function noteCalmTranscriptUserMessage(isOperational: boolean): void {
const patch = registry()[CALM_ASSISTANT_LAYOUT_PATCH];
if (!patch) return;
if (patch.replayDepth > 0) {
patch.replayOriginIsOperational = isOperational;
return;
}
if (patch.runIsActive && patch.runOriginRecorded) {
patch.runOriginIsOperational = patch.runOriginIsOperational && isOperational;
return;
}
patch.runOriginIsOperational = isOperational;
patch.runOriginRecorded = true;
}

export function beginCalmTranscriptReplay(): void {
const patch = registry()[CALM_ASSISTANT_LAYOUT_PATCH];
if (!patch) return;
if (patch.replayDepth === 0) patch.replayOriginIsOperational = false;
patch.replayDepth += 1;
}

export function endCalmTranscriptReplay(): void {
const patch = registry()[CALM_ASSISTANT_LAYOUT_PATCH];
if (!patch || patch.replayDepth === 0) return;
patch.replayDepth -= 1;
}

export function noteCalmTranscriptRunStart(): void {
const patch = registry()[CALM_ASSISTANT_LAYOUT_PATCH];
if (patch) patch.runIsActive = true;
}

export function noteCalmTranscriptRunSettled(): void {
const patch = registry()[CALM_ASSISTANT_LAYOUT_PATCH];
if (!patch) return;
patch.runIsActive = false;
patch.runOriginRecorded = false;
}

export function resetCalmTranscriptOrigin(): void {
const patch = registry()[CALM_ASSISTANT_LAYOUT_PATCH];
if (!patch) return;
patch.replayDepth = 0;
patch.replayOriginIsOperational = false;
patch.runIsActive = false;
patch.runOriginIsOperational = false;
patch.runOriginRecorded = false;
}

function withoutOperationalAcknowledgement(
message: AssistantMessage,
isOperational: boolean,
hidesAcknowledgement: boolean,
): AssistantMessage {
if (!isOperational || !hidesAcknowledgement) return message;
if (message.content.some((block) => block.type === "toolCall")) return message;

const text = message.content
.map((block) => (block.type === "text" ? block.text : ""))
.join("");
const isStreamingPrefix =
message.stopReason === "pending" &&
FIRSTMATE_NO_ACTION_ACKNOWLEDGEMENT.startsWith(text);
const isFinalAcknowledgement =
message.stopReason === "stop" &&
text === FIRSTMATE_NO_ACTION_ACKNOWLEDGEMENT;
if (!isStreamingPrefix && !isFinalAcknowledgement) return message;

return {
...message,
content: message.content.filter((block) => block.type !== "text"),
};
}

export function installCalmAssistantLayout(): void {
const hidesThinking = (): boolean => calmPresentationHides("assistant-thinking");
const installed = registry[CALM_ASSISTANT_LAYOUT_PATCH];
const hidesOperationalAcknowledgement = (): boolean =>
calmPresentationHides("synthetic-assistant");
const installed = registry()[CALM_ASSISTANT_LAYOUT_PATCH];
if (installed) {
installed.hidesThinking = hidesThinking;
installed.hidesOperationalAcknowledgement = hidesOperationalAcknowledgement;
return;
}

const patch: CalmAssistantLayoutPatch = { hidesThinking };
const patch: CalmAssistantLayoutPatch = {
assistantOperationalOrigins: new WeakMap<object, boolean>(),
replayDepth: 0,
replayOriginIsOperational: false,
runIsActive: false,
runOriginIsOperational: false,
runOriginRecorded: false,
hidesOperationalAcknowledgement,
hidesThinking,
};
const AssistantMessageComponent = PiCodingAgent.AssistantMessageComponent;
if (typeof AssistantMessageComponent !== "function") {
throw new Error("Firstmate Calm requires Pi AssistantMessageComponent");
Expand All @@ -49,20 +166,35 @@ export function installCalmAssistantLayout(): void {
message: AssistantMessage,
): void {
const state = this as unknown as AssistantMessagePresentationState;
let isOperational = patch.assistantOperationalOrigins.get(this);
if (isOperational === undefined) {
isOperational =
patch.replayDepth > 0
? patch.replayOriginIsOperational
: patch.runOriginIsOperational;
patch.assistantOperationalOrigins.set(this, isOperational);
}
const hideThinking =
state.hiddenThinkingLabel === "" &&
state.hideThinkingBlock &&
patch.hidesThinking();
const acknowledgementPresentation = withoutOperationalAcknowledgement(
message,
isOperational,
patch.hidesOperationalAcknowledgement(),
);
const presentationMessage = hideThinking
? {
...message,
content: message.content.filter((block) => block.type !== "thinking"),
...acknowledgementPresentation,
content: acknowledgementPresentation.content.filter(
(block) => block.type !== "thinking",
),
}
: message;
: acknowledgementPresentation;

originalUpdateContent.call(this, presentationMessage);
if (presentationMessage !== message) state.lastMessage = message;
};

registry[CALM_ASSISTANT_LAYOUT_PATCH] = patch;
registry()[CALM_ASSISTANT_LAYOUT_PATCH] = patch;
}
78 changes: 68 additions & 10 deletions .pi/extensions/lib/fm-calm-operational-user-layout.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
// Verified against Pi 0.81.1 and 0.82.0, which add the ordinary-user spacer and row
// together via InteractiveMode.addMessageToChat. This adapter probes that exact method
// and throws if it is missing; fm-calm.ts catches that and skips only this adapter with a
// diagnostic instead of blocking Calm or Pi. It changes only that presentation and never
// message delivery.
// Pi adds the ordinary-user spacer and row together through
// InteractiveMode.addMessageToChat, and replays or rebuilds the whole transcript through
// InteractiveMode.renderSessionItems.
// This file installs those as two independently probed adapters, each of which throws
// when its own exact method is missing; fm-calm.ts catches that and skips only the
// affected adapter with a diagnostic instead of blocking Calm or Pi.
// The operational-user-row adapter owns the zero-height row and the canonical user-row
// origin the assistant layout adapter consumes. The transcript-replay adapter only marks
// the replay window; without it, replayed rows fall back to run scoping, which resolves
// to visible.
// Neither adapter changes message delivery.
import type { UserMessageComponent as PiUserMessageComponent } from "@earendil-works/pi-coding-agent";
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
import {
beginCalmTranscriptReplay,
endCalmTranscriptReplay,
noteCalmTranscriptUserMessage,
} from "./fm-calm-assistant-layout.ts";
import { calmPresentationHides } from "./fm-calm-visibility.ts";
import { classifyFirstmateCurrentOperationalText } from "./fm-operational-input.ts";

Expand Down Expand Up @@ -34,16 +45,24 @@ type InteractiveModePrototype = {
message: UserMessageLike,
options?: AddMessageOptions,
): void;
renderSessionItems(this: unknown, items: unknown[], options?: unknown): void;
};
type CalmOperationalUserLayoutPatch = {
hidesOperationalInput: () => boolean;
isOperationalInput: (text: string) => boolean;
};
type CalmTranscriptReplayPatch = {
wrapped: true;
};

// Keep the introduction-version symbol stable so a compatible upgrade cannot
// double-patch a live process.
// Each symbol changes only when the closure it guards changes, so a compatible upgrade
// cannot double-patch a live process and an incompatible one cannot keep a stale closure
// installed under the same key.
const CALM_OPERATIONAL_USER_LAYOUT_PATCH = Symbol.for(
"firstmate:calm-operational-user-layout:pi-0.81.1",
"firstmate:calm-operational-user-layout:operational-ack-v2",
);
const CALM_TRANSCRIPT_REPLAY_PATCH = Symbol.for(
"firstmate:calm-transcript-replay:v1",
);
const LEGACY_CALM_OPERATIONAL_PREFIX = "\u2063Supervisor escalate (";

Expand Down Expand Up @@ -120,13 +139,20 @@ export function installCalmOperationalUserLayout(): void {
message: UserMessageLike,
options?: AddMessageOptions,
): void {
if (message.role !== "user" || !contentIsTextOnly(message.content)) {
if (message.role !== "user") {
originalAddMessageToChat.call(this, message, options);
return;
}

if (!contentIsTextOnly(message.content)) {
noteCalmTranscriptUserMessage(false);
originalAddMessageToChat.call(this, message, options);
return;
}
const text = this.getUserMessageText(message);
if (!text || !patch.isOperationalInput(text)) {
const isOperational = Boolean(text && patch.isOperationalInput(text));
noteCalmTranscriptUserMessage(isOperational);
if (!isOperational) {
originalAddMessageToChat.call(this, message, options);
return;
}
Expand All @@ -143,3 +169,35 @@ export function installCalmOperationalUserLayout(): void {

registry[CALM_OPERATIONAL_USER_LAYOUT_PATCH] = patch;
}

export function installCalmTranscriptReplayWindow(): void {
const registry = globalThis as typeof globalThis & {
[key: symbol]: CalmTranscriptReplayPatch | undefined;
};
if (registry[CALM_TRANSCRIPT_REPLAY_PATCH]) return;

const InteractiveMode = PiCodingAgent.InteractiveMode;
if (typeof InteractiveMode !== "function") {
throw new Error("Firstmate Calm requires Pi InteractiveMode");
}
const prototype = InteractiveMode.prototype as unknown as InteractiveModePrototype;
const originalRenderSessionItems = prototype.renderSessionItems;
if (typeof originalRenderSessionItems !== "function") {
throw new Error("Firstmate Calm requires Pi InteractiveMode.renderSessionItems");
}

prototype.renderSessionItems = function (
this: unknown,
items: unknown[],
options?: unknown,
): void {
beginCalmTranscriptReplay();
try {
originalRenderSessionItems.call(this, items, options);
} finally {
endCalmTranscriptReplay();
}
};

registry[CALM_TRANSCRIPT_REPLAY_PATCH] = { wrapped: true };
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ For Grok, `--trust` is needed once per clone so project hooks and the turn-end g
For Pi, approve the project trust prompt once per clone on first launch so the tracked `.pi/extensions/*.ts` files auto-load.
Pi's `/calm` toggle hides supported transcript chrome, including canonically classified Firstmate operational user rows, and uses a Calm-only animated working boat during active runs while preserving all model context and session data.
The hidden operational inputs remain ordinary user-role messages with unchanged delivery, ordering, authority, persistence, and exports.
Calm also hides the agent's exact no-action `Captain, shipshape.` acknowledgement when a run's inputs were all operational, with the exact rule and its limits owned by [docs/calm.md](docs/calm.md).
The preference persists for the effective Firstmate home, and toggling it off restores ordinary rendering.
[Calm's current behavior and supported limits](docs/calm.md) are separate from its [version-scoped maintainer evidence](docs/calm-mode-feasibility.md).

Expand Down
Loading