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
9 changes: 6 additions & 3 deletions .pi/extensions/fm-calm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,16 +161,19 @@ export default function (pi: ExtensionAPI) {
return;
}

// Arm stock HTML rendering only for the export/share pass itself.
// Pi 0.84+ showStatus replaces the previous status row in place, so a
// post-export setToolsExpanded toggle would overwrite "Session exported
// to: ..." with "Tool output: expanded/collapsed". Export does not re-
// render the live transcript, so resetting the flag is enough; /calm
// still uses the tools-expanded redraw when presentation actually changes.
exportRendering = true;
setCalmStockExportRendering(true);
publishPresentationState();
setTimeout(() => {
exportRendering = false;
setCalmStockExportRendering(false);
publishPresentationState();
const expanded = ctx.ui.getToolsExpanded();
ctx.ui.setToolsExpanded(!expanded);
ctx.ui.setToolsExpanded(expanded);
}, 0);
});
});
Expand Down
3 changes: 2 additions & 1 deletion docs/calm-mode-feasibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ Calm classifies only at Pi's transcript-presentation owner through the canonical

The session-start nudge already originates as a non-displayed custom message, so it remains on that existing path while retaining model context and session persistence.
Legacy Calm custom entries and messages remain in existing session artifacts, and their presentation entry still uses the supported zero-height renderer while active.
Cycling tool expansion and restoring its original value rebuilds controllable rows and leaves final `Ctrl+O` state unchanged.
Cycling tool expansion and restoring its original value rebuilds controllable rows and leaves final `Ctrl+O` state unchanged, and only genuine presentation changes such as the `/calm` toggle use that redraw.
The `/export` and `/share` pass arms stock rendering only for the pass itself and never cycles tool expansion: export does not re-render the live transcript, and Pi 0.84+ `showStatus` replaces the prior status row in place, so a post-export toggle would overwrite the exported-file status; `tests/fm-calm-pi-extension.test.sh` fails if the export or share path calls `setToolsExpanded`.
Exported and shared HTML retain genuine user prompts, genuine assistant responses, current operational user messages, ordinary tool rendering, and the complete session artifact.
Serialized session data and Pi 0.81.1's sidebar tree also retain legacy hidden operational custom messages.

