pi-fold 4.1.3 → 4.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/extensions/active-context.ts +101 -36
- package/extensions/lib/folding.ts +7 -2
- package/extensions/lib/measurement.ts +3 -1
- package/extensions/lib/persistence.ts +34 -1
- package/extensions/lib/status-widget.ts +122 -157
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,14 +33,18 @@ The package registers one tool, `pi_fold_context`, with nine actions over the tr
|
|
|
33
33
|
|
|
34
34
|
### The fold bar
|
|
35
35
|
|
|
36
|
-
In an interactive session one
|
|
36
|
+
In an interactive session one compact, full-height bar sits directly above Pi's footer.
|
|
37
37
|
|
|
38
38
|
<picture>
|
|
39
39
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/shaneconner/fold/main/media/fold-bar-dark.svg">
|
|
40
|
-
<img alt="
|
|
40
|
+
<img alt="A single full-height usage bar. Batlow colours group consolidated folds, span folds, tool folds, pinned content, marked content, and raw context in that order. The suffix shows percentage, commit point, and fold, pin, and mark counts." src="https://raw.githubusercontent.com/shaneconner/fold/main/media/fold-bar-light.svg">
|
|
41
41
|
</picture>
|
|
42
42
|
|
|
43
|
-
The
|
|
43
|
+
The forty-column bar uses full-height half-blocks for eighty steps of fill, following the provider's measured count against the serving budget. Within that fill, colours show the estimated visible composition, grouped in this order: **Consolidated Folds, Span Folds, Tool Folds, Pinned, Marked, Raw**. Compressed content uses the first half of [Crameri's Batlow palette](https://www.fabiocrameri.ch/colourmaps/); still-raw content uses the second half. Foreground lightness is adjusted for dark and light backgrounds, and label colours match the bar. Each nonempty category keeps at least one half-cell when the measured fill has room; remaining width follows relative mass. This visibility floor makes small compressed categories visible, so individual category widths are not token-exact. At very low occupancy, the largest shares take the available space. There are no individual fold edges, scores, or height differences.
|
|
44
|
+
|
|
45
|
+
The suffix reads `34% · commit at 55% · N Folds (X Cons., Y Span, Z Tool, X Pin, Y Mark)`. `N Folds` counts visible compressed folds; `Pin` counts protected entries and `Mark` counts pending marks, not additional compressed folds. A tool fold is lossless compression of tool-result context, not a count of tool calls. `/fold-status` retains the detailed token and nesting figures.
|
|
46
|
+
|
|
47
|
+
Matching dashed guides show `minTarget`, where a commit aims to land, and `maxTarget`, where automatic folding is triggered. Before a measurement the row says `not measured yet`; after a commit it identifies the old reading until the provider measures again. Crossing the target is routine runtime work, not a user action item. On reload, saved session context supplies the fold counts and composition without a new model message; the next real context event replaces that read-only view. Unavailable composition is stated as `mapping`; if folding stops, `FOLDING STOPPED` replaces the bar. Non-truecolor terminals use the theme's own colours.
|
|
44
48
|
|
|
45
49
|
## Results
|
|
46
50
|
|
|
@@ -29,13 +29,13 @@ import {
|
|
|
29
29
|
peekFoldSource,
|
|
30
30
|
prepareFold,
|
|
31
31
|
projectActiveContext,
|
|
32
|
+
renderFold,
|
|
32
33
|
projectionSlateCandidates,
|
|
33
34
|
protectEvidence,
|
|
34
35
|
requireActiveFold,
|
|
35
36
|
selectAutomaticRung,
|
|
36
37
|
setFoldProjectionState,
|
|
37
38
|
withExpandLease,
|
|
38
|
-
renderFoldParts,
|
|
39
39
|
} from "./lib/folding.ts";
|
|
40
40
|
import {
|
|
41
41
|
admissionVerdict,
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
orderedRoots,
|
|
53
53
|
parseNativeCompactionCompletion,
|
|
54
54
|
exactMapped,
|
|
55
|
+
foldInterval,
|
|
55
56
|
parseNativeCompactionDecision,
|
|
56
57
|
parseProviderContextMeasurementReceipt,
|
|
57
58
|
persistenceProjection,
|
|
@@ -63,7 +64,7 @@ import {
|
|
|
63
64
|
stringIds,
|
|
64
65
|
toolPayload,
|
|
65
66
|
} from "./lib/measurement.ts";
|
|
66
|
-
import { renderFoldBar, type FoldBarModel } from "./lib/status-widget.ts";
|
|
67
|
+
import { emptyFoldBarMass, renderFoldBar, type FoldBarKind, type FoldBarModel } from "./lib/status-widget.ts";
|
|
67
68
|
import type {
|
|
68
69
|
NativeCompactionCompletionReceipt,
|
|
69
70
|
NativeCompactionDecisionReceipt,
|
|
@@ -89,6 +90,7 @@ import {
|
|
|
89
90
|
sameStateProjection,
|
|
90
91
|
semanticStateSha256,
|
|
91
92
|
topologySha256,
|
|
93
|
+
adoptDurableFoldRecords,
|
|
92
94
|
} from "./lib/persistence.ts";
|
|
93
95
|
import type { MaterializedStatePersistence } from "./lib/persistence.ts";
|
|
94
96
|
import {
|
|
@@ -716,7 +718,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
716
718
|
* named only when it is close enough to matter.
|
|
717
719
|
*/
|
|
718
720
|
/**
|
|
719
|
-
* THE FOLD BAR
|
|
721
|
+
* THE FOLD BAR. One coloured composition row directly above the footer,
|
|
720
722
|
* drawn by `renderFoldBar` from a model this function rebuilds on every status update.
|
|
721
723
|
* The component reads the model and the LIVE theme at render time, so it repaints on
|
|
722
724
|
* a theme switch, which the status string cannot; the host is asked to render after
|
|
@@ -729,8 +731,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
729
731
|
/** The provider measurement the last landed commit ran against; the reading is
|
|
730
732
|
* stale while it is still the latest one. */
|
|
731
733
|
staleSince: null as unknown,
|
|
732
|
-
/**
|
|
733
|
-
|
|
734
|
+
/** Display-only reconstruction while waiting for the first real context event. */
|
|
735
|
+
startupSnapshot: null as ActiveContextSnapshot | null,
|
|
734
736
|
};
|
|
735
737
|
const installFoldBar = (ctx: any): void => {
|
|
736
738
|
if (foldBar.installed || typeof ctx.ui?.setWidget !== "function") return;
|
|
@@ -738,6 +740,8 @@ export function registerActiveContext(pi: any, options: {
|
|
|
738
740
|
ctx.ui.setWidget(entryTypePrefix, (tui: any) => {
|
|
739
741
|
foldBar.requestRender = () => { try { tui.requestRender(); } catch { } };
|
|
740
742
|
return {
|
|
743
|
+
/** The model behind the row, for the lab and the gates. */
|
|
744
|
+
get model(): FoldBarModel | null { return foldBar.model; },
|
|
741
745
|
render(width: number): string[] {
|
|
742
746
|
if (!foldBar.model) return [];
|
|
743
747
|
try { return [renderFoldBar(foldBar.model, width, ctx.ui.theme)]; }
|
|
@@ -748,7 +752,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
748
752
|
}, { placement: "belowEditor" });
|
|
749
753
|
};
|
|
750
754
|
const foldBarModel = (ctx: any, input: {
|
|
751
|
-
share: number | null;
|
|
755
|
+
share: number | null; staged: number; weighed: boolean;
|
|
752
756
|
}): FoldBarModel => {
|
|
753
757
|
const state = persistence.state;
|
|
754
758
|
// THE SAME SNAPSHOT /fold-status PRICES AGAINST. `markFreedBytes` answers ZERO, silently,
|
|
@@ -757,46 +761,81 @@ export function registerActiveContext(pi: any, options: {
|
|
|
757
761
|
// "5 staged, 0 to free" beside a status command pricing the same marks at 41,877
|
|
758
762
|
// tokens (Shane, 2026-09-02, session 01a06272). The authoritative snapshot is memoized
|
|
759
763
|
// on the branch length, so reading it here costs nothing while nothing has changed.
|
|
760
|
-
let snapshot = lifecycle.latestSnapshot;
|
|
764
|
+
let snapshot = lifecycle.latestSnapshot ?? foldBar.startupSnapshot;
|
|
761
765
|
try { snapshot = authoritativeSnapshotFor(ctx); } catch { }
|
|
762
|
-
|
|
763
|
-
let placeholderChars = 0;
|
|
764
|
-
let hiddenChars = 0;
|
|
765
|
-
if (state && snapshot) {
|
|
766
|
-
for (const mark of pendingMarks(state)) stagedTokens += estimatedTokens(markFreedBytes(snapshot, state, mark));
|
|
767
|
-
for (const root of orderedRoots(state, snapshot)) placeholderChars += root.fold.placeholderChars;
|
|
768
|
-
// WHAT FOLDING BOUGHT, IN WINDOW TERMS. `sourceChars` is the transcript count, base64
|
|
769
|
-
// and all, and read as tokens it said "8.6M" over a 1M window. Each standing fold's
|
|
770
|
-
// source is priced by the image law once per fold OBJECT (folds are immutable and
|
|
771
|
-
// replaced, never mutated, so a WeakMap memo is exact) and summed over the whole
|
|
772
|
-
// forest, which is additive because a parent's source is its children's placeholders.
|
|
773
|
-
for (const fold of state.folds) {
|
|
774
|
-
let priced = foldBar.pricedSource.get(fold);
|
|
775
|
-
if (priced === undefined) {
|
|
776
|
-
const source = renderFoldParts(fold.parts, state, snapshot);
|
|
777
|
-
priced = source ? pricedBytes(source) : fold.sourceChars;
|
|
778
|
-
foldBar.pricedSource.set(fold, priced);
|
|
779
|
-
}
|
|
780
|
-
hiddenChars += Math.max(0, priced - fold.placeholderChars);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
const budget = input.budgetTokens > 0 ? input.budgetTokens : 1;
|
|
784
|
-
return {
|
|
766
|
+
const model: FoldBarModel = {
|
|
785
767
|
brand: brandNoun,
|
|
786
768
|
share: input.share,
|
|
787
769
|
commitShare: thresholds.maxTarget,
|
|
788
770
|
aimShare: thresholds.minTarget,
|
|
789
|
-
|
|
771
|
+
mapped: false, mass: emptyFoldBarMass(),
|
|
790
772
|
stagedMarks: input.staged,
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
hiddenTokens: estimatedTokens(hiddenChars),
|
|
773
|
+
folds: 0, foldSpans: 0, foldTruncations: 0, foldConsolidations: 0,
|
|
774
|
+
unplacedItems: 0,
|
|
775
|
+
pinnedRefs: state ? state.protected.length : 0,
|
|
795
776
|
weighed: input.weighed,
|
|
796
777
|
staleAfterCommit: input.share !== null && foldBar.staleSince !== null &&
|
|
797
778
|
foldBar.staleSince === measurements.lastProviderMeasurement,
|
|
798
779
|
stopped: ladder.automaticFailure?.message ?? null,
|
|
799
780
|
};
|
|
781
|
+
if (!state || !snapshot) return model;
|
|
782
|
+
model.mapped = true;
|
|
783
|
+
// Price the visible projection once per category, never a hidden fold's original
|
|
784
|
+
// source. Revealing a parent reveals its parts, not necessarily every descendant.
|
|
785
|
+
// Correct inclusive intervals prevent the last hidden message being priced as raw.
|
|
786
|
+
const visibleAt = new Map<number, { fold: ActiveFold; end: number }>();
|
|
787
|
+
const byId = new Map(state.folds.map((fold) => [fold.id, fold]));
|
|
788
|
+
const visit = (fold: ActiveFold, interval: { start: number; end: number } | null): void => {
|
|
789
|
+
if (!interval) { model.unplacedItems += 1; return; }
|
|
790
|
+
const revealed = state.expanded.includes(fold.id) || refsProtected(flattenFoldRefs(fold, state), state, snapshot);
|
|
791
|
+
if (revealed) {
|
|
792
|
+
for (const part of fold.parts) {
|
|
793
|
+
if (part.kind !== "fold") continue;
|
|
794
|
+
const child = byId.get(part.foldId);
|
|
795
|
+
if (child) visit(child, foldInterval(child, state, snapshot));
|
|
796
|
+
}
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
visibleAt.set(interval.start, { fold, end: interval.end });
|
|
800
|
+
model.folds += 1;
|
|
801
|
+
if (fold.kind === "chapter") model.foldSpans += 1;
|
|
802
|
+
else if (fold.kind === "tool-result") model.foldTruncations += 1;
|
|
803
|
+
else model.foldConsolidations += 1;
|
|
804
|
+
};
|
|
805
|
+
const roots = orderedRoots(state, snapshot);
|
|
806
|
+
const placedRoots = new Set(roots.map((root) => root.fold.id));
|
|
807
|
+
model.unplacedItems += state.folds.filter((fold) => !fold.parentId && !placedRoots.has(fold.id)).length;
|
|
808
|
+
for (const root of roots) visit(root.fold, root);
|
|
809
|
+
|
|
810
|
+
// Resolve all marks before walking roots: a pending consolidation can begin at a
|
|
811
|
+
// root and include raw gaps farther on. Membership, not an encountered start edge,
|
|
812
|
+
// determines Mark. A set also prevents overlapping marks charging a message twice.
|
|
813
|
+
const marked = new Set<number>();
|
|
814
|
+
for (const mark of pendingMarks(state)) {
|
|
815
|
+
const span = markSpanRefs(state, mark);
|
|
816
|
+
const indices = span.refs.map((ref) => exactMapped(snapshot, ref)?.index ?? -1);
|
|
817
|
+
if (span.unresolved || !indices.length || indices.some((index) => index < 0)) {
|
|
818
|
+
model.unplacedItems += 1;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
for (const index of indices) marked.add(index);
|
|
822
|
+
}
|
|
823
|
+
const pinnedKeys = new Set(state.protected.map(objectRefKey));
|
|
824
|
+
for (let index = 0; index < snapshot.messages.length; index += 1) {
|
|
825
|
+
const root = visibleAt.get(index);
|
|
826
|
+
if (root) {
|
|
827
|
+
const kind: FoldBarKind = root.fold.kind === "chapter" ? "span"
|
|
828
|
+
: root.fold.kind === "tool-result" ? "tool" : "consolidated";
|
|
829
|
+
model.mass[kind] += pricedBytes(renderFold(root.fold, state, snapshot) ?? []);
|
|
830
|
+
index = root.end; // inclusive: advance PAST the final hidden message
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const ref = snapshot.mapped[index]?.ref;
|
|
834
|
+
const kind: FoldBarKind = ref && pinnedKeys.has(objectRefKey(ref)) ? "pinned"
|
|
835
|
+
: marked.has(index) ? "marked" : "raw";
|
|
836
|
+
model.mass[kind] += pricedBytes([snapshot.messages[index]]);
|
|
837
|
+
}
|
|
838
|
+
return model;
|
|
800
839
|
};
|
|
801
840
|
|
|
802
841
|
const updateStatus = (ctx: any): void => {
|
|
@@ -810,7 +849,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
810
849
|
installFoldBar(ctx);
|
|
811
850
|
if (foldBar.installed) {
|
|
812
851
|
foldBar.model = foldBarModel(ctx, {
|
|
813
|
-
share,
|
|
852
|
+
share, staged,
|
|
814
853
|
weighed: ladder.bandTopMeasurement !== null &&
|
|
815
854
|
ladder.bandTopMeasurement === measurements.lastProviderMeasurement,
|
|
816
855
|
});
|
|
@@ -1041,6 +1080,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1041
1080
|
cancelPreparation();
|
|
1042
1081
|
lifecycle.latestSnapshot = null;
|
|
1043
1082
|
lifecycle.latestSnapshotError = null;
|
|
1083
|
+
foldBar.startupSnapshot = null;
|
|
1044
1084
|
measurements.latestRatio = null;
|
|
1045
1085
|
measurements.lastProviderMeasurement = null;
|
|
1046
1086
|
measurements.wallInflowSteps.length = 0;
|
|
@@ -1143,6 +1183,13 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1143
1183
|
);
|
|
1144
1184
|
measurements.latestRatio = contextUsageRatio(measurements.lastProviderMeasurement);
|
|
1145
1185
|
persistence.persisted = clone(durableRestored);
|
|
1186
|
+
// Reuse Pi's own reconstructed messages, including custom messages and branch
|
|
1187
|
+
// selection. This is ONLY a view for the bar: do not populate latestSnapshot,
|
|
1188
|
+
// advance the frontier, persist a projection, or pretend a provider event occurred.
|
|
1189
|
+
if (typeof ctx.ui?.setWidget === "function" && Array.isArray(restoredMessages)) {
|
|
1190
|
+
try { foldBar.startupSnapshot = snapshotForEvent(ctx, restoredMessages); }
|
|
1191
|
+
catch { /* Keep the honest mapping fallback if the host cannot supply a view. */ }
|
|
1192
|
+
}
|
|
1146
1193
|
if (restoreError) safeNotify(
|
|
1147
1194
|
ctx,
|
|
1148
1195
|
`Active-context state was ignored; Pi native context remains authoritative: ${String(restoreError)}`,
|
|
@@ -2528,6 +2575,11 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2528
2575
|
state: persistence.state!,
|
|
2529
2576
|
generation: generationAtStart,
|
|
2530
2577
|
});
|
|
2578
|
+
const adoption = adoptDurableFoldRecords(nextState.folds, persistence.persistedFoldRecords);
|
|
2579
|
+
if (adoption.adopted.length) {
|
|
2580
|
+
emit("context.adopt", { adopted: adoption.adopted.length, fold_ids: adoption.adopted.slice(0, 64) });
|
|
2581
|
+
return { preparedFold, nextState: { ...nextState, folds: adoption.folds } };
|
|
2582
|
+
}
|
|
2531
2583
|
return { preparedFold, nextState };
|
|
2532
2584
|
};
|
|
2533
2585
|
|
|
@@ -2769,6 +2821,16 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2769
2821
|
clips: [...(result.state.clips ?? []), ...clipAdditions] } };
|
|
2770
2822
|
}
|
|
2771
2823
|
}
|
|
2824
|
+
// A RE-CUT SPAN ADOPTS ITS DURABLE RECORD (persistence.ts, adoptDurableFoldRecords):
|
|
2825
|
+
// a fold minted here under an id the store already holds, with the same source, takes
|
|
2826
|
+
// the record's bytes before the projection is measured, so the persist path finds
|
|
2827
|
+
// equal digests where session 01a06746 found "Conflicting durable active-context
|
|
2828
|
+
// fold" and stopped folding for good.
|
|
2829
|
+
const adoption = adoptDurableFoldRecords(result.state.folds, persistence.persistedFoldRecords);
|
|
2830
|
+
if (adoption.adopted.length) {
|
|
2831
|
+
result = { ...result, state: { ...result.state, folds: adoption.folds } };
|
|
2832
|
+
emit("context.adopt", { adopted: adoption.adopted.length, fold_ids: adoption.adopted.slice(0, 64) });
|
|
2833
|
+
}
|
|
2772
2834
|
persistence.state = result.state;
|
|
2773
2835
|
const bytesAfter = pricedBytes(projectActiveContext(snapshot, persistence.state));
|
|
2774
2836
|
const freedBytes = Math.max(0, bytesBefore - bytesAfter);
|
|
@@ -2804,6 +2866,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
2804
2866
|
closing_consolidation_marks: closingAdded,
|
|
2805
2867
|
absorbed_wedges: wedges.absorbed.length,
|
|
2806
2868
|
clipped_results: clippedResults,
|
|
2869
|
+
adopted_records: adoption.adopted.length,
|
|
2807
2870
|
freed_bytes: freedBytes,
|
|
2808
2871
|
freed_tokens: estimatedTokens(freedBytes),
|
|
2809
2872
|
rewrite_tokens: rewriteTokens,
|
|
@@ -3143,6 +3206,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
3143
3206
|
try {
|
|
3144
3207
|
const snapshot = snapshotForEvent(ctx, event.messages);
|
|
3145
3208
|
lifecycle.latestSnapshot = snapshot;
|
|
3209
|
+
foldBar.startupSnapshot = null;
|
|
3146
3210
|
if (ladder.automaticFailure) {
|
|
3147
3211
|
ladder.automaticFailure.suppressedCallbacks = Math.min(
|
|
3148
3212
|
Number.MAX_SAFE_INTEGER,
|
|
@@ -4903,6 +4967,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
4903
4967
|
curation.receipts = [];
|
|
4904
4968
|
try { ctx.ui?.setStatus?.(entryTypePrefix, undefined); } catch { }
|
|
4905
4969
|
foldBar.model = null;
|
|
4970
|
+
foldBar.startupSnapshot = null;
|
|
4906
4971
|
foldBar.requestRender?.();
|
|
4907
4972
|
});
|
|
4908
4973
|
|
|
@@ -1181,12 +1181,17 @@ export function activeContextStatus(
|
|
|
1181
1181
|
sourceIdsTruncated: eligibleSourceIds.length > 64,
|
|
1182
1182
|
startId: eligibleSourceIds[0],
|
|
1183
1183
|
endId: eligibleSourceIds.at(-1),
|
|
1184
|
-
action: { action: "
|
|
1184
|
+
action: { action: "reboundary", ids: eligibleEndpoints },
|
|
1185
1185
|
} : null,
|
|
1186
1186
|
currentTurnRequiresBoundary: false,
|
|
1187
|
+
// EVERY ACTION ADVERTISED HERE IS ONE THE SCHEMA ACCEPTS (gate 168). This block named a
|
|
1188
|
+
// "fold" action with a 1,000-character brief long after the schema had replaced it
|
|
1189
|
+
// with reboundary and the cap had moved to the policy's; the model followed the page,
|
|
1190
|
+
// was refused by the schema, and had to read the schema to learn the real name.
|
|
1187
1191
|
actions: {
|
|
1188
1192
|
status: { action: "status", offset, limit },
|
|
1189
|
-
|
|
1193
|
+
reboundary: { action: "reboundary", ids: ["<source-or-fold-id>"] },
|
|
1194
|
+
brief: { action: "brief", id: "<pending-fold-id>", brief: `<factual brief, at most ${ACTIVE_CONTEXT_POLICY.maxBriefChars} characters>` },
|
|
1190
1195
|
pin: { action: "pin", ids: ["<source-or-fold-id>"] },
|
|
1191
1196
|
},
|
|
1192
1197
|
};
|
|
@@ -405,7 +405,9 @@ export function explicitProtectedMass(
|
|
|
405
405
|
let refs = 0;
|
|
406
406
|
for (const item of snapshot.mapped) {
|
|
407
407
|
if (!item.ref || !explicit.has(objectRefKey(item.ref))) continue;
|
|
408
|
-
|
|
408
|
+
// Priced, not counted: a pinned screenshot is one image against the pin cap and on
|
|
409
|
+
// the bar, not its base64 length (pricedBytes, gate 166's law).
|
|
410
|
+
total += pricedBytes([item.message]);
|
|
409
411
|
refs += 1;
|
|
410
412
|
}
|
|
411
413
|
return { bytes: total, refs };
|
|
@@ -789,11 +789,44 @@ export function parseActiveContextStateV2(value: unknown, sessionId: string): Ac
|
|
|
789
789
|
return parsed;
|
|
790
790
|
}
|
|
791
791
|
|
|
792
|
+
/**
|
|
793
|
+
* A RE-CUT SPAN ADOPTS ITS DURABLE RECORD (2026-09-04).
|
|
794
|
+
*
|
|
795
|
+
* A fold's id is derived from its parts, so the same span cut twice mints the same id,
|
|
796
|
+
* and a durable record under an id is immutable by design. Those two facts met in
|
|
797
|
+
* session 01a06746: a persistence projection on the first pass after a restart could
|
|
798
|
+
* not seat one consolidation root and dropped it with its 30 children, the spans read
|
|
799
|
+
* as raw, the frontier cut them again, and the re-cut folds differed from their records
|
|
800
|
+
* by 25 bytes of placeholder text. `normalizeFoldsForPersistedRecords` and the persist
|
|
801
|
+
* path both refused the conflict, folding suspended, and every restart walked into the
|
|
802
|
+
* same refusal because the durable state had already lost the folds.
|
|
803
|
+
*
|
|
804
|
+
* The record is the canonical fold for its source. A fold whose id has a record with
|
|
805
|
+
* the SAME kind and SAME source hash takes the record's bytes (brief, provenance,
|
|
806
|
+
* placeholder size, createdAt) and keeps only its own parent link, so the persist path
|
|
807
|
+
* finds equal digests and the session goes on. A record under the same id with a
|
|
808
|
+
* DIFFERENT source is a corrupt store and still refuses by name.
|
|
809
|
+
*/
|
|
810
|
+
export function adoptDurableFoldRecords(
|
|
811
|
+
folds: ActiveFold[],
|
|
812
|
+
records: Map<string, FoldRecordEntry>,
|
|
813
|
+
): { folds: ActiveFold[]; adopted: string[] } {
|
|
814
|
+
const adopted: string[] = [];
|
|
815
|
+
const next = folds.map((fold) => {
|
|
816
|
+
const record = records.get(fold.id);
|
|
817
|
+
if (!record || sameFoldRecordIdentity(fold, record.fold)) return fold;
|
|
818
|
+
if (record.fold.kind !== fold.kind || record.fold.sourceSha256 !== fold.sourceSha256) return fold;
|
|
819
|
+
adopted.push(fold.id);
|
|
820
|
+
return { ...clone(record.fold), parentId: fold.parentId };
|
|
821
|
+
});
|
|
822
|
+
return { folds: next, adopted };
|
|
823
|
+
}
|
|
824
|
+
|
|
792
825
|
export function normalizeFoldsForPersistedRecords(
|
|
793
826
|
folds: ActiveFold[],
|
|
794
827
|
records: Map<string, FoldRecordEntry>,
|
|
795
828
|
): ActiveFold[] {
|
|
796
|
-
const normalized = folds.map((fold) => {
|
|
829
|
+
const normalized = adoptDurableFoldRecords(folds, records).folds.map((fold) => {
|
|
797
830
|
const record = records.get(fold.id);
|
|
798
831
|
if (!record) return clone(fold);
|
|
799
832
|
if (!sameFoldRecordIdentity(fold, record.fold)) {
|
|
@@ -1,91 +1,55 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* THE FOLD BAR: one widget row above the footer, drawn by us rather than by the host.
|
|
3
|
-
*
|
|
4
|
-
* The always-on status line is a STRING handed to `ctx.ui.setStatus`, and everything a
|
|
5
|
-
* bar would need is denied to a string: the host sorts statuses by key and joins them,
|
|
6
|
-
* collapses runs of spaces, truncates from the right, and bakes ANSI that goes stale on a
|
|
7
|
-
* theme switch. `ctx.ui.setWidget` hands us a component instead: `render(width)` receives
|
|
8
|
-
* the terminal width, the row is ours alone, and the theme is read at render time so a
|
|
9
|
-
* `/theme` switch repaints it. Placed below the editor it sits directly above the footer,
|
|
10
|
-
* which is where Claude Code draws the same bar.
|
|
11
|
-
*
|
|
12
|
-
* WHAT THE BAR ARGUES: how full the window is against the two points where something
|
|
13
|
-
* happens to it. The fill is read against ticks ON the bar at the aim (minTarget) and the
|
|
14
|
-
* commit point (maxTarget). Inside the fill, oldest on the left: `▂` for the placeholders
|
|
15
|
-
* of standing folds, `▓` for staged raw spans (what the next commit collapses), `▇` for
|
|
16
|
-
* raw the ladder has not claimed. The label states what folding has already bought.
|
|
17
|
-
*
|
|
18
|
-
* THE BAR IS A FIXED WIDTH. A bar sized to the leftover width moves the commit tick
|
|
19
|
-
* between renders, and the tick is the landmark the whole row exists to place the fill
|
|
20
|
-
* against. What varies with the terminal is the label, cut from the right.
|
|
21
|
-
*
|
|
22
|
-
* WHAT IT WILL NOT DRAW: an unmeasured window (an empty bar is a guess drawn; law 2 of
|
|
23
|
-
* the human surfaces), a suspended session (no bar promising a commit beside FOLDING
|
|
24
|
-
* STOPPED; law 5), fold positions (a placeholder is a few hundred tokens against cells of
|
|
25
|
-
* tens of thousands), and fold depth, which /fold-editor shows with a row per fold.
|
|
26
|
-
*
|
|
27
|
-
* COLOUR: raw, staged and folded form an ordered ladder of how much of the window each
|
|
28
|
-
* state costs, so they are three classes sampled off Crameri's batlow, a perceptually
|
|
29
|
-
* uniform, colour-vision-deficiency-safe sequential map; on a 256-colour terminal they
|
|
30
|
-
* fall back to the theme's own names. Scaffolding (empty cells, the aim tick, the brand)
|
|
31
|
-
* stays on the theme's dim ink; the commit tick takes the theme's warning colour, which
|
|
32
|
-
* is what the host uses for its own context percentage past 70.
|
|
33
|
-
*/
|
|
34
|
-
|
|
1
|
+
/** One full-height usage bar, coloured by six disjoint content categories. */
|
|
35
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
36
3
|
|
|
37
4
|
export const FOLD_BAR_WIDTH = 40;
|
|
5
|
+
// The first half of Batlow is compressed; the second half is still raw.
|
|
6
|
+
export const FOLD_BAR_KINDS = ["consolidated", "span", "tool", "pinned", "marked", "raw"] as const;
|
|
7
|
+
export type FoldBarKind = typeof FOLD_BAR_KINDS[number];
|
|
8
|
+
export type FoldBarMass = Record<FoldBarKind, number>;
|
|
9
|
+
export const emptyFoldBarMass = (): FoldBarMass => ({ consolidated: 0, span: 0, tool: 0, pinned: 0, marked: 0, raw: 0 });
|
|
38
10
|
|
|
39
|
-
/** Everything the row reads. Built by the runtime on every status update. */
|
|
40
11
|
export interface FoldBarModel {
|
|
41
12
|
brand: string;
|
|
42
|
-
/** The provider's own count against the serving budget, or null when unmeasured. */
|
|
43
13
|
share: number | null;
|
|
44
|
-
/** maxTarget and minTarget of the live thresholds, as shares. */
|
|
45
14
|
commitShare: number;
|
|
46
15
|
aimShare: number;
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
stagedTokens: number;
|
|
51
|
-
/** Share of the budget the standing folds' placeholders occupy (estimated). */
|
|
52
|
-
foldedShare: number;
|
|
16
|
+
/** Estimated visible mass, counted exactly once, in the category order above. */
|
|
17
|
+
mass: FoldBarMass;
|
|
18
|
+
mapped: boolean;
|
|
53
19
|
folds: number;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
20
|
+
foldSpans: number;
|
|
21
|
+
foldTruncations: number;
|
|
22
|
+
foldConsolidations: number;
|
|
23
|
+
pinnedRefs: number;
|
|
24
|
+
stagedMarks: number;
|
|
25
|
+
unplacedItems: number;
|
|
57
26
|
weighed: boolean;
|
|
58
|
-
/** A commit landed after the count `share` reads; the number is from before it. */
|
|
59
27
|
staleAfterCommit: boolean;
|
|
60
|
-
/** The suspension message when automatic folding has stopped, else null. */
|
|
61
28
|
stopped: string | null;
|
|
62
29
|
}
|
|
63
|
-
|
|
64
|
-
/** The subset of pi's Theme the row uses. */
|
|
65
30
|
export interface FoldBarTheme {
|
|
66
|
-
fg(color: "dim" | "muted" | "text" | "warning" | "error", text: string): string;
|
|
31
|
+
fg(color: "dim" | "muted" | "text" | "warning" | "error" | "accent", text: string): string;
|
|
67
32
|
bold(text: string): string;
|
|
68
33
|
getColorMode?(): "truecolor" | "256color";
|
|
69
|
-
getFgAnsi?(color: "text"): string;
|
|
34
|
+
getFgAnsi?(color: "text" | "muted" | "accent"): string;
|
|
70
35
|
name?: string;
|
|
71
36
|
}
|
|
72
37
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
|
|
38
|
+
// Batlow samples at 0, .2, .4 | .6, .8, 1, in the requested category order.
|
|
39
|
+
// Static foreground lightness adjustments retain readable labels on dark/white
|
|
40
|
+
// reference backgrounds. No palette generator or new dependency ships.
|
|
41
|
+
const ON_DARK: Record<FoldBarKind, string> = {
|
|
42
|
+
consolidated: "#6C89C7", span: "#5B919F", tool: "#729261",
|
|
43
|
+
pinned: "#B38E2F", marked: "#FBA689", raw: "#FACCFA",
|
|
44
|
+
};
|
|
45
|
+
const ON_LIGHT: Record<FoldBarKind, string> = {
|
|
46
|
+
consolidated: "#011959", span: "#185562", tool: "#577647",
|
|
47
|
+
pinned: "#927012", marked: "#AA624A", raw: "#8F6891",
|
|
48
|
+
};
|
|
78
49
|
|
|
79
|
-
/**
|
|
80
|
-
* WHICH WAY THE BACKGROUND GOES, read off the theme's own text colour rather than its
|
|
81
|
-
* name: a theme whose body text is dark is drawn on a light background. pi hands the
|
|
82
|
-
* text colour out as a truecolor escape, and a theme that does not (256-colour mode,
|
|
83
|
-
* or a host without the accessor) falls back to the name, then to dark.
|
|
84
|
-
*/
|
|
85
50
|
export function lightBackground(theme: FoldBarTheme): boolean {
|
|
86
51
|
try {
|
|
87
|
-
const
|
|
88
|
-
const match = /38;2;(\d+);(\d+);(\d+)/.exec(ansi);
|
|
52
|
+
const match = /38;2;(\d+);(\d+);(\d+)/.exec(theme.getFgAnsi?.("text") ?? "");
|
|
89
53
|
if (match) {
|
|
90
54
|
const [r, g, b] = match.slice(1).map((v) => Number(v) / 255);
|
|
91
55
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.5;
|
|
@@ -93,116 +57,117 @@ export function lightBackground(theme: FoldBarTheme): boolean {
|
|
|
93
57
|
} catch { }
|
|
94
58
|
return /light/i.test(theme.name ?? "");
|
|
95
59
|
}
|
|
96
|
-
|
|
97
60
|
const truecolor = (hex: string, text: string): string => {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const b = parseInt(hex.slice(5, 7), 16);
|
|
101
|
-
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[39m`;
|
|
61
|
+
const rgb = [1, 3, 5].map((at) => parseInt(hex.slice(at, at + 2), 16));
|
|
62
|
+
return `\x1b[38;2;${rgb.join(";")}m${text}\x1b[39m`;
|
|
102
63
|
};
|
|
103
64
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
65
|
+
/** Two half-cell samples per terminal column. The measured fill never grows to fit
|
|
66
|
+
* inventory. Each nonempty CATEGORY gets one half-cell when there is room, then quotas
|
|
67
|
+
* follow relative mass. This is a visibility floor, not a per-fold width or token-exact
|
|
68
|
+
* proportion. Both halves of a guide column are reserved so it cannot erase a category.
|
|
69
|
+
*/
|
|
70
|
+
export function foldBarCells(model: FoldBarModel, width = FOLD_BAR_WIDTH): Array<FoldBarKind | "empty" | "unknown" | "tick"> {
|
|
71
|
+
const samples = 2 * width;
|
|
72
|
+
const filled = Math.max(0, Math.min(samples, Math.round((model.share ?? 0) * samples)));
|
|
73
|
+
const ticks = foldBarTicks(model, width);
|
|
74
|
+
const total = FOLD_BAR_KINDS.reduce((sum, kind) => sum + model.mass[kind], 0);
|
|
75
|
+
const cells: Array<FoldBarKind | "empty" | "unknown" | "tick"> = Array.from({ length: samples }, (_, i) =>
|
|
76
|
+
ticks.has(Math.floor(i / 2)) ? "tick" : i < filled ? "unknown" : "empty");
|
|
77
|
+
const slots = cells.flatMap((kind, i) => kind === "unknown" ? [i] : []);
|
|
78
|
+
if (!model.mapped || total <= 0 || !slots.length) return cells;
|
|
79
|
+
const allocation = FOLD_BAR_KINDS.filter((kind) => model.mass[kind] > 0)
|
|
80
|
+
.map((kind) => ({ kind, quota: model.mass[kind] * slots.length / total, count: 0 }));
|
|
81
|
+
if (slots.length < allocation.length) {
|
|
82
|
+
// At very low occupancy there may be fewer slots than kinds. Keep the largest
|
|
83
|
+
// shares rather than inventing occupancy; ties keep the declared category order.
|
|
84
|
+
for (const item of [...allocation].sort((a, b) => b.quota - a.quota).slice(0, slots.length)) item.count = 1;
|
|
85
|
+
} else {
|
|
86
|
+
for (const item of allocation) item.count = Math.max(1, Math.floor(item.quota));
|
|
87
|
+
let assigned = allocation.reduce((sum, item) => sum + item.count, 0);
|
|
88
|
+
while (assigned !== slots.length) {
|
|
89
|
+
const direction = assigned < slots.length ? 1 : -1;
|
|
90
|
+
const candidates = allocation.filter((item) => direction > 0 || item.count > 1);
|
|
91
|
+
candidates.sort((a, b) => direction * ((b.quota - b.count) - (a.quota - a.count)));
|
|
92
|
+
candidates[0].count += direction;
|
|
93
|
+
assigned += direction;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
let next = 0;
|
|
97
|
+
for (const item of allocation) {
|
|
98
|
+
for (let n = 0; n < item.count; n += 1) cells[slots[next++]] = item.kind;
|
|
132
99
|
}
|
|
133
100
|
return cells;
|
|
134
101
|
}
|
|
135
|
-
|
|
136
|
-
/** Tick positions: the cell whose right edge is the share. */
|
|
137
|
-
export function foldBarTicks(model: FoldBarModel, width: number = FOLD_BAR_WIDTH): Map<number, "aim" | "commit"> {
|
|
102
|
+
export function foldBarTicks(model: FoldBarModel, width = FOLD_BAR_WIDTH): Map<number, "aim" | "commit"> {
|
|
138
103
|
const at = (share: number): number => Math.max(0, Math.min(width - 1, Math.round(share * width) - 1));
|
|
139
|
-
|
|
140
|
-
ticks.set(at(model.aimShare), "aim");
|
|
141
|
-
ticks.set(at(model.commitShare), "commit");
|
|
142
|
-
return ticks;
|
|
104
|
+
return new Map([[at(model.aimShare), "aim"], [at(model.commitShare), "commit"]]);
|
|
143
105
|
}
|
|
144
|
-
|
|
145
|
-
/** The row without colour, for tests and for a host that strips ANSI. */
|
|
146
106
|
export function foldBarPlainText(model: FoldBarModel): string {
|
|
147
|
-
return renderFoldBar(model, Number.POSITIVE_INFINITY, {
|
|
148
|
-
fg: (_c, t) => t, bold: (t) => t,
|
|
149
|
-
});
|
|
107
|
+
return renderFoldBar(model, Number.POSITIVE_INFINITY, { fg: (_c, t) => t, bold: (t) => t });
|
|
150
108
|
}
|
|
151
109
|
|
|
152
110
|
export function renderFoldBar(model: FoldBarModel, width: number, theme: FoldBarTheme): string {
|
|
111
|
+
const cut = (text: string): string => truncateToWidth(text, width, theme.fg("dim", "..."));
|
|
112
|
+
const neutral = (text: string): string => theme.fg("text", text);
|
|
113
|
+
const muted = (text: string): string => theme.fg("muted", text);
|
|
153
114
|
const brand = theme.fg("dim", model.brand);
|
|
154
|
-
if (model.stopped) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return
|
|
161
|
-
|
|
162
|
-
width, theme.fg("dim", "..."));
|
|
163
|
-
}
|
|
164
|
-
const rich = theme.getColorMode?.() === "truecolor";
|
|
165
|
-
const BATLOW = lightBackground(theme) ? BATLOW_ON_LIGHT : BATLOW_ON_DARK;
|
|
166
|
-
const ink: Record<Cell, (text: string) => string> = {
|
|
167
|
-
raw: (t) => rich ? truecolor(BATLOW.raw, t) : theme.fg("muted", t),
|
|
168
|
-
staged: (t) => rich ? truecolor(BATLOW.staged, t) : theme.fg("text", t),
|
|
169
|
-
folded: (t) => rich ? truecolor(BATLOW.folded, t) : theme.fg("dim", t),
|
|
170
|
-
empty: (t) => theme.fg("dim", t),
|
|
115
|
+
if (model.stopped) return cut(`${brand} ${theme.fg("error", theme.bold("FOLDING STOPPED"))}` +
|
|
116
|
+
(model.share === null ? "" : neutral(` · ${Math.round(model.share * 100)}% full`)));
|
|
117
|
+
if (model.share === null) return cut(`${brand} ${muted(`not measured yet · folds automatically at ${Math.round(model.commitShare * 100)}%`)}`);
|
|
118
|
+
const palette = lightBackground(theme) ? ON_LIGHT : ON_DARK;
|
|
119
|
+
const ink = (kind: FoldBarKind, text: string): string => {
|
|
120
|
+
if (theme.getColorMode?.() === "truecolor") return truecolor(palette[kind], text);
|
|
121
|
+
if (kind === "pinned") return theme.fg("accent", text);
|
|
122
|
+
return theme.fg(kind === "raw" ? "muted" : "text", text);
|
|
171
123
|
};
|
|
172
124
|
const cells = foldBarCells(model);
|
|
173
|
-
const
|
|
125
|
+
const solid = (kind: typeof cells[number]): string => {
|
|
126
|
+
if (kind === "tick") return neutral(theme.bold("┆"));
|
|
127
|
+
if (kind === "empty") return theme.fg("dim", "░");
|
|
128
|
+
if (kind === "unknown") return muted("█");
|
|
129
|
+
return ink(kind, "█");
|
|
130
|
+
};
|
|
131
|
+
const background = (kind: FoldBarKind): string | null => {
|
|
132
|
+
if (theme.getColorMode?.() === "truecolor") return truecolor(palette[kind], "").replace("[38;", "[48;");
|
|
133
|
+
// Pi exposes the same theme ink in 256-colour mode. Convert its foreground escape
|
|
134
|
+
// to background, without assuming an RGB theme or leaking styles into the label.
|
|
135
|
+
const fg = theme.getFgAnsi?.(kind === "pinned" ? "accent" : kind === "raw" ? "muted" : "text") ?? "";
|
|
136
|
+
const extended = /^\x1b\[38;(?:2;\d+;\d+;\d+|5;\d+)m$/.test(fg);
|
|
137
|
+
if (extended) return fg.replace("[38;", "[48;");
|
|
138
|
+
const basic = /^\x1b\[(3[0-7]|9[0-7])m$/.exec(fg);
|
|
139
|
+
return basic ? `\x1b[${Number(basic[1]) + 10}m` : null;
|
|
140
|
+
};
|
|
174
141
|
let bar = "";
|
|
175
|
-
for (let i = 0; i < cells.length; i +=
|
|
176
|
-
const
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
142
|
+
for (let i = 0; i < cells.length; i += 2) {
|
|
143
|
+
const left = cells[i], right = cells[i + 1];
|
|
144
|
+
if (left === right) { bar += solid(left); continue; }
|
|
145
|
+
// One full-height left-half glyph, right colour supplied by the cell background:
|
|
146
|
+
// two colours in one column, never a notch, gap, score, or extra terminal column.
|
|
147
|
+
if (right === "empty") {
|
|
148
|
+
bar += left === "unknown" ? muted("▌") : ink(left as FoldBarKind, "▌");
|
|
149
|
+
} else {
|
|
150
|
+
const bg = background(right as FoldBarKind);
|
|
151
|
+
bar += bg ? `${bg}${ink(left as FoldBarKind, "▌")}\x1b[49m` : solid(left);
|
|
152
|
+
}
|
|
180
153
|
}
|
|
181
154
|
const pct = Math.round(model.share * 100);
|
|
182
155
|
const parts: string[] = [];
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (model.staleAfterCommit) {
|
|
189
|
-
parts.push(theme.fg("muted", `${pct}% before the commit`));
|
|
190
|
-
} else {
|
|
191
|
-
parts.push(theme.fg("text", `${pct}%`));
|
|
192
|
-
if (model.share < model.commitShare) parts.push(theme.fg("muted", `commit at ${Math.round(model.commitShare * 100)}%`));
|
|
193
|
-
else if (model.weighed) parts.push(theme.fg("muted", "commit held"));
|
|
194
|
-
else parts.push(theme.fg("warning", "COMMIT DUE"));
|
|
195
|
-
}
|
|
196
|
-
// A COUNT FOR WHAT IS STAGED, TOKENS FOR WHAT FOLDING BOUGHT. Both figures are priced
|
|
197
|
-
// by the image law now (measurement.ts pricedBytes): "5.8M to free" beside a 1M window
|
|
198
|
-
// was 83 screenshots' base64 counted as text (Shane 2026-09-03). The unit is written
|
|
199
|
-
// out because pi's k/M format read as megabytes.
|
|
200
|
-
if (model.stagedMarks > 0) parts.push(ink.staged(`${model.stagedMarks} staged`));
|
|
201
|
-
if (model.folds > 0) {
|
|
202
|
-
parts.push(ink.folded(`${model.folds} fold${model.folds === 1 ? "" : "s"} hide ${formatTokens(model.hiddenTokens)} tokens`));
|
|
156
|
+
if (model.staleAfterCommit) parts.push(muted(`${pct}% before the commit`));
|
|
157
|
+
else {
|
|
158
|
+
const when = model.share < model.commitShare ? `commit at ${Math.round(model.commitShare * 100)}%`
|
|
159
|
+
: model.weighed ? "commit held" : "at commit point";
|
|
160
|
+
parts.push(neutral(`${pct}%`), muted(when));
|
|
203
161
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
162
|
+
if (model.mapped) {
|
|
163
|
+
// N counts compressed folds. Pin/Mark are accompanying states, not added to N.
|
|
164
|
+
const counts = [
|
|
165
|
+
ink("consolidated", `${model.foldConsolidations} Cons.`), ink("span", `${model.foldSpans} Span`),
|
|
166
|
+
ink("tool", `${model.foldTruncations} Tool`), ink("pinned", `${model.pinnedRefs} Pin`),
|
|
167
|
+
ink("marked", `${model.stagedMarks} Mark`),
|
|
168
|
+
];
|
|
169
|
+
parts.push(neutral(`${model.folds} Folds (`) + counts.join(muted(", ")) + neutral(")"));
|
|
170
|
+
if (model.unplacedItems > 0) parts.push(muted(`${model.unplacedItems} not mapped`));
|
|
171
|
+
} else parts.push(muted("mapping"));
|
|
172
|
+
return cut(`${bar} ${parts.join(theme.fg("dim", " · "))}`);
|
|
208
173
|
}
|