Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4259b90
Make Pi Calm conversation-only
morris2spears Jul 29, 2026
fd7dddc
no-mistakes(review): surface actionable tool errors in Calm; fix work…
morris2spears Jul 29, 2026
df653b0
no-mistakes(review): clamp Calm error lines to terminal width; probe …
morris2spears Jul 29, 2026
89eebc6
no-mistakes(review): surface shared Calm tool error text once per turn
morris2spears Jul 29, 2026
669f520
no-mistakes(review): scope Calm tool-error dedup to one assistant turn
morris2spears Jul 29, 2026
597293e
no-mistakes(review): share Calm tool-error turn scope across adapter …
morris2spears Jul 29, 2026
9b5f6fe
no-mistakes(document): fix stale Calm working-activity and trusted-TU…
morris2spears Jul 29, 2026
899d0e2
scope calm tool errors to interrupted assistant turns
morris2spears Jul 29, 2026
398a0c2
no-mistakes(review): hide remaining non-conversation Calm rows; fix t…
morris2spears Jul 30, 2026
c5dfdec
no-mistakes(test): fix stale Pi composer readiness probe in live e2e
morris2spears Jul 30, 2026
db07b77
drop leaked pi live e2e lab dir from the tree and ignore it
morris2spears Jul 30, 2026
2399c25
no-mistakes(document): note pi-tui Spacer seam in Calm compatibility …
morris2spears Jul 30, 2026
eb1b56c
no-mistakes(document): document /calm refusal outside trusted interac…
morris2spears Jul 30, 2026
d1aa516
no-mistakes(review): keep captain !bash rows visible under Calm
morris2spears Jul 30, 2026
f5c8025
no-mistakes(review): sync calm feasibility taxonomy with visible !bas…
morris2spears Jul 30, 2026
da76c82
no-mistakes(document): correct stale doc-audience counts in Calm feas…
morris2spears Jul 30, 2026
a4bdf58
no-mistakes(review): resolve Calm spacer hide policy lazily across re…
morris2spears Jul 30, 2026
44cd085
no-mistakes(document): record Calm spacer reload invariant and test e…
morris2spears Jul 30, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ config/tg-mode.env
config/cmux-socket-password
config/wedge-alarm
config/herdr-presentation-spaces
.pi-live-e2e.*
82 changes: 61 additions & 21 deletions .pi/extensions/fm-calm.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// Firstmate's home-persistent Pi transcript presentation toggle.
//
// Verified against Pi 0.81.1 and 0.82.0, which expose built-in ToolDefinitions, per-slot
// renderers, renderShell: "self", session_start replacement reasons,
// Verified against Pi 0.81.1 through 0.82.1, which expose built-in ToolDefinitions,
// per-slot renderers, renderShell: "self", session_start replacement reasons,
// ExtensionUIContext.setToolsExpanded(), setWorkingVisible(), and
// setHiddenThinkingLabel(). The focused tests pin those assumptions but never reject a
// newer Pi solely for its version. The collapsed-thinking and operational-user
// presentation adapters probe the exact API they patch and degrade independently with a
// diagnostic (see installCalmPresentationAdapter below) if a future Pi removes it; Pi
// still exposes no global renderer for arbitrary built-in or custom rows.
// newer Pi solely for its version. The assistant, tool-row, operational-user, and
// non-conversation row presentation adapters probe the exact API they patch and degrade
// independently with a diagnostic (see installCalmPresentationAdapter below) if a future Pi
// removes it.
// docs/configuration.md owns the home-local Calm preference contract.
import { randomUUID } from "node:crypto";
import {
Expand All @@ -21,6 +21,7 @@ import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type {
ExtensionAPI,
ExtensionContext,
ToolDefinition,
ToolRenderResultOptions,
} from "@earendil-works/pi-coding-agent";
Expand All @@ -36,7 +37,20 @@ import {
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 {
installCalmBranchSummaryLayout,
installCalmCacheNoticeLayout,
installCalmCompactionSummaryLayout,
installCalmCustomEntryLayout,
installCalmCustomMessageLayout,
installCalmLeadingSpacerLayout,
installCalmSkillInvocationLayout,
} from "./lib/fm-calm-nonconversation-layout.ts";
import { installCalmOperationalUserLayout } from "./lib/fm-calm-operational-user-layout.ts";
import {
installCalmToolErrorTurnBoundary,
installCalmToolLayout,
} from "./lib/fm-calm-tool-layout.ts";
import {
calmPresentationHides,
calmPresentationIsActive,
Expand Down Expand Up @@ -87,9 +101,24 @@ function installCalmPresentationAdapter(name: string, install: () => void): void
}
}

function isTrustedInteractiveContext(
ctx: Pick<ExtensionContext, "isProjectTrusted" | "mode">,
): boolean {
return ctx.mode === "tui" && ctx.isProjectTrusted();
}

export default function (pi: ExtensionAPI) {
installCalmPresentationAdapter("collapsed-thinking", installCalmAssistantLayout);
installCalmPresentationAdapter("tool-row", installCalmToolLayout);
installCalmPresentationAdapter("tool-error-turn", installCalmToolErrorTurnBoundary);
installCalmPresentationAdapter("operational-user-row", installCalmOperationalUserLayout);
installCalmPresentationAdapter("skill-invocation-row", installCalmSkillInvocationLayout);
installCalmPresentationAdapter("compaction-summary-row", installCalmCompactionSummaryLayout);
installCalmPresentationAdapter("branch-summary-row", installCalmBranchSummaryLayout);
installCalmPresentationAdapter("custom-message-row", installCalmCustomMessageLayout);
installCalmPresentationAdapter("custom-entry-row", installCalmCustomEntryLayout);
installCalmPresentationAdapter("cache-notice-row", installCalmCacheNoticeLayout);
installCalmPresentationAdapter("hidden-row-spacing", installCalmLeadingSpacerLayout);

let exportRendering = false;
let removeTerminalInputHandler: (() => void) | undefined;
Expand Down Expand Up @@ -125,6 +154,17 @@ export default function (pi: ExtensionAPI) {
stockExportRendering: exportRendering,
});
};
const redrawTranscript = (ctx: ExtensionContext): void => {
const expanded = ctx.ui.getToolsExpanded();
ctx.ui.setToolsExpanded(!expanded);
ctx.ui.setToolsExpanded(expanded);
};
const applyPresentation = (ctx: ExtensionContext, active: boolean): void => {
ctx.ui.setWorkingVisible(!active);
ctx.ui.setHiddenThinkingLabel(active ? "" : undefined);
ctx.ui.setStatus("firstmate-calm", undefined);
redrawTranscript(ctx);
};