Expand Down
44 changes: 34 additions & 10 deletions tests/fm-calm-pi-extension.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ wait_for_text() {
# Include recent scrollback: expanding a long restored transcript can move
# the asserted tool output above the current viewport while the footer and
# editor remain visible.
tmux -L "$TMUX_SOCKET" capture-pane -p -t "$TMUX_SESSION" -S -600 >"$file" 2>/dev/null || true
# -J joins soft-wrapped lines. Pi 0.84+ truecolor/layout can leave short
# assistant rows invisible to plain capture-pane while join still sees them.
tmux -L "$TMUX_SOCKET" capture-pane -p -J -t "$TMUX_SESSION" -S -600 >"$file" 2>/dev/null || true
grep -Fq "$text" "$file" 2>/dev/null && return 0
sleep 0.05
i=$((i + 1))
Expand Down Expand Up @@ -1200,6 +1202,7 @@ let editorText = "";
let terminalInputHandler;
let workingVisible;
let hiddenThinkingLabel = "unset";
let toolsExpandedCalls = 0;
const statuses = new Map();
const sessionEntries = [{ type: "message", message: { role: "toolResult", content: "kept" } }];
const entriesBefore = JSON.stringify(sessionEntries);
Expand All @@ -1224,6 +1227,7 @@ const commandContext = {
statuses.set(key, value);
},
setToolsExpanded(value) {
toolsExpandedCalls += 1;
expanded = value;
for (const row of rows) row.actual.setExpanded(value);
watchActual.setExpanded(value);
Expand Down Expand Up @@ -1340,6 +1344,7 @@ for (const { name, actual } of rows) {
}
}
async function assertStockHtmlRendering(command, submitData) {
const toolsExpandedBefore = toolsExpandedCalls;
editorText = command;
terminalInputHandler(submitData);
const htmlRenderer = createToolHtmlRenderer({
Expand Down Expand Up @@ -1367,6 +1372,11 @@ async function assertStockHtmlRendering(command, submitData) {
}
editorText = "";
await new Promise((resolve) => setTimeout(resolve, 0));
// Regression: Pi 0.84+ showStatus replaces the prior status row in place, so a
// post-export setToolsExpanded toggle clobbers "Session exported to: ...".
if (toolsExpandedCalls !== toolsExpandedBefore) {
throw new Error(`${command} export/share path must not call setToolsExpanded (would clobber export status)`);
}
}

await assertStockHtmlRendering("/export calm.html", "\r");
Expand Down Expand Up @@ -1497,7 +1507,7 @@ JS
}

test_operational_followup_turn_e2e() {
local project home config sessions version label case_name calm_state expected_notifications session_file pane i captain_line handled_line geometry_gap exact_session
local project home config sessions version label case_name calm_state expected_notifications session_file pane i captain_line handled_line geometry_gap exact_session captain_count
if ! command -v pi >/dev/null 2>&1 || ! command -v tmux >/dev/null 2>&1; then
echo "skip: pi or tmux not found for Pi operational follow-up E2E"
return 0
Expand Down Expand Up @@ -1674,7 +1684,7 @@ TS
"cd '$project' && env FM_HOME='$home' PI_CODING_AGENT_DIR='$config' FM_OPERATIONAL_INPUT_SCRIPT='$OPERATIONAL_INPUT' PI_OFFLINE=1 pi --approve --no-context-files --no-skills --no-prompt-templates --no-extensions $extensions $session_arg; rc=\$?; printf '\nPI_EXIT=%s\n' \"\$rc\"; sleep 20"
i=0
while [ "$i" -lt 120 ]; do
pane=$(tmux -L "$TMUX_SOCKET" capture-pane -p -t "$TMUX_SESSION" -S - 2>/dev/null || true)
pane=$(tmux -L "$TMUX_SOCKET" capture-pane -p -J -t "$TMUX_SESSION" -S - 2>/dev/null || true)
printf '%s\n' "$pane" | grep -Fq 'followup-e2e.ts' && break
sleep 0.05
i=$((i + 1))
Expand All @@ -1697,9 +1707,23 @@ TS
fail "Pi follow-up $label case did not process the monitoring notification"
fi

pane=$(tmux -L "$TMUX_SOCKET" capture-pane -p -t "$TMUX_SESSION" -S - 2>/dev/null || true)
[ "$(printf '%s\n' "$pane" | grep -Fc "CAPTAIN_ANSWER_$label" || true)" -eq 1 ] \
|| fail "Pi follow-up $label case rendered a duplicate captain answer"
# Wait for the captain answer on the joined pane. Session completion alone is
# not enough: under Pi 0.84+ a plain capture can miss short assistant rows for
# a beat, and the old "duplicate" assertion treated count 0 as a product bug.
i=0
pane=""
captain_count=0
while [ "$i" -lt 80 ]; do
pane=$(tmux -L "$TMUX_SOCKET" capture-pane -p -J -t "$TMUX_SESSION" -S - 2>/dev/null || true)
captain_count=$(printf '%s\n' "$pane" | grep -Fc "CAPTAIN_ANSWER_$label" || true)
if [ "$captain_count" -ge 1 ] && printf '%s\n' "$pane" | grep -Fq "MONITOR_HANDLED_${label}_ONE"; then
break
fi
sleep 0.05
i=$((i + 1))
done
[ "$captain_count" -eq 1 ] \
|| fail "Pi follow-up $label case expected exactly one captain answer on screen, found $captain_count"
assert_contains "$pane" "CAPTAIN_PROMPT_$label" "Pi follow-up $label case hid the genuine captain prompt"
assert_contains "$pane" "MONITOR_HANDLED_${label}_ONE" "Pi follow-up $label case did not render the intended processing result"
if [ "$calm_state" = on ]; then
Expand Down Expand Up @@ -1802,7 +1826,7 @@ JS
"cd '$project' && env FM_HOME='$home' PI_CODING_AGENT_DIR='$config' FM_OPERATIONAL_INPUT_SCRIPT='$OPERATIONAL_INPUT' PI_OFFLINE=1 pi --approve --no-context-files --no-skills --no-prompt-templates --no-extensions -e ./.pi/extensions/fm-calm.ts -e ./followup-e2e.ts --session '$exact_session'; rc=\$?; printf '\nPI_EXIT=%s\n' \"\$rc\"; sleep 20"
i=0
while [ "$i" -lt 120 ]; do
pane=$(tmux -L "$TMUX_SOCKET" capture-pane -p -t "$TMUX_SESSION" -S - 2>/dev/null || true)
pane=$(tmux -L "$TMUX_SOCKET" capture-pane -p -J -t "$TMUX_SESSION" -S - 2>/dev/null || true)
printf '%s\n' "$pane" | grep -Fq 'MONITOR_HANDLED_exact_watcher_ONE' && break
sleep 0.05
i=$((i + 1))
Expand Down Expand Up @@ -3229,7 +3253,7 @@ JSON
while [ "$active_screen_wait" -lt 120 ]; do
# Include scrollback so earlier genuine content stays inspectable even when the
# transcript is long enough to push the original user prompt above the viewport.
tmux -L "$TMUX_SOCKET" capture-pane -p -t "$TMUX_SESSION" -S -600 >"$hidden_snapshot"
tmux -L "$TMUX_SOCKET" capture-pane -p -J -t "$TMUX_SESSION" -S -600 >"$hidden_snapshot"
# Wait for the redraw this block asserts: collapsed thinking hides, the built-in
# tool rows restored before this first-ever activation collapse with it, and the
# retained genuine rows are back on screen.
Expand Down Expand Up @@ -3274,7 +3298,7 @@ JSON
tmux -L "$TMUX_SOCKET" send-keys -t "$TMUX_SESSION" M-s
active_screen_wait=0
while [ "$active_screen_wait" -lt 120 ]; do
tmux -L "$TMUX_SOCKET" capture-pane -p -t "$TMUX_SESSION" >"$active_before_snapshot"
tmux -L "$TMUX_SOCKET" capture-pane -p -J -t "$TMUX_SESSION" >"$active_before_snapshot"
if grep -Fq "Warning: CALM_TRANSIENT_DIAGNOSTIC" "$active_before_snapshot" &&
! grep -Fq "/calm-diagnostic-e2e" "$active_before_snapshot"; then
break
Expand Down Expand Up @@ -3350,7 +3374,7 @@ for (const [needle, kind] of expected) {
JS
active_screen_wait=0
while [ "$active_screen_wait" -lt 120 ]; do
tmux -L "$TMUX_SOCKET" capture-pane -p -t "$TMUX_SESSION" >"$active_hidden_snapshot"
tmux -L "$TMUX_SOCKET" capture-pane -p -J -t "$TMUX_SESSION" >"$active_hidden_snapshot"
if grep -Fq " Error:" "$active_hidden_snapshot" &&
! grep -Fq "/calm-inject-e2e" "$active_hidden_snapshot"; then
break
Expand Down
Loading