@aemonge-dev/pi-stardeck 0.0.0-stage → 0.6.0
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/LICENSE +21 -0
- package/README.md +240 -2
- package/demo/pi-stardeck.tape +59 -0
- package/docs/architecture.md +106 -0
- package/docs/assets/pi-stardeck.gif +0 -0
- package/docs/development.md +128 -0
- package/licenses/README.md +19 -0
- package/licenses/pi-vim-LICENSE +21 -0
- package/package.json +81 -4
- package/packages/footer/README.md +15 -0
- package/packages/footer/index.ts +37 -0
- package/packages/footer/package.json +32 -0
- package/packages/footer/src/protocol.ts +10 -0
- package/packages/footer/src/quota.ts +26 -0
- package/packages/footer/src/telemetry.ts +59 -0
- package/packages/pi-stardeck/README.md +220 -0
- package/packages/pi-stardeck/index.ts +691 -0
- package/packages/pi-stardeck/package.json +32 -0
- package/packages/pi-stardeck/src/activity-age.ts +184 -0
- package/packages/pi-stardeck/src/capabilities.ts +105 -0
- package/packages/pi-stardeck/src/deck.ts +807 -0
- package/packages/pi-stardeck/src/gauge.ts +1323 -0
- package/packages/pi-stardeck/src/git-refresh.ts +5 -0
- package/packages/pi-stardeck/src/process-resources.ts +428 -0
- package/packages/pi-stardeck/src/title-state.ts +195 -0
- package/packages/pi-stardeck/src/title.ts +443 -0
- package/packages/pi-vim-top-border/LICENSE +21 -0
- package/packages/pi-vim-top-border/LOCAL-CHANGES.md +71 -0
- package/packages/pi-vim-top-border/README.md +577 -0
- package/packages/pi-vim-top-border/clipboard-mirror.ts +451 -0
- package/packages/pi-vim-top-border/clipboard-policy.ts +23 -0
- package/packages/pi-vim-top-border/cursor-shape.ts +131 -0
- package/packages/pi-vim-top-border/index.ts +4195 -0
- package/packages/pi-vim-top-border/input-keys.ts +49 -0
- package/packages/pi-vim-top-border/mode-change-command.ts +120 -0
- package/packages/pi-vim-top-border/mode-colors.ts +97 -0
- package/packages/pi-vim-top-border/mode-label.ts +62 -0
- package/packages/pi-vim-top-border/motions.ts +297 -0
- package/packages/pi-vim-top-border/package.json +47 -0
- package/packages/pi-vim-top-border/prompt-external-editor.ts +152 -0
- package/packages/pi-vim-top-border/prompt-rail.ts +334 -0
- package/packages/pi-vim-top-border/prompt-status.ts +131 -0
- package/packages/pi-vim-top-border/settings.ts +287 -0
- package/packages/pi-vim-top-border/text-objects.ts +389 -0
- package/packages/pi-vim-top-border/types.ts +51 -0
- package/packages/pi-vim-top-border/visual.ts +65 -0
- package/packages/pi-vim-top-border/word-boundary-cache.ts +196 -0
- package/packages/status/README.md +398 -0
- package/packages/status/index.ts +1717 -0
- package/packages/status/package.json +33 -0
- package/packages/status/src/active-work.ts +235 -0
- package/packages/status/src/config.ts +218 -0
- package/packages/status/src/diagnostics.ts +238 -0
- package/packages/status/src/goal-block.ts +377 -0
- package/packages/status/src/goal.ts +170 -0
- package/packages/status/src/ledger.ts +101 -0
- package/packages/status/src/openspec.ts +394 -0
- package/packages/status/src/orchestration.ts +237 -0
- package/packages/status/src/publisher.ts +1294 -0
- package/packages/status/src/reducer.ts +190 -0
- package/packages/status/src/runtime-runs.ts +267 -0
- package/packages/status/src/state.ts +178 -0
- package/packages/status/src/title-activity.ts +224 -0
- package/packages/status/src/title-state.ts +218 -0
- package/packages/status/src/turn-cost.ts +63 -0
- package/packages/status/src/types.ts +228 -0
- package/src/deck-surface.ts +46 -0
- package/src/health.ts +227 -0
- package/src/index.ts +27 -0
- package/src/pi-status.ts +334 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
|
+
|
|
3
|
+
import type { HeaderActivity, HeaderActivitySegment, HeaderWork } from './gauge.ts';
|
|
4
|
+
|
|
5
|
+
export const ACTIVE_STATUS_ICON = '';
|
|
6
|
+
export const PASSIVE_STATUS_ICON = '';
|
|
7
|
+
export const MAJOR_SEPARATOR = '⟩';
|
|
8
|
+
export const MINOR_SEPARATOR = '›';
|
|
9
|
+
export const FOCUS_ICON = MAJOR_SEPARATOR;
|
|
10
|
+
export const LIFECYCLE_CELL_BUDGET = Math.max(
|
|
11
|
+
visibleWidth(`${ACTIVE_STATUS_ICON} understand`),
|
|
12
|
+
visibleWidth(`${PASSIVE_STATUS_ICON} understand`),
|
|
13
|
+
);
|
|
14
|
+
const MAJOR_PREFIX = ` ${MAJOR_SEPARATOR} `;
|
|
15
|
+
const MINOR_PREFIX = ` ${MINOR_SEPARATOR} `;
|
|
16
|
+
const MIN_FOCUS_TEXT_CELLS = 4;
|
|
17
|
+
const ANSI_SGR = /\u001b\[[0-?]*[ -/]*[@-~]/gu;
|
|
18
|
+
|
|
19
|
+
type ActivityLabels = { full: string; compact: string };
|
|
20
|
+
|
|
21
|
+
const ACTIVITY_LABELS: Record<HeaderActivity['kind'], ActivityLabels> = {
|
|
22
|
+
understanding: { full: 'interpreting current request', compact: 'interpreting' },
|
|
23
|
+
inspection: { full: 'inspecting project context', compact: 'inspection' },
|
|
24
|
+
synthesis: { full: 'finalizing current request', compact: 'finalizing' },
|
|
25
|
+
planning: { full: 'preparing implementation plan', compact: 'planning' },
|
|
26
|
+
'skill-proposal': {
|
|
27
|
+
full: 'preparing skill proposal',
|
|
28
|
+
compact: 'skill proposal',
|
|
29
|
+
},
|
|
30
|
+
mutation: { full: 'updating selected files', compact: 'update' },
|
|
31
|
+
implementation: {
|
|
32
|
+
full: 'implementing selected change',
|
|
33
|
+
compact: 'implementation',
|
|
34
|
+
},
|
|
35
|
+
'change-review': {
|
|
36
|
+
full: 'reviewing applied changes',
|
|
37
|
+
compact: 'reviewing changes',
|
|
38
|
+
},
|
|
39
|
+
regression: { full: 'running regression', compact: 'regression' },
|
|
40
|
+
verification: {
|
|
41
|
+
full: 'running focused verification',
|
|
42
|
+
compact: 'verification',
|
|
43
|
+
},
|
|
44
|
+
'result-review': {
|
|
45
|
+
full: 'reviewing check results',
|
|
46
|
+
compact: 'reviewing results',
|
|
47
|
+
},
|
|
48
|
+
recovery: { full: 'repairing bounded failure', compact: 'repair' },
|
|
49
|
+
'awaiting-validation': {
|
|
50
|
+
full: 'Human validation',
|
|
51
|
+
compact: 'validation',
|
|
52
|
+
},
|
|
53
|
+
'awaiting-input': {
|
|
54
|
+
full: 'next direction',
|
|
55
|
+
compact: 'input',
|
|
56
|
+
},
|
|
57
|
+
'operation-aborted': {
|
|
58
|
+
full: 'operation aborted',
|
|
59
|
+
compact: 'aborted',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const DISPLAY_LIFECYCLES: Readonly<Record<string, string>> = {
|
|
64
|
+
understanding: 'understand',
|
|
65
|
+
working: 'work',
|
|
66
|
+
waiting: 'wait',
|
|
67
|
+
assuring: 'assure',
|
|
68
|
+
learning: 'learn',
|
|
69
|
+
answering: 'answer',
|
|
70
|
+
blocked: 'blocked',
|
|
71
|
+
listening: 'ready',
|
|
72
|
+
aborted: 'stopped',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const ACTIVE_LIFECYCLES = new Set([
|
|
76
|
+
'understanding',
|
|
77
|
+
'working',
|
|
78
|
+
'assuring',
|
|
79
|
+
'learning',
|
|
80
|
+
'answering',
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
export type LifecycleTitleLayout = {
|
|
84
|
+
anchor: string;
|
|
85
|
+
lifecycle: string;
|
|
86
|
+
activity: string;
|
|
87
|
+
focusBoundary: string;
|
|
88
|
+
focus: string;
|
|
89
|
+
focusDetail: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const TITLE_SECTION_COLORS = {
|
|
93
|
+
anchor: 'accent',
|
|
94
|
+
lifecycle: 'accent',
|
|
95
|
+
activity: 'accent',
|
|
96
|
+
focusBoundary: 'thinkingHigh',
|
|
97
|
+
focus: 'success',
|
|
98
|
+
focusDetail: 'success',
|
|
99
|
+
emptyFocus: 'dim',
|
|
100
|
+
} as const;
|
|
101
|
+
|
|
102
|
+
const EMPTY_FOCUS_LABELS = new Set(['no focus', 'no openspec', 'no focused task']);
|
|
103
|
+
|
|
104
|
+
type TitleSectionColor = 'accent' | 'thinkingHigh' | 'success' | 'dim';
|
|
105
|
+
|
|
106
|
+
export function lifecycleSectionColor(_lifecycle: string): 'accent' {
|
|
107
|
+
return TITLE_SECTION_COLORS.lifecycle;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function focusSectionColor(focus: string): 'success' | 'dim' {
|
|
111
|
+
return EMPTY_FOCUS_LABELS.has(focus.trim().toLowerCase())
|
|
112
|
+
? TITLE_SECTION_COLORS.emptyFocus
|
|
113
|
+
: TITLE_SECTION_COLORS.focus;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renderLifecycleTitleSections(
|
|
117
|
+
layout: LifecycleTitleLayout,
|
|
118
|
+
lifecycle: string,
|
|
119
|
+
_idle: boolean,
|
|
120
|
+
theme: {
|
|
121
|
+
fg: (color: TitleSectionColor, text: string) => string;
|
|
122
|
+
bold: (text: string) => string;
|
|
123
|
+
},
|
|
124
|
+
): string {
|
|
125
|
+
const focusColor = focusSectionColor(layout.focus);
|
|
126
|
+
return (
|
|
127
|
+
theme.fg(TITLE_SECTION_COLORS.anchor, theme.bold(layout.anchor)) +
|
|
128
|
+
theme.fg(lifecycleSectionColor(lifecycle), theme.bold(layout.lifecycle)) +
|
|
129
|
+
theme.fg(TITLE_SECTION_COLORS.activity, layout.activity) +
|
|
130
|
+
theme.fg(TITLE_SECTION_COLORS.focusBoundary, layout.focusBoundary) +
|
|
131
|
+
theme.fg(focusColor, theme.bold(layout.focus)) +
|
|
132
|
+
theme.fg(focusColor, layout.focusDetail)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function boundedWidth(width: number): number {
|
|
137
|
+
return Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function truncatePlainToWidth(text: string, width: number): string {
|
|
141
|
+
return truncateToWidth(text, width, '…').replace(ANSI_SGR, '');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function progressSuffix(
|
|
145
|
+
activity: Pick<HeaderActivitySegment, 'current' | 'total'>,
|
|
146
|
+
): string {
|
|
147
|
+
if (activity.current === undefined || activity.total === undefined) return '';
|
|
148
|
+
return ` ${activity.current}/${activity.total}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function activityLabels(activity: HeaderActivity): ActivityLabels {
|
|
152
|
+
const labels = ACTIVITY_LABELS[activity.kind];
|
|
153
|
+
const progress = progressSuffix(activity);
|
|
154
|
+
return {
|
|
155
|
+
full: `${labels.full}${progress}`,
|
|
156
|
+
compact: `${labels.compact}${progress}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function activitySegmentLabels(segment: HeaderActivitySegment): ActivityLabels {
|
|
161
|
+
const progress = progressSuffix(segment);
|
|
162
|
+
return {
|
|
163
|
+
full: `${segment.label}${progress}`,
|
|
164
|
+
compact: `${segment.compact ?? segment.label}${progress}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function effectiveActivityPathLabels(work: HeaderWork): ActivityLabels[] {
|
|
169
|
+
if (work.activityPath !== undefined) {
|
|
170
|
+
return work.activityPath.map(activitySegmentLabels);
|
|
171
|
+
}
|
|
172
|
+
return work.activity ? [activityLabels(work.activity)] : [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
type FittedFocus = { primary: string; detail: string };
|
|
176
|
+
|
|
177
|
+
function fitNarrativeTitleSections(
|
|
178
|
+
titles: readonly string[],
|
|
179
|
+
width: number,
|
|
180
|
+
): FittedFocus {
|
|
181
|
+
const available = boundedWidth(width);
|
|
182
|
+
if (available <= 0 || titles.length === 0) return { primary: '', detail: '' };
|
|
183
|
+
for (let start = 0; start < titles.length; start += 1) {
|
|
184
|
+
const visibleTitles = titles.slice(start);
|
|
185
|
+
const candidate = visibleTitles.join(' › ');
|
|
186
|
+
if (visibleWidth(candidate) <= available) {
|
|
187
|
+
return {
|
|
188
|
+
primary: visibleTitles[0] ?? '',
|
|
189
|
+
detail:
|
|
190
|
+
visibleTitles.length > 1
|
|
191
|
+
? `${MINOR_PREFIX}${visibleTitles.slice(1).join(MINOR_PREFIX)}`
|
|
192
|
+
: '',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
primary: truncatePlainToWidth(titles.at(-1) ?? '', available),
|
|
198
|
+
detail: '',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function fitNarrativeTitles(titles: readonly string[], width: number): string {
|
|
203
|
+
const fitted = fitNarrativeTitleSections(titles, width);
|
|
204
|
+
return `${fitted.primary}${fitted.detail}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function statusPrefix(work: HeaderWork, agentBusy: boolean): string {
|
|
208
|
+
const lifecycleIsActive = ACTIVE_LIFECYCLES.has(work.lifecycle.trim().toLowerCase());
|
|
209
|
+
return `${agentBusy && lifecycleIsActive ? ACTIVE_STATUS_ICON : PASSIVE_STATUS_ICON} `;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function lifecycleDisplayLabel(lifecycle: string): string {
|
|
213
|
+
const normalized = lifecycle.trim().toLowerCase();
|
|
214
|
+
return DISPLAY_LIFECYCLES[normalized] ?? normalized;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function fullLifecycle(work: HeaderWork, agentBusy: boolean): string {
|
|
218
|
+
return truncatePlainToWidth(
|
|
219
|
+
`${statusPrefix(work, agentBusy)}${lifecycleDisplayLabel(work.lifecycle)}`,
|
|
220
|
+
LIFECYCLE_CELL_BUDGET,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function compactLifecycle(work: HeaderWork, agentBusy: boolean): string {
|
|
225
|
+
return `${statusPrefix(work, agentBusy)}${truncatePlainToWidth(
|
|
226
|
+
lifecycleDisplayLabel(work.lifecycle),
|
|
227
|
+
10,
|
|
228
|
+
)}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function sectionLayout(
|
|
232
|
+
lifecycle: string,
|
|
233
|
+
activity = '',
|
|
234
|
+
focusBoundary = '',
|
|
235
|
+
focus = '',
|
|
236
|
+
focusDetail = '',
|
|
237
|
+
): LifecycleTitleLayout {
|
|
238
|
+
const prefix = [ACTIVE_STATUS_ICON, PASSIVE_STATUS_ICON]
|
|
239
|
+
.map((icon) => `${icon} `)
|
|
240
|
+
.find((candidate) => lifecycle.startsWith(candidate));
|
|
241
|
+
if (prefix) {
|
|
242
|
+
return {
|
|
243
|
+
anchor: prefix,
|
|
244
|
+
lifecycle: lifecycle.slice(prefix.length),
|
|
245
|
+
activity,
|
|
246
|
+
focusBoundary,
|
|
247
|
+
focus,
|
|
248
|
+
focusDetail,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
anchor: lifecycle,
|
|
253
|
+
lifecycle: '',
|
|
254
|
+
activity,
|
|
255
|
+
focusBoundary,
|
|
256
|
+
focus,
|
|
257
|
+
focusDetail,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function fitActivityFocus(
|
|
262
|
+
lifecycle: string,
|
|
263
|
+
activity: string,
|
|
264
|
+
titles: readonly string[],
|
|
265
|
+
width: number,
|
|
266
|
+
): LifecycleTitleLayout | null {
|
|
267
|
+
const activityTail = `${MINOR_PREFIX}${activity}`;
|
|
268
|
+
const coreWidth = visibleWidth(lifecycle) + visibleWidth(activityTail);
|
|
269
|
+
if (coreWidth > width) return null;
|
|
270
|
+
|
|
271
|
+
const focusWidth = width - coreWidth - visibleWidth(MAJOR_PREFIX);
|
|
272
|
+
const focus =
|
|
273
|
+
focusWidth >= MIN_FOCUS_TEXT_CELLS
|
|
274
|
+
? fitNarrativeTitleSections(titles, focusWidth)
|
|
275
|
+
: { primary: '', detail: '' };
|
|
276
|
+
return sectionLayout(
|
|
277
|
+
lifecycle,
|
|
278
|
+
activityTail,
|
|
279
|
+
focus.primary ? MAJOR_PREFIX : '',
|
|
280
|
+
focus.primary,
|
|
281
|
+
focus.detail,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function fitStructuredActivityFocus(
|
|
286
|
+
lifecycle: string,
|
|
287
|
+
activities: readonly string[],
|
|
288
|
+
titles: readonly string[],
|
|
289
|
+
width: number,
|
|
290
|
+
requireFocus: boolean,
|
|
291
|
+
): LifecycleTitleLayout | null {
|
|
292
|
+
const activityTail = activities.map((label) => `${MINOR_PREFIX}${label}`).join('');
|
|
293
|
+
const coreWidth = visibleWidth(lifecycle) + visibleWidth(activityTail);
|
|
294
|
+
if (coreWidth > width) return null;
|
|
295
|
+
if (titles.length === 0) return sectionLayout(lifecycle, activityTail);
|
|
296
|
+
|
|
297
|
+
const focusWidth = width - coreWidth - visibleWidth(MAJOR_PREFIX);
|
|
298
|
+
const focus =
|
|
299
|
+
focusWidth >= MIN_FOCUS_TEXT_CELLS
|
|
300
|
+
? fitNarrativeTitleSections(titles, focusWidth)
|
|
301
|
+
: { primary: '', detail: '' };
|
|
302
|
+
if (requireFocus && !focus.primary) return null;
|
|
303
|
+
return sectionLayout(
|
|
304
|
+
lifecycle,
|
|
305
|
+
activityTail,
|
|
306
|
+
focus.primary ? MAJOR_PREFIX : '',
|
|
307
|
+
focus.primary,
|
|
308
|
+
focus.detail,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function distinctActivityVariants(labels: readonly ActivityLabels[]): string[][] {
|
|
313
|
+
const candidates: string[][] = [
|
|
314
|
+
labels.map((label) => label.full),
|
|
315
|
+
labels.map((label) => label.compact),
|
|
316
|
+
];
|
|
317
|
+
if (labels.length > 1) {
|
|
318
|
+
candidates.push(['…', labels.at(-1)?.full ?? '']);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const signatures = new Set<string>();
|
|
322
|
+
return candidates.filter((candidate) => {
|
|
323
|
+
const signature = candidate.join('\u0000');
|
|
324
|
+
if (signatures.has(signature)) return false;
|
|
325
|
+
signatures.add(signature);
|
|
326
|
+
return true;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function layoutStructuredActivityPath(
|
|
331
|
+
work: HeaderWork,
|
|
332
|
+
width: number,
|
|
333
|
+
agentBusy: boolean,
|
|
334
|
+
): LifecycleTitleLayout {
|
|
335
|
+
const labels = effectiveActivityPathLabels(work);
|
|
336
|
+
const lifecycle = fullLifecycle(work, agentBusy);
|
|
337
|
+
const variants = distinctActivityVariants(labels);
|
|
338
|
+
|
|
339
|
+
for (const variant of variants) {
|
|
340
|
+
const fitted = fitStructuredActivityFocus(
|
|
341
|
+
lifecycle,
|
|
342
|
+
variant,
|
|
343
|
+
work.titles,
|
|
344
|
+
width,
|
|
345
|
+
work.titles.length > 0,
|
|
346
|
+
);
|
|
347
|
+
if (fitted) return fitted;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (work.titles.length > 0) {
|
|
351
|
+
for (const variant of variants) {
|
|
352
|
+
const fitted = fitStructuredActivityFocus(lifecycle, variant, [], width, false);
|
|
353
|
+
if (fitted) return fitted;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const deepest = labels.at(-1);
|
|
358
|
+
const emergencyLifecycle = compactLifecycle(work, agentBusy);
|
|
359
|
+
if (deepest) {
|
|
360
|
+
for (const deepestLabel of [deepest.full, deepest.compact]) {
|
|
361
|
+
const fitted = fitStructuredActivityFocus(
|
|
362
|
+
emergencyLifecycle,
|
|
363
|
+
[deepestLabel],
|
|
364
|
+
[],
|
|
365
|
+
width,
|
|
366
|
+
false,
|
|
367
|
+
);
|
|
368
|
+
if (fitted) return fitted;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (visibleWidth(emergencyLifecycle) >= width) {
|
|
373
|
+
return sectionLayout(truncatePlainToWidth(emergencyLifecycle, width));
|
|
374
|
+
}
|
|
375
|
+
const remaining = width - visibleWidth(emergencyLifecycle);
|
|
376
|
+
if (!deepest || remaining <= visibleWidth(MINOR_PREFIX)) {
|
|
377
|
+
return sectionLayout(truncatePlainToWidth(emergencyLifecycle, width));
|
|
378
|
+
}
|
|
379
|
+
return sectionLayout(
|
|
380
|
+
emergencyLifecycle,
|
|
381
|
+
truncatePlainToWidth(`${MINOR_PREFIX}${deepest.compact}`, remaining),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function layoutLifecycleTitle(
|
|
386
|
+
work: HeaderWork,
|
|
387
|
+
width: number,
|
|
388
|
+
agentBusy = ACTIVE_LIFECYCLES.has(work.lifecycle.trim().toLowerCase()),
|
|
389
|
+
): LifecycleTitleLayout {
|
|
390
|
+
const available = boundedWidth(width);
|
|
391
|
+
if (available <= 0) {
|
|
392
|
+
return {
|
|
393
|
+
anchor: '',
|
|
394
|
+
lifecycle: '',
|
|
395
|
+
activity: '',
|
|
396
|
+
focusBoundary: '',
|
|
397
|
+
focus: '',
|
|
398
|
+
focusDetail: '',
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (work.activityPath && work.activityPath.length > 0) {
|
|
403
|
+
return layoutStructuredActivityPath(work, available, agentBusy);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!work.activity || work.activityPath !== undefined) {
|
|
407
|
+
const lifecycle = fullLifecycle(work, agentBusy);
|
|
408
|
+
const focus = fitNarrativeTitleSections(
|
|
409
|
+
work.titles,
|
|
410
|
+
Math.max(0, available - visibleWidth(lifecycle) - visibleWidth(MAJOR_PREFIX)),
|
|
411
|
+
);
|
|
412
|
+
return sectionLayout(
|
|
413
|
+
lifecycle,
|
|
414
|
+
'',
|
|
415
|
+
focus.primary ? MAJOR_PREFIX : '',
|
|
416
|
+
focus.primary,
|
|
417
|
+
focus.detail,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const lifecycle = fullLifecycle(work, agentBusy);
|
|
422
|
+
const labels = activityLabels(work.activity);
|
|
423
|
+
const full = fitActivityFocus(lifecycle, labels.full, work.titles, available);
|
|
424
|
+
if (full) return full;
|
|
425
|
+
|
|
426
|
+
const compactActivity = fitActivityFocus(lifecycle, labels.compact, [], available);
|
|
427
|
+
if (compactActivity) return compactActivity;
|
|
428
|
+
|
|
429
|
+
const emergencyLifecycle = compactLifecycle(work, agentBusy);
|
|
430
|
+
const emergency = fitActivityFocus(emergencyLifecycle, labels.compact, [], available);
|
|
431
|
+
if (emergency) return emergency;
|
|
432
|
+
|
|
433
|
+
if (visibleWidth(emergencyLifecycle) >= available) {
|
|
434
|
+
return sectionLayout(truncatePlainToWidth(emergencyLifecycle, available));
|
|
435
|
+
}
|
|
436
|
+
return sectionLayout(
|
|
437
|
+
emergencyLifecycle,
|
|
438
|
+
truncatePlainToWidth(
|
|
439
|
+
` · ${labels.compact}`,
|
|
440
|
+
available - visibleWidth(emergencyLifecycle),
|
|
441
|
+
),
|
|
442
|
+
);
|
|
443
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lajarre
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Local changes
|
|
2
|
+
|
|
3
|
+
This is a Galactica-managed local fork of [`pi-vim`](https://github.com/lajarre/pi-vim)
|
|
4
|
+
0.14.1.
|
|
5
|
+
|
|
6
|
+
## Prompt telemetry rails
|
|
7
|
+
|
|
8
|
+
The lifecycle header owns the `❯` prompt anchor. Compact status islands now float inside
|
|
9
|
+
the editor rails while each rail balances its own left and right groups:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
wait › Human validation ⟩ Implement compact telemetry islands on the prompt rails
|
|
13
|
+
─ ───────────────────────── 3/3 ⟩ 9/19 › 48/64 ──
|
|
14
|
+
│
|
|
15
|
+
─ 00:00'00 ───────────── 51.1% › 2 ⟩ ▤ 252M › 1% ⟩ $1.73 ──
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The top rail keeps the vi-mode glyph alone on the left, with live MCP availability and
|
|
19
|
+
work progress in one right-anchored island. The MCP segment uses the plug glyph ``,
|
|
20
|
+
stays visible as dim `0/0` before valid status arrives, yields first on narrow
|
|
21
|
+
terminals, and deliberately excludes Neovim LSP inventory because that is not an agent
|
|
22
|
+
capability. The bottom rail keeps the eight-cell foreground-activity chronometer on the
|
|
23
|
+
left, with context, compaction count, RAM, CPU, and session cost on the right. A major
|
|
24
|
+
separator splits context from process resources; the separator before cost uses the same
|
|
25
|
+
purple major-boundary color rather than inheriting the blue cost value. Telemetry is
|
|
26
|
+
right-aligned immediately before the closing `──`. The footer model group instead ends
|
|
27
|
+
with Ramona's bounded `⟨ ` avatar tail. The header remains unpadded; no cross-row
|
|
28
|
+
alignment consumes its working width. On narrow terminals, optional progress and process
|
|
29
|
+
resources disappear before mode, chronometer, or context. The chronometer is deep cyan
|
|
30
|
+
while foreground work is active and dim only while settled. Rail data is consumed from
|
|
31
|
+
the existing bounded `pi-fancy-footer:widget` snapshots; session cost is derived locally
|
|
32
|
+
from assistant usage already present in the active branch.
|
|
33
|
+
|
|
34
|
+
Mode words are replaced with single Nerd Font glyphs:
|
|
35
|
+
|
|
36
|
+
| Mode | Glyph | Theme token |
|
|
37
|
+
| -------------------- | ----- | -------------------- |
|
|
38
|
+
| Insert | `` | `success` |
|
|
39
|
+
| Normal | `` | `borderAccent` |
|
|
40
|
+
| Visual / Visual Line | `` | `customMessageLabel` |
|
|
41
|
+
| EX | `` | `warning` |
|
|
42
|
+
|
|
43
|
+
Pending commands do not expand the mode island; the single glyph remains stable while
|
|
44
|
+
the command is composed. Labels use foreground color only—no reverse-video or background
|
|
45
|
+
block. Mode remains alone at the top-left edge.
|
|
46
|
+
|
|
47
|
+
## Integrated Neovim prompt handoff
|
|
48
|
+
|
|
49
|
+
The configured `app.editor.external` key is intercepted inside the modal editor before
|
|
50
|
+
Pi's blocking external-editor handler. The prompt is round-tripped through the
|
|
51
|
+
configured `externalEditor` while Pi's TUI remains live, so normal edits do not print
|
|
52
|
+
launch/resume messages. During handoff the inactive Pi prompt border uses the theme's
|
|
53
|
+
`dim` color; the mode color returns when Neovim closes. Neovim owns terminal-row
|
|
54
|
+
reservation so the PTY receives a real resize instead of relying on a Pi widget. Prompt
|
|
55
|
+
files live only in private temporary directories, and the `PI_NVIM_BRIDGE` descriptor is
|
|
56
|
+
inherited by the launcher rather than written or passed as an argument.
|
|
57
|
+
|
|
58
|
+
## Portable clipboard helper
|
|
59
|
+
|
|
60
|
+
Clipboard reads and writes use self-contained platform commands (`wl-copy` / `wl-paste`,
|
|
61
|
+
X11 tools, macOS tools, Windows PowerShell, or Termux) with OSC 52 fallback for remote
|
|
62
|
+
sessions. The fork therefore does not require a physical
|
|
63
|
+
`@earendil-works/pi-coding-agent` installation just to resolve clipboard code.
|
|
64
|
+
|
|
65
|
+
## Updating
|
|
66
|
+
|
|
67
|
+
This package is linked from Galactica into `~/.pi/agent/packages` and loaded by the
|
|
68
|
+
relative settings entry `./packages/pi-vim-top-border`. Do not overwrite it with
|
|
69
|
+
`pi update`. To adopt a newer upstream release, copy the newer source into this managed
|
|
70
|
+
directory, reapply the focused local changes, and run diagnostics plus the interactive
|
|
71
|
+
render and clipboard smoke tests.
|