registerFirstmateSyntheticPresentation(pi);

Expand Down Expand Up @@ -238,13 +278,16 @@ export default function (pi: ExtensionAPI) {

pi.on("session_start", (_event, ctx) => {
exportRendering = false;
setCalmPresentation(loadCalmPreference());
setCalmStockExportRendering(false);
publishPresentationState();
ctx.ui.setWorkingVisible(true);
ctx.ui.setHiddenThinkingLabel(calmPresentationIsActive() ? "" : undefined);
ctx.ui.setStatus("firstmate-calm", undefined);
removeTerminalInputHandler?.();
removeTerminalInputHandler = undefined;

const trustedInteractive = isTrustedInteractiveContext(ctx);
setCalmPresentation(trustedInteractive && loadCalmPreference());
publishPresentationState();
if (!trustedInteractive) return;

applyPresentation(ctx, calmPresentationIsActive());
removeTerminalInputHandler = ctx.ui.onTerminalInput((data) => {
if (!getKeybindings().matches(data, "tui.input.submit")) return;

Expand All @@ -264,27 +307,24 @@ export default function (pi: ExtensionAPI) {
exportRendering = false;
setCalmStockExportRendering(false);
publishPresentationState();
const expanded = ctx.ui.getToolsExpanded();
ctx.ui.setToolsExpanded(!expanded);
ctx.ui.setToolsExpanded(expanded);
redrawTranscript(ctx);
}, 0);
});
});

pi.registerCommand("calm", {
description: "Toggle Firstmate's supported conversation-only transcript presentation.",
handler: async (_args, ctx) => {
if (!isTrustedInteractiveContext(ctx)) {
ctx.ui.notify("Calm is available only in a trusted interactive Pi session.", "warning");
return;
}

const active = !calmPresentationIsActive();
persistCalmPreference(active);
setCalmPresentation(active);
publishPresentationState();
ctx.ui.setWorkingVisible(true);
ctx.ui.setHiddenThinkingLabel(active ? "" : undefined);
ctx.ui.setStatus("firstmate-calm", undefined);

const expanded = ctx.ui.getToolsExpanded();
ctx.ui.setToolsExpanded(!expanded);
ctx.ui.setToolsExpanded(expanded);
applyPresentation(ctx, active);
},
});
}
11 changes: 3 additions & 8 deletions .pi/extensions/lib/fm-calm-assistant-layout.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Verified against Pi 0.81.1 and 0.82.0, which export AssistantMessageComponent with an
// updateContent method. installCalmAssistantLayout() probes that exact method and throws
// Verified against Pi 0.81.1 through 0.82.1, 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.
import type { AssistantMessageComponent as PiAssistantMessageComponent } from "@earendil-works/pi-coding-agent";
Expand All @@ -9,8 +9,6 @@ import { calmPresentationHides } from "./fm-calm-visibility.ts";
type AssistantMessage = Parameters<PiAssistantMessageComponent["updateContent"]>[0];

type AssistantMessagePresentationState = {
hiddenThinkingLabel: string;
hideThinkingBlock: boolean;
lastMessage?: AssistantMessage;
};

Expand Down Expand Up @@ -49,10 +47,7 @@ export function installCalmAssistantLayout(): void {
message: AssistantMessage,
): void {
const state = this as unknown as AssistantMessagePresentationState;
const hideThinking =
state.hiddenThinkingLabel === "" &&
state.hideThinkingBlock &&
patch.hidesThinking();
const hideThinking = patch.hidesThinking();
const presentationMessage = hideThinking
? {
...message,
Expand Down
Loading
Loading