@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,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types and constants for vim-mode extension
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type Mode = 'normal' | 'insert' | 'visual' | 'visual-line';
|
|
6
|
+
export type CharMotion = 'f' | 'F' | 't' | 'T';
|
|
7
|
+
export type PendingMotion = CharMotion | null;
|
|
8
|
+
export type PendingOperator = 'd' | 'c' | 'y' | null;
|
|
9
|
+
|
|
10
|
+
export interface LastCharMotion {
|
|
11
|
+
motion: CharMotion;
|
|
12
|
+
char: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Normal mode key mappings: key -> escape sequence (or null for mode switch)
|
|
16
|
+
export const NORMAL_KEYS: Record<string, string | null> = {
|
|
17
|
+
h: '\x1b[D', // left
|
|
18
|
+
j: '\x1b[B', // down
|
|
19
|
+
k: '\x1b[A', // up
|
|
20
|
+
l: '\x1b[C', // right
|
|
21
|
+
'0': '\x01', // line start
|
|
22
|
+
$: null, // line end
|
|
23
|
+
x: null, // delete char (custom clipboard handling)
|
|
24
|
+
X: null, // delete char before cursor (custom clipboard handling)
|
|
25
|
+
D: null, // delete to end of line (custom clipboard handling)
|
|
26
|
+
C: null, // change to end of line (delete to end + insert mode)
|
|
27
|
+
S: null, // substitute line (delete line content + insert mode)
|
|
28
|
+
s: null, // substitute char (delete char + insert mode)
|
|
29
|
+
i: null, // insert mode
|
|
30
|
+
a: null, // append (insert + right)
|
|
31
|
+
A: null, // append at end of line
|
|
32
|
+
I: null, // insert at start of line
|
|
33
|
+
o: null, // open line below
|
|
34
|
+
O: null, // open line above
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Character motion keys that wait for a target character
|
|
38
|
+
export const CHAR_MOTION_KEYS = new Set<string>(['f', 'F', 't', 'T']);
|
|
39
|
+
|
|
40
|
+
// Escape sequences
|
|
41
|
+
export const ESC_LEFT = '\x1b[D';
|
|
42
|
+
export const ESC_RIGHT = '\x1b[C';
|
|
43
|
+
export const CTRL_A = '\x01'; // line start
|
|
44
|
+
export const CTRL_E = '\x05'; // line end
|
|
45
|
+
export const CTRL_K = '\x0b'; // kill to end of line
|
|
46
|
+
export const CTRL_R = '\x12'; // ctrl+r — readline redo trigger in vim layer
|
|
47
|
+
export const CTRL_UNDERSCORE = '\x1f'; // ctrl+_ — readline undo
|
|
48
|
+
export const CTRL_V = '\x16'; // ctrl+v — native clipboard paste (app.clipboard.pasteImage)
|
|
49
|
+
export const NEWLINE = '\n'; // newline character
|
|
50
|
+
export const ESC_UP = '\x1b[A'; // cursor up
|
|
51
|
+
export const ESC_DOWN = '\x1b[B'; // cursor down
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual-mode selection geometry.
|
|
3
|
+
*
|
|
4
|
+
* A visual selection is an anchor (where `v`/`V` was pressed) plus the live
|
|
5
|
+
* cursor. Both ends are inclusive, so the grapheme under the later end belongs
|
|
6
|
+
* to the selection. Character-wise selections span an absolute text range;
|
|
7
|
+
* line-wise selections span whole lines and ignore the columns.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getLineGraphemes } from './motions.js';
|
|
11
|
+
import type { Mode } from './types.js';
|
|
12
|
+
|
|
13
|
+
export type VisualMode = 'visual' | 'visual-line';
|
|
14
|
+
export type VisualPosition = { line: number; col: number };
|
|
15
|
+
|
|
16
|
+
export function isVisualMode(mode: Mode): mode is VisualMode {
|
|
17
|
+
return mode === 'visual' || mode === 'visual-line';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Negative when `a` precedes `b` in buffer order, positive when it follows. */
|
|
21
|
+
export function compareVisualPositions(a: VisualPosition, b: VisualPosition): number {
|
|
22
|
+
return a.line !== b.line ? a.line - b.line : a.col - b.col;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The two endpoints in buffer order, regardless of which one is the anchor. */
|
|
26
|
+
export function orderVisualEndpoints(
|
|
27
|
+
anchor: VisualPosition,
|
|
28
|
+
cursor: VisualPosition,
|
|
29
|
+
): { start: VisualPosition; end: VisualPosition } {
|
|
30
|
+
return compareVisualPositions(anchor, cursor) <= 0
|
|
31
|
+
? { start: anchor, end: cursor }
|
|
32
|
+
: { start: cursor, end: anchor };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Whole lines covered by a line-wise selection. */
|
|
36
|
+
export function getVisualLineRange(
|
|
37
|
+
anchor: VisualPosition,
|
|
38
|
+
cursor: VisualPosition,
|
|
39
|
+
): { startLine: number; endLine: number } {
|
|
40
|
+
return {
|
|
41
|
+
startLine: Math.min(anchor.line, cursor.line),
|
|
42
|
+
endLine: Math.max(anchor.line, cursor.line),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Exclusive end column for an inclusive selection end sitting at `col`.
|
|
48
|
+
* Grapheme-aware, so a selection ending on an emoji takes the whole cluster.
|
|
49
|
+
* A column at or past the end of the line yields the line length.
|
|
50
|
+
*/
|
|
51
|
+
export function getInclusiveEndColumn(line: string, col: number): number {
|
|
52
|
+
if (col >= line.length) return line.length;
|
|
53
|
+
const segment = getLineGraphemes(line).find((s) => col < s.end);
|
|
54
|
+
return segment ? segment.end : line.length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Keep a stale anchor inside the buffer after the text changed underneath it. */
|
|
58
|
+
export function clampVisualPosition(
|
|
59
|
+
position: VisualPosition,
|
|
60
|
+
lines: string[],
|
|
61
|
+
): VisualPosition {
|
|
62
|
+
const line = Math.max(0, Math.min(position.line, lines.length - 1));
|
|
63
|
+
const col = Math.max(0, Math.min(position.col, (lines[line] ?? '').length));
|
|
64
|
+
return { line, col };
|
|
65
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-local cache for Vim word motion boundaries.
|
|
3
|
+
*
|
|
4
|
+
* Keyed by semantic class + exact line content to avoid stale boundary reuse.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { WordMotionClass } from './motions.js';
|
|
8
|
+
|
|
9
|
+
export type WordMotionDirection = 'forward' | 'backward';
|
|
10
|
+
export type WordMotionTarget = 'start' | 'end';
|
|
11
|
+
|
|
12
|
+
enum CharType {
|
|
13
|
+
Space = 0,
|
|
14
|
+
Word = 1,
|
|
15
|
+
Other = 2,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WordBoundaryData {
|
|
19
|
+
readonly length: number;
|
|
20
|
+
readonly charTypes: Uint8Array;
|
|
21
|
+
readonly runStartByIndex: Int32Array;
|
|
22
|
+
readonly runEndByIndex: Int32Array;
|
|
23
|
+
readonly nextNonSpaceAtOrAfter: Int32Array;
|
|
24
|
+
readonly prevNonSpaceAtOrBefore: Int32Array;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getCharType(
|
|
28
|
+
ch: string | undefined,
|
|
29
|
+
semanticClass: WordMotionClass = 'word',
|
|
30
|
+
): CharType {
|
|
31
|
+
if (!ch || /\s/.test(ch)) return CharType.Space;
|
|
32
|
+
if (semanticClass === 'WORD') return CharType.Word;
|
|
33
|
+
if (/\w/.test(ch)) return CharType.Word;
|
|
34
|
+
return CharType.Other;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildWordBoundaryData(
|
|
38
|
+
line: string,
|
|
39
|
+
semanticClass: WordMotionClass = 'word',
|
|
40
|
+
): WordBoundaryData {
|
|
41
|
+
const len = line.length;
|
|
42
|
+
const charTypes = new Uint8Array(len);
|
|
43
|
+
const runStartByIndex = new Int32Array(len);
|
|
44
|
+
const runEndByIndex = new Int32Array(len);
|
|
45
|
+
const nextNonSpaceAtOrAfter = new Int32Array(len + 1);
|
|
46
|
+
const prevNonSpaceAtOrBefore = new Int32Array(len);
|
|
47
|
+
|
|
48
|
+
nextNonSpaceAtOrAfter.fill(-1);
|
|
49
|
+
prevNonSpaceAtOrBefore.fill(-1);
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < len; i++) {
|
|
52
|
+
charTypes[i] = getCharType(line[i], semanticClass);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (let runStart = 0; runStart < len; ) {
|
|
56
|
+
const runType = charTypes[runStart] ?? CharType.Space;
|
|
57
|
+
let runEnd = runStart;
|
|
58
|
+
while (runEnd + 1 < len && charTypes[runEnd + 1] === runType) {
|
|
59
|
+
runEnd++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (let i = runStart; i <= runEnd; i++) {
|
|
63
|
+
runStartByIndex[i] = runStart;
|
|
64
|
+
runEndByIndex[i] = runEnd;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
runStart = runEnd + 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let nextNonSpace = -1;
|
|
71
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
72
|
+
if (charTypes[i] !== CharType.Space) {
|
|
73
|
+
nextNonSpace = i;
|
|
74
|
+
}
|
|
75
|
+
nextNonSpaceAtOrAfter[i] = nextNonSpace;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let prevNonSpace = -1;
|
|
79
|
+
for (let i = 0; i < len; i++) {
|
|
80
|
+
if (charTypes[i] !== CharType.Space) {
|
|
81
|
+
prevNonSpace = i;
|
|
82
|
+
}
|
|
83
|
+
prevNonSpaceAtOrBefore[i] = prevNonSpace;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
length: len,
|
|
88
|
+
charTypes,
|
|
89
|
+
runStartByIndex,
|
|
90
|
+
runEndByIndex,
|
|
91
|
+
nextNonSpaceAtOrAfter,
|
|
92
|
+
prevNonSpaceAtOrBefore,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function findTargetInLine(
|
|
97
|
+
data: WordBoundaryData,
|
|
98
|
+
col: number,
|
|
99
|
+
direction: WordMotionDirection,
|
|
100
|
+
target: WordMotionTarget,
|
|
101
|
+
): number {
|
|
102
|
+
const len = data.length;
|
|
103
|
+
if (len === 0) return 0;
|
|
104
|
+
|
|
105
|
+
let i = Math.max(0, Math.min(col, len));
|
|
106
|
+
|
|
107
|
+
if (direction === 'forward') {
|
|
108
|
+
if (i >= len) return len;
|
|
109
|
+
|
|
110
|
+
if (target === 'start') {
|
|
111
|
+
if (data.charTypes[i] !== CharType.Space) {
|
|
112
|
+
i = (data.runEndByIndex[i] ?? i) + 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (i >= len) return len;
|
|
116
|
+
|
|
117
|
+
if (data.charTypes[i] === CharType.Space) {
|
|
118
|
+
const next = data.nextNonSpaceAtOrAfter[i] ?? -1;
|
|
119
|
+
return next === -1 ? len : next;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return i;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (i < len - 1) i++;
|
|
126
|
+
|
|
127
|
+
if (i >= len) return len;
|
|
128
|
+
|
|
129
|
+
if (data.charTypes[i] === CharType.Space) {
|
|
130
|
+
const next = data.nextNonSpaceAtOrAfter[i] ?? -1;
|
|
131
|
+
if (next === -1) return len;
|
|
132
|
+
i = next;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return data.runEndByIndex[i] ?? i;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (i >= len) i = len - 1;
|
|
139
|
+
if (i > 0) i--;
|
|
140
|
+
|
|
141
|
+
if (data.charTypes[i] === CharType.Space) {
|
|
142
|
+
const prev = data.prevNonSpaceAtOrBefore[i] ?? -1;
|
|
143
|
+
if (prev !== -1) i = prev;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return data.runStartByIndex[i] ?? i;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const DEFAULT_MAX_CACHE_ENTRIES = 256;
|
|
150
|
+
|
|
151
|
+
export class WordBoundaryCache {
|
|
152
|
+
private readonly entries = new Map<string, WordBoundaryData>();
|
|
153
|
+
private readonly maxEntries: number;
|
|
154
|
+
|
|
155
|
+
constructor(maxEntries: number = DEFAULT_MAX_CACHE_ENTRIES) {
|
|
156
|
+
this.maxEntries =
|
|
157
|
+
Number.isInteger(maxEntries) && maxEntries > 0
|
|
158
|
+
? maxEntries
|
|
159
|
+
: DEFAULT_MAX_CACHE_ENTRIES;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private makeCacheKey(line: string, semanticClass: WordMotionClass): string {
|
|
163
|
+
return `${semanticClass}\u0000${line}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get(line: string, semanticClass: WordMotionClass = 'word'): WordBoundaryData {
|
|
167
|
+
const key = this.makeCacheKey(line, semanticClass);
|
|
168
|
+
const cached = this.entries.get(key);
|
|
169
|
+
if (cached) return cached;
|
|
170
|
+
|
|
171
|
+
const built = buildWordBoundaryData(line, semanticClass);
|
|
172
|
+
|
|
173
|
+
if (this.entries.size >= this.maxEntries) {
|
|
174
|
+
const oldestKey = this.entries.keys().next().value;
|
|
175
|
+
if (oldestKey !== undefined) {
|
|
176
|
+
this.entries.delete(oldestKey);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.entries.set(key, built);
|
|
181
|
+
return built;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
tryFindTarget(
|
|
185
|
+
line: string,
|
|
186
|
+
col: number,
|
|
187
|
+
direction: WordMotionDirection,
|
|
188
|
+
target: WordMotionTarget,
|
|
189
|
+
semanticClass: WordMotionClass = 'word',
|
|
190
|
+
): number | null {
|
|
191
|
+
if (!Number.isInteger(col) || col < 0) return null;
|
|
192
|
+
|
|
193
|
+
const boundaries = this.get(line, semanticClass);
|
|
194
|
+
return findTargetInLine(boundaries, col, direction, target);
|
|
195
|
+
}
|
|
196
|
+
}
|