pi-cc-extensions 0.8.45 → 0.8.47

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.
@@ -1,113 +0,0 @@
1
- import {
2
- CURSOR_MARKER,
3
- truncateToWidth,
4
- visibleWidth,
5
- } from "@earendil-works/pi-tui"
6
-
7
- export interface FixedEditorClusterInput {
8
- width: number
9
- terminalRows: number
10
- statusLines?: string[]
11
- aboveWidgetLines?: string[]
12
- editorLines: string[]
13
- belowWidgetLines?: string[]
14
- footerLines?: string[]
15
- }
16
-
17
- export interface FixedEditorCursor {
18
- row: number
19
- col: number
20
- }
21
-
22
- export interface FixedEditorClusterRender {
23
- lines: string[]
24
- cursor: FixedEditorCursor | null
25
- }
26
-
27
- function normalizeLines(lines: string[] | undefined, width: number): string[] {
28
- if (!lines || width <= 0) return []
29
-
30
- return lines
31
- .filter((line) => line !== undefined && line !== null)
32
- .map((line) =>
33
- visibleWidth(line) > width
34
- ? truncateToWidth(line, width, "", true)
35
- : line,
36
- )
37
- }
38
-
39
- function takeTail(lines: string[], count: number): string[] {
40
- if (count <= 0) return []
41
- return lines.length <= count ? lines : lines.slice(lines.length - count)
42
- }
43
-
44
- function capEditorLines(lines: string[], count: number): string[] {
45
- if (count <= 0) return []
46
- if (lines.length <= count) return lines
47
-
48
- const cursorRow = lines.findIndex((line) => line.includes(CURSOR_MARKER))
49
- if (cursorRow !== -1) {
50
- const start = Math.max(
51
- 0,
52
- Math.min(cursorRow - count + 1, lines.length - count),
53
- )
54
- return lines.slice(start, start + count)
55
- }
56
-
57
- return lines.slice(lines.length - count)
58
- }
59
-
60
- function extractCursor(lines: string[]): FixedEditorClusterRender {
61
- let cursor: FixedEditorCursor | null = null
62
- const cleaned = lines.map((line, row) => {
63
- const markerIndex = line.indexOf(CURSOR_MARKER)
64
- if (markerIndex === -1) return line
65
-
66
- cursor ??= {
67
- row,
68
- col: visibleWidth(line.slice(0, markerIndex)),
69
- }
70
-
71
- return (
72
- line.slice(0, markerIndex) +
73
- line.slice(markerIndex + CURSOR_MARKER.length)
74
- )
75
- })
76
-
77
- return { lines: cleaned, cursor }
78
- }
79
-
80
- export function renderFixedEditorCluster(
81
- input: FixedEditorClusterInput,
82
- ): FixedEditorClusterRender {
83
- const width = Math.max(1, input.width)
84
- const maxRows = Math.max(1, input.terminalRows - 1)
85
-
86
- const statusLines = normalizeLines(input.statusLines, width)
87
- const aboveWidgetLines = normalizeLines(input.aboveWidgetLines, width)
88
- const editorSource = normalizeLines(input.editorLines, width)
89
- const belowWidgetLines = normalizeLines(input.belowWidgetLines, width)
90
- const footerLines = normalizeLines(input.footerLines, width)
91
-
92
- const editorLines = capEditorLines(editorSource, maxRows)
93
- let remaining = maxRows - editorLines.length
94
-
95
- const footer = takeTail(footerLines, remaining)
96
- remaining -= footer.length
97
-
98
- const belowWidgets = takeTail(belowWidgetLines, remaining)
99
- remaining -= belowWidgets.length
100
-
101
- const aboveWidgets = takeTail(aboveWidgetLines, remaining)
102
- remaining -= aboveWidgets.length
103
-
104
- const status = takeTail(statusLines, remaining)
105
-
106
- return extractCursor([
107
- ...status,
108
- ...aboveWidgets,
109
- ...editorLines,
110
- ...belowWidgets,
111
- ...footer,
112
- ])
113
- }
@@ -1,259 +0,0 @@
1
- import {
2
- copyToClipboard,
3
- type ExtensionAPI,
4
- type ExtensionContext,
5
- } from "@earendil-works/pi-coding-agent"
6
- import type { Component } from "@earendil-works/pi-tui"
7
- import { visibleWidth } from "@earendil-works/pi-tui"
8
- import { renderFixedEditorCluster } from "./cluster.js"
9
- import {
10
- emergencyTerminalModeReset,
11
- TerminalSplitCompositor,
12
- type TerminalLike,
13
- } from "./terminal-split.js"
14
-
15
- const WIDGET_KEY = "pi-fixed-editor-probe"
16
- const WARNING_MESSAGE = "pi-fixed-editor: unsupported Pi TUI layout"
17
-
18
- interface ContainerLike extends Component {
19
- children: Component[]
20
- }
21
-
22
- interface TuiLike {
23
- children: Component[]
24
- terminal?: TerminalLike
25
- focusedComponent?: Component | null
26
- requestRender: (force?: boolean) => void
27
- getShowHardwareCursor?: () => boolean
28
- }
29
-
30
- interface ContainerMatch {
31
- container: ContainerLike
32
- index: number
33
- }
34
-
35
- let compositor: TerminalSplitCompositor | null = null
36
- let isInstalled = false
37
- let didWarnUnsupported = false
38
- let fixedStatusContainer: Component | null = null
39
- let fixedWidgetContainerAbove: Component | null = null
40
- let fixedEditorContainer: Component | null = null
41
- let fixedWidgetContainerBelow: Component | null = null
42
- let fixedFooterContainer: Component | null = null
43
-
44
- class ProbeComponent implements Component {
45
- private readonly install: () => void
46
- private hasQueuedInstall = false
47
-
48
- constructor(onInstall: () => void) {
49
- this.install = onInstall
50
- }
51
-
52
- render(): string[] {
53
- if (!this.hasQueuedInstall) {
54
- this.hasQueuedInstall = true
55
- queueMicrotask(this.install)
56
- }
57
- return []
58
- }
59
-
60
- invalidate(): void {
61
- this.hasQueuedInstall = false
62
- }
63
- }
64
-
65
- function isContainerLike(value: unknown): value is ContainerLike {
66
- return (
67
- typeof value === "object" &&
68
- value !== null &&
69
- Array.isArray(Reflect.get(value, "children")) &&
70
- typeof Reflect.get(value, "render") === "function"
71
- )
72
- }
73
-
74
- function isRenderable(value: unknown): value is Component {
75
- return (
76
- typeof value === "object" &&
77
- value !== null &&
78
- typeof Reflect.get(value, "render") === "function" &&
79
- typeof Reflect.get(value, "invalidate") === "function"
80
- )
81
- }
82
-
83
- function getTuiChildren(tui: TuiLike): Component[] {
84
- return Array.isArray(tui.children) ? tui.children : []
85
- }
86
-
87
- function findContainerWithChild(
88
- tui: TuiLike,
89
- child: Component,
90
- ): ContainerMatch | null {
91
- const children = getTuiChildren(tui)
92
- const index = children.findIndex(
93
- (candidate) =>
94
- isContainerLike(candidate) && candidate.children.includes(child),
95
- )
96
- if (index === -1) return null
97
-
98
- const container = children[index]
99
- return isContainerLike(container) ? { container, index } : null
100
- }
101
-
102
- function findEditorContainer(tui: TuiLike): ContainerMatch | null {
103
- const focusedComponent = Reflect.get(tui, "focusedComponent")
104
- if (isRenderable(focusedComponent)) {
105
- const match = findContainerWithChild(tui, focusedComponent)
106
- if (match) return match
107
- }
108
-
109
- const children = getTuiChildren(tui)
110
- const index = children.findIndex((candidate) => {
111
- if (!isContainerLike(candidate)) return false
112
- return candidate.children.some((child) => {
113
- return (
114
- typeof Reflect.get(child, "getText") === "function" &&
115
- typeof Reflect.get(child, "setText") === "function" &&
116
- typeof Reflect.get(child, "handleInput") === "function"
117
- )
118
- })
119
- })
120
-
121
- if (index === -1) return null
122
- const container = children[index]
123
- return isContainerLike(container) ? { container, index } : null
124
- }
125
-
126
- function renderHidden(
127
- activeCompositor: TerminalSplitCompositor,
128
- component: Component | null,
129
- width: number,
130
- ): string[] {
131
- if (!component) return []
132
- return activeCompositor
133
- .renderHidden(component, width)
134
- .filter((line) => visibleWidth(line) > 0)
135
- }
136
-
137
- function resetContainers(): void {
138
- fixedStatusContainer = null
139
- fixedWidgetContainerAbove = null
140
- fixedEditorContainer = null
141
- fixedWidgetContainerBelow = null
142
- fixedFooterContainer = null
143
- }
144
-
145
- function teardown(options?: { resetExtendedKeyboardModes?: boolean }): void {
146
- const hadCompositor = compositor !== null
147
- compositor?.dispose(options)
148
- if (!hadCompositor && options?.resetExtendedKeyboardModes) {
149
- try {
150
- process.stdout.write(emergencyTerminalModeReset())
151
- } catch {
152
- // Exit cleanup cannot surface useful terminal write failures.
153
- }
154
- }
155
- compositor = null
156
- isInstalled = false
157
- resetContainers()
158
- }
159
-
160
- function warnUnsupported(ctx: ExtensionContext): void {
161
- if (didWarnUnsupported || ctx.mode !== "tui") return
162
- didWarnUnsupported = true
163
- ctx.ui.notify(WARNING_MESSAGE, "warning")
164
- }
165
-
166
- function install(ctx: ExtensionContext, tui: TuiLike): void {
167
- if (isInstalled || compositor) return
168
- if (ctx.mode !== "tui") return
169
-
170
- const terminal = tui.terminal
171
- if (!terminal || typeof terminal.write !== "function") {
172
- warnUnsupported(ctx)
173
- return
174
- }
175
-
176
- const editorMatch = findEditorContainer(tui)
177
- if (!editorMatch) {
178
- warnUnsupported(ctx)
179
- return
180
- }
181
-
182
- const children = getTuiChildren(tui)
183
- fixedStatusContainer = children[editorMatch.index - 2] ?? null
184
- fixedWidgetContainerAbove = children[editorMatch.index - 1] ?? null
185
- fixedEditorContainer = editorMatch.container
186
- fixedWidgetContainerBelow = children[editorMatch.index + 1] ?? null
187
- fixedFooterContainer = children[editorMatch.index + 2] ?? null
188
-
189
- let nextCompositor: TerminalSplitCompositor
190
- nextCompositor = new TerminalSplitCompositor({
191
- tui,
192
- terminal,
193
- onCopySelection: (text) => {
194
- void copyToClipboard(text)
195
- },
196
- getShowHardwareCursor: () =>
197
- typeof tui.getShowHardwareCursor === "function" &&
198
- tui.getShowHardwareCursor(),
199
- renderCluster: (width, terminalRows) =>
200
- renderFixedEditorCluster({
201
- width,
202
- terminalRows,
203
- statusLines: renderHidden(nextCompositor, fixedStatusContainer, width),
204
- aboveWidgetLines: renderHidden(
205
- nextCompositor,
206
- fixedWidgetContainerAbove,
207
- width,
208
- ),
209
- editorLines: renderHidden(nextCompositor, fixedEditorContainer, width),
210
- belowWidgetLines: renderHidden(
211
- nextCompositor,
212
- fixedWidgetContainerBelow,
213
- width,
214
- ),
215
- footerLines: renderHidden(nextCompositor, fixedFooterContainer, width),
216
- }),
217
- })
218
-
219
- compositor = nextCompositor
220
- if (fixedStatusContainer) nextCompositor.hideRenderable(fixedStatusContainer)
221
- if (fixedWidgetContainerAbove)
222
- nextCompositor.hideRenderable(fixedWidgetContainerAbove)
223
- if (fixedEditorContainer) nextCompositor.hideRenderable(fixedEditorContainer)
224
- if (fixedWidgetContainerBelow)
225
- nextCompositor.hideRenderable(fixedWidgetContainerBelow)
226
- if (fixedFooterContainer) nextCompositor.hideRenderable(fixedFooterContainer)
227
-
228
- try {
229
- nextCompositor.install()
230
- isInstalled = true
231
- tui.requestRender(true)
232
- } catch {
233
- teardown({ resetExtendedKeyboardModes: true })
234
- warnUnsupported(ctx)
235
- }
236
- }
237
-
238
- export default function fixedEditor(pi: ExtensionAPI) {
239
- pi.on("session_start", (_event, ctx) => {
240
- didWarnUnsupported = false
241
- if (ctx.mode !== "tui") return
242
-
243
- ctx.ui.setWidget(
244
- WIDGET_KEY,
245
- (tui) => {
246
- const tuiLike = tui as unknown as TuiLike
247
- return new ProbeComponent(() => install(ctx, tuiLike))
248
- },
249
- { placement: "aboveEditor" },
250
- )
251
- })
252
-
253
- pi.on("session_shutdown", (_event, ctx) => {
254
- if (ctx.mode === "tui") {
255
- ctx.ui.setWidget(WIDGET_KEY, undefined)
256
- }
257
- teardown({ resetExtendedKeyboardModes: true })
258
- })
259
- }