compaction: the surface — settings row, two ruled dividers, summary dialog, send gate (round 14 slice 3)

- settings: compaction.thresholdTokens row on the Agent tab (owner pick)
  in a Context group — CommitInput mirroring the graph budget row, hint
  resolved through the SAME core resolver the trigger reads; PUT rejects
  below the 32k floor with the anti-churn rationale; own section
  (compaction) in SETTINGS_SECTIONS + the settings-signal union
- view fold: one marker → two dividers — the minimal MOMENT divider at
  the marker (retrying-message register, no rule lines) and the longer
  CUT divider emitted immediately before the first kept message
  (phase-divider geometry, chip carries '· Nk tok'); both carry the full
  summary payload; full history stays visible to the user
- components/compaction-divider.tsx: the dividers + summary dialog
  (pure exported body for SSR pins, the delete-dialog pattern; Esc +
  Close dismiss, backdrop does not)
- chat.tsx: compaction-changed SSE flips the running state — composer
  canSend, onSend/doSend/doSteerOrSend, and both docks' submit + hint
  ('compacting session…') gate on it (the server's registry mutex is the
  structural gate; this is the affordance); dialog mounts beside the
  abort dialog
- pins: settings-body (4), view fold (2), divider SSR (4)
This commit is contained in:
amosblomqvist
2026-10-05 22:51:08 +03:00
parent dd159cc8fb
commit ffc4df1938
14 changed files with 465 additions and 19 deletions
+3 -2
View File
@@ -575,8 +575,9 @@ function sanitizeGraph(raw: Record<string, unknown>): GraphSettings {
/** clamp-never-reject (the sanitizer's discipline): a finite positive
* number floors to COMPACTION_THRESHOLD_FLOOR, anything else drops (the
* factory default resolves at read time). */
const COMPACTION_THRESHOLD_FLOOR = 32_000;
* factory default resolves at read time). Exported for the PUT parser's
* reject-with-rationale (the graph chunk-budget precedent). */
export const COMPACTION_THRESHOLD_FLOOR = 32_000;
function sanitizeCompaction(raw: Record<string, unknown>): CompactionSettings {
const out: CompactionSettings = {};
+41
View File
@@ -35,6 +35,7 @@
import type { AssistantMessage, ToolResultMessage } from "@earendil-works/pi-ai";
import type { LhMarkerMessage, LhMessage } from "./native-messages.ts";
import { modelSolutionForConclude, PRACTICE_CONCLUDE_TOOL } from "./native-messages.ts";
import { activeCompactionOf, type CompactionDetails } from "./compaction.ts";
import { QUESTION_TOOLS } from "./pending.ts";
import type { SubagentArtifactData, SubagentResultMeta } from "./contracts/jobs.ts";
import type { PlanBlockData } from "./contracts/chat.ts";
@@ -145,9 +146,30 @@ export function projectView(messages: readonly LhMessage[], live?: LiveTurnState
return undefined;
};
// The active compaction (owner round 14): the fold emits the CUT
// DIVIDER — the longer, at-the-seam marker — immediately BEFORE the
// first kept message, so the transcript shows exactly where history
// was summarized away. Everything before it stays visible to the
// user (full history is never hidden); only the MODEL's context jumps.
const compaction = activeCompactionOf(messages);
for (const m of messages) {
const role = (m as { role?: string }).role;
if (compaction && m.id === compaction.details.firstKeptMessageId) {
out.push({
id: `cut_${compaction.id}`,
role: "user",
parts: [
{
type: "data-compaction-cut",
id: `cc_${compaction.id}`,
data: { content: compaction.content, details: compaction.details },
},
],
});
}
if (role === "user") {
const content = m.content as string | Array<{ type: string; text?: string }>;
const texts =
@@ -383,6 +405,25 @@ function projectMarker(marker: LhMarkerMessage, out: LhViewMessage[]): void {
// UI-only (the ask-style dismissal): the synthesized conclude
// pair — which rides the NEXT input — carries the record's face.
return;
case "compaction": {
// THE MOMENT DIVIDER (owner round 14): minimal centered small
// text at the compaction moment — where the user currently is
// (the marker appends at the record's tail). Clicking opens the
// full summary. The cut divider is emitted by the fold loop at
// the kept boundary (see projectView).
out.push({
id: marker.id,
role: "user",
parts: [
{
type: "data-compaction",
id: `cm_${marker.id}`,
data: { content: marker.content, details: marker.details as CompactionDetails },
},
],
});
return;
}
case "plan-block": {
// No message of its own: splice into the preceding assistant
// group, IN FLOW after the last successful plan_init call.