@timurproko/a1 0.1.8-dev.335 → 0.1.8-dev.368
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 +18 -0
- package/dist/composition/owned-ui.d.ts +2 -0
- package/dist/composition/owned-ui.js +20 -3
- package/dist/contracts/owned-ui/index.d.ts +2 -0
- package/dist/contracts/owned-ui/index.js +2 -0
- package/dist/contracts/owned-ui/model.d.ts +38 -3
- package/dist/contracts/owned-ui/prompt-history.d.ts +18 -0
- package/dist/contracts/owned-ui/prompt-suggestions.d.ts +1 -1
- package/dist/contracts/owned-ui/prompt-suggestions.js +4 -1
- package/dist/contracts/owned-ui/suggestion-diagnostics.d.ts +22 -0
- package/dist/contracts/owned-ui/suggestion-diagnostics.js +9 -0
- package/dist/contracts/owned-ui/transcript-lifecycle.d.ts +5 -0
- package/dist/contracts/owned-ui/transcript-lifecycle.js +33 -0
- package/dist/contracts/owned-ui/validation.js +45 -2
- package/dist/contracts/presentation/index.d.ts +8 -0
- package/dist/features/owned-ui/settings-app.js +1 -1
- package/dist/features/prompt-history/image-sidecar.d.ts +32 -0
- package/dist/features/prompt-history/image-sidecar.js +130 -0
- package/dist/features/prompt-history/index.d.ts +2 -0
- package/dist/features/prompt-history/index.js +1 -0
- package/dist/features/prompt-history/paths.d.ts +1 -0
- package/dist/features/prompt-history/paths.js +2 -1
- package/dist/features/prompt-history/store.d.ts +1 -1
- package/dist/features/prompt-history/store.js +46 -2
- package/dist/features/prompt-history/worker.js +1 -1
- package/dist/features/prompt-suggestions/diagnostics.d.ts +18 -0
- package/dist/features/prompt-suggestions/diagnostics.js +82 -0
- package/dist/features/prompt-suggestions/index.d.ts +1 -0
- package/dist/features/prompt-suggestions/index.js +1 -0
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +9 -1
- package/dist/integrations/pi/components/owned-editor-ux.js +11 -8
- package/dist/integrations/pi/components/prompt-input-port.d.ts +18 -0
- package/dist/integrations/pi/components/prompt-input-port.js +1 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +66 -11
- package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
- package/dist/integrations/pi/components/shell-footer-status.js +9 -5
- package/dist/integrations/pi/components/shell-presenters-info.js +7 -2
- package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +6 -2
- package/dist/integrations/pi/components/shell-presenters-transcript.js +81 -49
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +17 -4
- package/dist/integrations/pi/components/shell-shared-facade.js +2 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +3 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.js +20 -4
- package/dist/integrations/pi/components/tool-result-adapter.d.ts +6 -0
- package/dist/integrations/pi/components/tool-result-adapter.js +27 -0
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -0
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +5 -0
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -2
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +41 -33
- package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
- package/dist/integrations/pi/components/upstream/components/session-footer.js +12 -7
- package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
- package/dist/integrations/pi/engine/adapter.d.ts +2 -1
- package/dist/integrations/pi/engine/adapter.js +181 -84
- package/dist/integrations/pi/engine/tool-rendering.d.ts +13 -0
- package/dist/integrations/pi/engine/tool-rendering.js +122 -0
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +30 -0
- package/dist/integrations/pi/session-ui/prompt-chips.js +128 -1
- package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +8 -0
- package/dist/integrations/pi/session-ui/prompt-history-controller.js +30 -2
- package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +11 -3
- package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +126 -51
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +24 -2
- package/dist/integrations/pi/session-ui/session-shell-root.js +108 -38
- package/dist/integrations/pi/session-ui/session-shell.js +51 -31
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -1
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +106 -16
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
- package/dist/integrations/pi/tui-runtime/adapter.js +35 -11
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -1
- package/dist/integrations/pi/tui-runtime/index.d.ts +1 -0
- package/dist/integrations/pi/tui-runtime/index.js +1 -0
- package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +13 -0
- package/dist/integrations/pi/tui-runtime/mouse-report-input.js +59 -0
- package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +32 -0
- package/dist/integrations/pi/tui-runtime/overlay-geometry.js +159 -0
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +1 -1
- package/dist/product-identity.json +1 -0
- package/dist/ui/components/index.d.ts +1 -0
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/line-input.d.ts +2 -3
- package/dist/ui/components/line-input.js +4 -20
- package/dist/ui/components/mouse.d.ts +1 -1
- package/dist/ui/components/mouse.js +4 -3
- package/dist/ui/components/prompt-input.d.ts +32 -0
- package/dist/ui/components/prompt-input.js +54 -0
- package/docs/architecture/boundaries.md +1 -0
- package/docs/architecture/history-editor-provenance.md +5 -1
- package/docs/architecture/project-structure.md +1 -0
- package/docs/architecture/prompt-suggestions.md +68 -0
- package/docs/architecture/ui-reference-provenance.md +23 -0
- package/docs/features/modal-content-interaction.md +37 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, statSync } from "
|
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
5
5
|
import { assertPromptHistorySubmission, PROMPT_HISTORY_MAX_ENTRY_BYTES, PROMPT_HISTORY_MAX_TEXT_BYTES } from "../../contracts/owned-ui/index.js";
|
|
6
|
+
import { collectImageChipIdentifiers, PromptImageSidecar } from "./image-sidecar.js";
|
|
6
7
|
export class HistoryStorageError extends Error {
|
|
7
8
|
code;
|
|
8
9
|
certainty;
|
|
@@ -17,11 +18,13 @@ export class PromptHistoryStore {
|
|
|
17
18
|
path;
|
|
18
19
|
profileId;
|
|
19
20
|
#database;
|
|
20
|
-
|
|
21
|
+
#sidecar;
|
|
22
|
+
constructor(path, profileId, limit, imagesDir) {
|
|
21
23
|
this.path = path;
|
|
22
24
|
this.profileId = profileId;
|
|
23
25
|
if (!Number.isInteger(limit) || limit < 10 || limit > 100 || limit % 10 !== 0)
|
|
24
26
|
throw new HistoryStorageError("schema");
|
|
27
|
+
this.#sidecar = imagesDir === undefined ? undefined : new PromptImageSidecar(imagesDir);
|
|
25
28
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
26
29
|
if (lstatSync(dirname(path)).isSymbolicLink())
|
|
27
30
|
throw new HistoryStorageError("unavailable");
|
|
@@ -67,6 +70,10 @@ export class PromptHistoryStore {
|
|
|
67
70
|
this.#database.close();
|
|
68
71
|
throw error;
|
|
69
72
|
}
|
|
73
|
+
// Rationale: opportunistically reclaim any sidecar whose row was pruned in a prior process
|
|
74
|
+
// crash or by another concurrent opener. Bounded by the number of files on disk minus the
|
|
75
|
+
// surviving reference set; failures are swallowed to preserve honest history behavior.
|
|
76
|
+
this.#sweepOrphanedSidecars();
|
|
70
77
|
}
|
|
71
78
|
record(input) {
|
|
72
79
|
assertPromptHistorySubmission(input);
|
|
@@ -133,11 +140,48 @@ export class PromptHistoryStore {
|
|
|
133
140
|
#prune(limit) {
|
|
134
141
|
const rows = this.#database.prepare("SELECT sequence,bytes FROM prompts ORDER BY sequence DESC").all();
|
|
135
142
|
let bytes = 0;
|
|
143
|
+
const dropped = [];
|
|
136
144
|
for (const [index, row] of rows.entries()) {
|
|
137
145
|
bytes += Number(row.bytes);
|
|
138
146
|
if (index >= limit || bytes > PROMPT_HISTORY_MAX_TEXT_BYTES)
|
|
139
|
-
|
|
147
|
+
dropped.push(row.sequence);
|
|
140
148
|
}
|
|
149
|
+
if (dropped.length === 0)
|
|
150
|
+
return;
|
|
151
|
+
// Invariant: sidecar reap uses text still referenced by SURVIVING rows so a row still
|
|
152
|
+
// referencing an id shared with a pruned row keeps the file. Collect surviving ids before
|
|
153
|
+
// deleting so a concurrent snapshot cannot see a live row whose sidecar is already gone.
|
|
154
|
+
const droppedTexts = dropped.map(sequence => this.#database.prepare("SELECT text FROM prompts WHERE sequence=?").get(sequence)?.text).filter((value) => typeof value === "string");
|
|
155
|
+
for (const sequence of dropped)
|
|
156
|
+
this.#database.prepare("DELETE FROM prompts WHERE sequence=?").run(sequence);
|
|
157
|
+
if (this.#sidecar === undefined)
|
|
158
|
+
return;
|
|
159
|
+
const survivingIds = this.#collectReferencedIdentifiers();
|
|
160
|
+
for (const text of droppedTexts) {
|
|
161
|
+
for (const id of collectImageChipIdentifiers(text)) {
|
|
162
|
+
if (!survivingIds.has(id))
|
|
163
|
+
this.#sidecar.unlink(id);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
#collectReferencedIdentifiers() {
|
|
168
|
+
const identifiers = new Set();
|
|
169
|
+
const rows = this.#database.prepare("SELECT text FROM prompts").all();
|
|
170
|
+
for (const row of rows) {
|
|
171
|
+
if (typeof row.text !== "string")
|
|
172
|
+
continue;
|
|
173
|
+
for (const id of collectImageChipIdentifiers(row.text))
|
|
174
|
+
identifiers.add(id);
|
|
175
|
+
}
|
|
176
|
+
return identifiers;
|
|
177
|
+
}
|
|
178
|
+
#sweepOrphanedSidecars() {
|
|
179
|
+
if (this.#sidecar === undefined)
|
|
180
|
+
return;
|
|
181
|
+
try {
|
|
182
|
+
this.#sidecar.sweep(this.#collectReferencedIdentifiers());
|
|
183
|
+
}
|
|
184
|
+
catch { /* Rationale: sidecar sweep never blocks history readiness. */ }
|
|
141
185
|
}
|
|
142
186
|
#checkSize(maintain = false) {
|
|
143
187
|
let size = 0;
|
|
@@ -6,7 +6,7 @@ if (port !== null) {
|
|
|
6
6
|
let store;
|
|
7
7
|
let chain = Promise.resolve();
|
|
8
8
|
const options = workerData;
|
|
9
|
-
const open = retry(() => { store = new PromptHistoryStore(options.path, options.profileId, options.limit); });
|
|
9
|
+
const open = retry(() => { store = new PromptHistoryStore(options.path, options.profileId, options.limit, options.imagesDir); });
|
|
10
10
|
void open.then(() => port.postMessage({ id: 0, ok: true }), error => port.postMessage({ id: 0, ok: false, code: classifyHistoryError(error) }));
|
|
11
11
|
port.on("message", (request) => {
|
|
12
12
|
chain = chain.then(async () => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type SuggestionDiagnosticObserver, type SuggestionDiagnosticRecord } from "../../contracts/owned-ui/index.js";
|
|
2
|
+
export interface SuggestionDiagnosticCaptureOptions {
|
|
3
|
+
readonly enabled?: boolean;
|
|
4
|
+
/** Explicit local destination, or an injected sink for deterministic I/O tests. */
|
|
5
|
+
readonly destination?: string;
|
|
6
|
+
readonly writeSnapshot?: (snapshot: string) => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/** Bounded metadata snapshots; one in-flight write and one replaceable latest snapshot. */
|
|
9
|
+
export declare class SuggestionDiagnosticCapture implements SuggestionDiagnosticObserver {
|
|
10
|
+
#private;
|
|
11
|
+
readonly options: SuggestionDiagnosticCaptureOptions;
|
|
12
|
+
constructor(options?: SuggestionDiagnosticCaptureOptions);
|
|
13
|
+
record(record: SuggestionDiagnosticRecord): void;
|
|
14
|
+
snapshot(): readonly SuggestionDiagnosticRecord[];
|
|
15
|
+
/** Explicit evidence boundary, never awaited by input or rendering. */
|
|
16
|
+
flush(): Promise<void>;
|
|
17
|
+
dispose(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { SUGGESTION_DECISION_REASONS, SUGGESTION_DIAGNOSTIC_EVENTS, } from "../../contracts/owned-ui/index.js";
|
|
3
|
+
/** Bounded metadata snapshots; one in-flight write and one replaceable latest snapshot. */
|
|
4
|
+
export class SuggestionDiagnosticCapture {
|
|
5
|
+
options;
|
|
6
|
+
#records = [];
|
|
7
|
+
#pending;
|
|
8
|
+
#writing;
|
|
9
|
+
#disposed = false;
|
|
10
|
+
#write;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.#write = options.writeSnapshot ?? (options.destination === undefined ? undefined
|
|
14
|
+
: snapshot => writeFile(options.destination, snapshot, { encoding: "utf8", mode: 0o600 }));
|
|
15
|
+
}
|
|
16
|
+
record(record) {
|
|
17
|
+
if (!this.options.enabled || this.#disposed)
|
|
18
|
+
return;
|
|
19
|
+
const safe = sanitize(record);
|
|
20
|
+
if (safe === null)
|
|
21
|
+
return;
|
|
22
|
+
this.#records.push(safe);
|
|
23
|
+
if (this.#records.length > 128)
|
|
24
|
+
this.#records.shift();
|
|
25
|
+
let snapshot = this.#serialize();
|
|
26
|
+
while (Buffer.byteLength(snapshot, "utf8") > 65_536) {
|
|
27
|
+
this.#records.shift();
|
|
28
|
+
snapshot = this.#serialize();
|
|
29
|
+
}
|
|
30
|
+
if (this.#write) {
|
|
31
|
+
this.#pending = snapshot;
|
|
32
|
+
this.#pump();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
snapshot() { return this.#records.map(record => ({ ...record })); }
|
|
36
|
+
/** Explicit evidence boundary, never awaited by input or rendering. */
|
|
37
|
+
async flush() {
|
|
38
|
+
while (this.#writing)
|
|
39
|
+
await this.#writing;
|
|
40
|
+
}
|
|
41
|
+
dispose() {
|
|
42
|
+
this.#disposed = true;
|
|
43
|
+
this.#records = [];
|
|
44
|
+
this.#pending = undefined;
|
|
45
|
+
}
|
|
46
|
+
#serialize() { return JSON.stringify({ schema: "prompt-suggestion-diagnostics-v1", records: this.#records }); }
|
|
47
|
+
#pump() {
|
|
48
|
+
if (this.#writing || this.#pending === undefined || !this.#write || this.#disposed)
|
|
49
|
+
return;
|
|
50
|
+
const snapshot = this.#pending;
|
|
51
|
+
this.#pending = undefined;
|
|
52
|
+
this.#writing = Promise.resolve().then(() => this.#write(snapshot)).catch(() => {
|
|
53
|
+
// Security: do not retain or display raw filesystem/provider errors.
|
|
54
|
+
}).finally(() => {
|
|
55
|
+
this.#writing = undefined;
|
|
56
|
+
this.#pump();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function sanitize(record) {
|
|
61
|
+
if (!SUGGESTION_DIAGNOSTIC_EVENTS.includes(record.event))
|
|
62
|
+
return null;
|
|
63
|
+
const reasoning = ["ordinary", "off", "minimal", "low", "medium", "high", "xhigh", "max", "unavailable"].includes(record.reasoning)
|
|
64
|
+
? record.reasoning : "unavailable";
|
|
65
|
+
const reason = record.reason !== undefined && SUGGESTION_DECISION_REASONS.includes(record.reason) ? record.reason : undefined;
|
|
66
|
+
// Security: explicit projection prevents arbitrary extra fields, including raw error payloads, from escaping.
|
|
67
|
+
return {
|
|
68
|
+
event: record.event, ...(reason === undefined ? {} : { reason }),
|
|
69
|
+
session: boundedNumber(record.session), run: boundedNumber(record.run), response: boundedNumber(record.response),
|
|
70
|
+
request: boundedNumber(record.request), provider: identifier(record.provider), model: identifier(record.model),
|
|
71
|
+
reasoning, elapsedMs: boundedNumber(record.elapsedMs),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function boundedNumber(value) {
|
|
75
|
+
return Number.isFinite(value) ? Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.floor(value))) : 0;
|
|
76
|
+
}
|
|
77
|
+
function identifier(value) {
|
|
78
|
+
// Security: identifiers are metadata, not display names or arbitrary JSON. Reject controls/path-shaped values.
|
|
79
|
+
if (typeof value !== "string" || /[\p{C}\\\s]/u.test(value) || value.includes(":") || value.startsWith("/"))
|
|
80
|
+
return "redacted";
|
|
81
|
+
return value.slice(0, 64);
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./diagnostics.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./diagnostics.js";
|
|
@@ -59,6 +59,14 @@ export interface PromptSelectionUxOptions {
|
|
|
59
59
|
readonly requestRender: () => void;
|
|
60
60
|
readonly getRows: () => number;
|
|
61
61
|
/** Presentation-only columns reserved before semantic editor text. */
|
|
62
|
-
readonly
|
|
62
|
+
readonly promptGeometry?: (width: number, padding: number) => {
|
|
63
|
+
readonly prefixWidth: number;
|
|
64
|
+
readonly innerWidth: number;
|
|
65
|
+
readonly paddingX: number;
|
|
66
|
+
readonly contentWidth: number;
|
|
67
|
+
readonly layoutWidth: number;
|
|
68
|
+
};
|
|
63
69
|
}
|
|
64
70
|
export declare function createPromptSelectionInterceptor(editor: Editor, keybindings: KeybindingsManager, options: PromptSelectionUxOptions): OwnedEditorUxInterceptor;
|
|
71
|
+
/** Shares the existing atomic-aware body layout with prompt/menu composition. */
|
|
72
|
+
export declare function editorVisualLineCount(editor: Editor, width: number): number | undefined;
|
|
@@ -230,18 +230,17 @@ class PromptSelectionInterceptor {
|
|
|
230
230
|
}
|
|
231
231
|
#renderVisible(width, next) {
|
|
232
232
|
const rows = next().map(row => row.replaceAll(ATOMIC_SPACE_SENTINEL, " "));
|
|
233
|
-
const
|
|
234
|
-
const padding = Math.min(this.editor.getPaddingX(),
|
|
235
|
-
const prefixWidth =
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const layoutWidth = Math.max(1, contentWidth - (padding ? 0 : 1));
|
|
233
|
+
const shared = this.options.promptGeometry?.(width, this.editor.getPaddingX());
|
|
234
|
+
const padding = shared?.paddingX ?? Math.min(this.editor.getPaddingX(), Math.max(0, Math.floor((width - 1) / 2)));
|
|
235
|
+
const prefixWidth = shared?.prefixWidth ?? 0;
|
|
236
|
+
const contentWidth = shared?.contentWidth ?? Math.max(1, width - padding * 2);
|
|
237
|
+
const layoutWidth = shared?.layoutWidth ?? Math.max(1, contentWidth - (padding ? 0 : 1));
|
|
239
238
|
const visualLines = editorVisualLineMap(this.editor, layoutWidth)
|
|
240
239
|
?? buildVisualLineMap(editorState(this.editor).lines, layoutWidth);
|
|
241
240
|
const scrollOffset = this.editor.interaction?.scrollOffset() ?? numericProperty(this.editor, "scrollOffset");
|
|
242
241
|
const maxVisibleLines = Math.max(5, Math.floor(this.options.getRows() * 0.3));
|
|
243
242
|
const textRows = Math.max(0, Math.min(visualLines.length - scrollOffset, maxVisibleLines, rows.length - 2));
|
|
244
|
-
this.#geometry = { width, padding, layoutWidth, visualLines, scrollOffset, textRows, hiddenRanges: [] };
|
|
243
|
+
this.#geometry = { width, padding, prefixWidth, layoutWidth, visualLines, scrollOffset, textRows, hiddenRanges: [] };
|
|
245
244
|
if (this.#atomicFocus() !== undefined) {
|
|
246
245
|
for (let row = 0; row < rows.length; row += 1) {
|
|
247
246
|
rows[row] = (rows[row] ?? "").replaceAll(CURSOR_MARKER, "");
|
|
@@ -376,7 +375,7 @@ class PromptSelectionInterceptor {
|
|
|
376
375
|
return undefined;
|
|
377
376
|
const line = (geometry.lines ?? editorState(this.editor).lines)[visual.logicalLine] ?? "";
|
|
378
377
|
const segment = line.slice(visual.startCol, visual.startCol + visual.length);
|
|
379
|
-
const displayColumn = Math.max(0, column - 1 - geometry.padding -
|
|
378
|
+
const displayColumn = Math.max(0, column - 1 - geometry.padding - geometry.prefixWidth);
|
|
380
379
|
return {
|
|
381
380
|
line: visual.logicalLine,
|
|
382
381
|
col: restoreColumn(visual.startCol + indexAtDisplayWidth(segment, displayColumn), geometry.hiddenRanges[visual.logicalLine] ?? []),
|
|
@@ -900,6 +899,10 @@ function numericProperty(target, key) {
|
|
|
900
899
|
const value = Object.getOwnPropertyDescriptor(target, key)?.value;
|
|
901
900
|
return typeof value === "number" ? value : 0;
|
|
902
901
|
}
|
|
902
|
+
/** Shares the existing atomic-aware body layout with prompt/menu composition. */
|
|
903
|
+
export function editorVisualLineCount(editor, width) {
|
|
904
|
+
return editorVisualLineMap(editor, width)?.length;
|
|
905
|
+
}
|
|
903
906
|
function editorVisualLineMap(editor, width) {
|
|
904
907
|
if (editor.interaction !== undefined)
|
|
905
908
|
return editor.interaction.visualLines(width);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface PiShellPromptInputGeometry {
|
|
2
|
+
readonly prefixWidth: number;
|
|
3
|
+
readonly innerWidth: number;
|
|
4
|
+
readonly paddingX: number;
|
|
5
|
+
readonly contentWidth: number;
|
|
6
|
+
readonly layoutWidth: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PiShellPromptInputBody {
|
|
9
|
+
readonly rows: readonly string[];
|
|
10
|
+
readonly topRule?: string | undefined;
|
|
11
|
+
readonly bottomRule?: string | undefined;
|
|
12
|
+
readonly after?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export interface PiShellPromptInputPresentation {
|
|
15
|
+
geometry(width: number, padding?: number): PiShellPromptInputGeometry;
|
|
16
|
+
styleRule(text: string): string;
|
|
17
|
+
render(width: number, body: (innerWidth: number) => PiShellPromptInputBody, ruled?: boolean, padding?: number): string[];
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { CombinedAutocompleteProvider, matchesKey, setKeybindings, } from "#pi-tui";
|
|
2
|
+
import { CombinedAutocompleteProvider, matchesKey, setKeybindings, visibleWidth, } from "#pi-tui";
|
|
3
3
|
import { PROMPT_HISTORY_EDITOR_REPLACEMENT } from "../../../contracts/owned-ui/index.js";
|
|
4
4
|
import { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
|
|
5
5
|
import { OwnedEditor } from "./upstream/components/owned-editor.js";
|
|
6
|
-
import { OwnedEditorUxInterception, createPromptSelectionInterceptor, } from "./owned-editor-ux.js";
|
|
6
|
+
import { OwnedEditorUxInterception, createPromptSelectionInterceptor, editorVisualLineCount, } from "./owned-editor-ux.js";
|
|
7
7
|
import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
|
|
8
8
|
import { createTuiFacade, ensureTheme, isAutocompleteProvider, } from "./shell-shared-facade.js";
|
|
9
9
|
export const PINNED_PI_BUILTIN_SLASH_COMMANDS = [
|
|
@@ -33,6 +33,7 @@ export const PINNED_PI_BUILTIN_SLASH_COMMANDS = [
|
|
|
33
33
|
export function createPiShellEditor(options) {
|
|
34
34
|
ensureTheme();
|
|
35
35
|
const tui = createTuiFacade(options);
|
|
36
|
+
const inputPresentation = options.keybindingProfile === "a1" ? options.promptPresentation?.input : undefined;
|
|
36
37
|
const keybindings = options.keybindingProfile === "a1"
|
|
37
38
|
? KeybindingsManager.createForOwnedInput(options.agentDir)
|
|
38
39
|
: KeybindingsManager.create(options.agentDir);
|
|
@@ -40,20 +41,33 @@ export function createPiShellEditor(options) {
|
|
|
40
41
|
if (options.persistentHistory === true && options.keybindingProfile === "a1" && options.historyEditor === undefined) {
|
|
41
42
|
throw new Error("Persistent history requires the loaded owned editor");
|
|
42
43
|
}
|
|
44
|
+
const scrollInfo = { emitted: false, counter: undefined };
|
|
45
|
+
const selectListTheme = getSelectListTheme();
|
|
43
46
|
const EditorClass = options.keybindingProfile === "a1" && options.persistentHistory === true ? options.historyEditor : OwnedEditor;
|
|
44
47
|
const editor = new EditorClass(tui, {
|
|
45
|
-
borderColor: (value) => piTheme().fg("borderMuted", value),
|
|
46
|
-
selectList:
|
|
48
|
+
borderColor: (value) => inputPresentation === undefined ? piTheme().fg("borderMuted", value) : inputPresentation.styleRule(value),
|
|
49
|
+
selectList: options.keybindingProfile !== "a1" ? selectListTheme : {
|
|
50
|
+
...selectListTheme,
|
|
51
|
+
scrollInfo: text => {
|
|
52
|
+
// Protocol: the pinned list emits its final counter row through this callback.
|
|
53
|
+
scrollInfo.emitted = true;
|
|
54
|
+
scrollInfo.counter = /^ \((\d+\/\d+)\)$/u.exec(text)?.[1];
|
|
55
|
+
return selectListTheme.scrollInfo(text);
|
|
56
|
+
},
|
|
57
|
+
},
|
|
47
58
|
}, keybindings, {
|
|
48
59
|
paddingX: PINNED_PI_LAYOUT.editorPaddingX,
|
|
49
60
|
autocompleteMaxVisible: PINNED_PI_LAYOUT.autocompleteMaxVisible,
|
|
50
61
|
persistentHistory: options.persistentHistory === true,
|
|
51
62
|
styleHistoryLabel: text => piTheme().fg("dim", text),
|
|
63
|
+
...(options.keybindingProfile === "a1" ? {
|
|
64
|
+
terminalRows: options.getRows,
|
|
65
|
+
getVisualLineCount: (width) => editorVisualLineCount(editor, width),
|
|
66
|
+
} : {}),
|
|
52
67
|
...(options.keybindingProfile === "a1" && options.promptPresentation !== undefined ? {
|
|
53
|
-
|
|
68
|
+
...(inputPresentation === undefined ? {} : { inputPresentation }),
|
|
54
69
|
styleSuggestion: options.promptPresentation.styleSuggestion,
|
|
55
70
|
styleSuggestionCaret: options.promptPresentation.styleSuggestionCaret,
|
|
56
|
-
terminalRows: options.getRows,
|
|
57
71
|
} : {}),
|
|
58
72
|
});
|
|
59
73
|
const editorUx = options.keybindingProfile === "a1"
|
|
@@ -70,7 +84,7 @@ export function createPiShellEditor(options) {
|
|
|
70
84
|
decorateRow: options.decorateEditorRow ?? (row => row),
|
|
71
85
|
requestRender: options.requestRender,
|
|
72
86
|
getRows: options.getRows,
|
|
73
|
-
...(
|
|
87
|
+
...(inputPresentation === undefined ? {} : { promptGeometry: (width, padding) => inputPresentation.geometry(width, padding) }),
|
|
74
88
|
}),
|
|
75
89
|
], {
|
|
76
90
|
render: width => editor.render(width),
|
|
@@ -80,9 +94,9 @@ export function createPiShellEditor(options) {
|
|
|
80
94
|
let thinkingLevel = "off";
|
|
81
95
|
let isBashMode = false;
|
|
82
96
|
const updateBorderColor = () => {
|
|
83
|
-
editor.borderColor =
|
|
84
|
-
?
|
|
85
|
-
: piTheme().getThinkingBorderColor(thinkingLevel);
|
|
97
|
+
editor.borderColor = inputPresentation !== undefined
|
|
98
|
+
? text => inputPresentation.styleRule(text)
|
|
99
|
+
: isBashMode ? piTheme().getBashModeBorderColor() : piTheme().getThinkingBorderColor(thinkingLevel);
|
|
86
100
|
tui.requestRender();
|
|
87
101
|
};
|
|
88
102
|
let autocompleteProvider;
|
|
@@ -147,13 +161,43 @@ export function createPiShellEditor(options) {
|
|
|
147
161
|
if (options.onPromptSuggestionAccepted !== undefined) {
|
|
148
162
|
editor.onPromptSuggestionAccepted = options.onPromptSuggestionAccepted;
|
|
149
163
|
}
|
|
164
|
+
let bodyGeometry = { rowOffset: 0, rowCount: 0 };
|
|
150
165
|
return {
|
|
151
|
-
|
|
166
|
+
...(editorUx === undefined ? {} : { bodyGeometry: () => bodyGeometry }),
|
|
167
|
+
render: width => {
|
|
168
|
+
scrollInfo.emitted = false;
|
|
169
|
+
scrollInfo.counter = undefined;
|
|
170
|
+
const rows = editorUx?.render(width) ?? editor.render(width);
|
|
171
|
+
if (editorUx === undefined)
|
|
172
|
+
return rows;
|
|
173
|
+
const rowCount = editor.getRenderedBodyRowCount();
|
|
174
|
+
const menu = rows.slice(rowCount, scrollInfo.emitted ? -1 : undefined);
|
|
175
|
+
const label = scrollInfo.counter === undefined ? "" : `${scrollInfo.counter} `;
|
|
176
|
+
// Compatibility: match the history border's four-cell inset and dim label style.
|
|
177
|
+
const counterBorder = label.length > 0 && 4 + visibleWidth(label) <= width
|
|
178
|
+
? editor.borderColor("─── ") + piTheme().fg("dim", label)
|
|
179
|
+
+ editor.borderColor("─".repeat(width - 4 - visibleWidth(label)))
|
|
180
|
+
: editor.borderColor("─".repeat(width));
|
|
181
|
+
// Rationale: when the menu is open, drop the plain top border above the
|
|
182
|
+
// menu and instead show the counter on the border directly above the input
|
|
183
|
+
// prompt (the body's own top border row), mirroring the history header.
|
|
184
|
+
const bodyRows = menu.length === 0
|
|
185
|
+
? rows.slice(0, rowCount)
|
|
186
|
+
: [counterBorder, ...rows.slice(1, rowCount)];
|
|
187
|
+
bodyGeometry = { rowOffset: menu.length, rowCount };
|
|
188
|
+
// Invariant: decorate/select in body coordinates first, then move the whole menu.
|
|
189
|
+
// Completion state, sizing, styles, and pagination still belong to the editor.
|
|
190
|
+
return [...menu, ...bodyRows];
|
|
191
|
+
},
|
|
152
192
|
activateKeybindings: () => setKeybindings(keybindings),
|
|
153
193
|
keybindingConfig: () => keybindings.getEffectiveConfig(),
|
|
154
194
|
reloadKeybindings: () => { keybindings.reload(); setKeybindings(keybindings); },
|
|
155
195
|
matchesTerminalKey: (data, key) => matchesKey(data, key),
|
|
156
196
|
handleInput: data => {
|
|
197
|
+
// Compatibility: an unassigned reverse Tab must not clear selection or reach autocomplete fallback.
|
|
198
|
+
if (editorUx !== undefined && matchesKey(data, "shift+tab")
|
|
199
|
+
&& !Object.values(keybindings.getEffectiveConfig()).some(keys => (Array.isArray(keys) ? keys : [keys]).some(key => key !== undefined && matchesKey(data, key))))
|
|
200
|
+
return;
|
|
157
201
|
if (editorUx === undefined)
|
|
158
202
|
editor.handleInput(data);
|
|
159
203
|
else
|
|
@@ -210,6 +254,17 @@ export function createPiShellEditor(options) {
|
|
|
210
254
|
editor.invalidate();
|
|
211
255
|
},
|
|
212
256
|
canPresentPromptSuggestion: () => editor.canPresentPromptSuggestion(),
|
|
257
|
+
promptSuggestionBlockReason: () => {
|
|
258
|
+
if (!editor.focused)
|
|
259
|
+
return "not-focused";
|
|
260
|
+
if (editor.disableSubmit)
|
|
261
|
+
return "not-ready";
|
|
262
|
+
if (editor.getText().length > 0)
|
|
263
|
+
return "draft";
|
|
264
|
+
if (editor.isShowingAutocomplete())
|
|
265
|
+
return "autocomplete";
|
|
266
|
+
return editor.canPresentPromptSuggestion() ? null : "prompt-mode";
|
|
267
|
+
},
|
|
213
268
|
hasSelection: () => editorUx?.hasSelection() ?? false,
|
|
214
269
|
ownsPointer: () => editorUx?.ownsPointer() ?? false,
|
|
215
270
|
handlePointer: event => editorUx?.handlePointer(event) ?? false,
|
|
@@ -3,5 +3,5 @@ import { type PiShellViewComponentPort, type PiShellStatusPort, type PiShellQueu
|
|
|
3
3
|
export declare function createPiShellHeader(options?: PiShellHeaderOptions): PiShellHeaderPort;
|
|
4
4
|
export declare function createPiShellLoadedResources(resources: readonly PiShellResourceEntry[], initialExpanded?: boolean): PiShellLoadedResourcesPort;
|
|
5
5
|
export declare function createPiShellStatus(view: OwnedUiSessionViewModel, formatProgressStatus: (message: string) => string, runtime?: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">): PiShellStatusPort;
|
|
6
|
-
export declare function createPiShellFooter(view: OwnedUiSessionViewModel, cwd: string): PiShellViewComponentPort;
|
|
6
|
+
export declare function createPiShellFooter(view: OwnedUiSessionViewModel, cwd: string, profile?: "pi" | "a1"): PiShellViewComponentPort;
|
|
7
7
|
export declare function createPiQueuedInputStatus(submissions: readonly string[], presentation?: "pinned" | "custom-viewport"): PiShellQueuedInputPort;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { rawKeyHint, VERSION } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Spacer, Text, } from "#pi-tui";
|
|
3
3
|
import { SessionFooter } from "./upstream/components/session-footer.js";
|
|
4
|
+
import { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
|
|
4
5
|
import { WorkingStatusIndicator } from "./upstream/components/status-indicator.js";
|
|
5
6
|
import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
|
|
6
7
|
import { createTuiFacade, ensureTheme, } from "./shell-shared-facade.js";
|
|
@@ -16,6 +17,8 @@ export function createPiShellHeader(options = {}) {
|
|
|
16
17
|
render(width) {
|
|
17
18
|
if (options.quiet)
|
|
18
19
|
return [];
|
|
20
|
+
if (expanded && options.getKeybindings !== undefined)
|
|
21
|
+
full.setText(expandedHeaderText(options.getKeybindings()));
|
|
19
22
|
return [
|
|
20
23
|
...new Spacer(1).render(width),
|
|
21
24
|
...(expanded ? full : compact).render(width),
|
|
@@ -111,9 +114,9 @@ export function createPiShellStatus(view, formatProgressStatus, runtime) {
|
|
|
111
114
|
},
|
|
112
115
|
};
|
|
113
116
|
}
|
|
114
|
-
export function createPiShellFooter(view, cwd) {
|
|
117
|
+
export function createPiShellFooter(view, cwd, profile = "pi") {
|
|
115
118
|
ensureTheme();
|
|
116
|
-
const footer = new SessionFooter(() => view, cwd);
|
|
119
|
+
const footer = new SessionFooter(() => view, cwd, profile);
|
|
117
120
|
return {
|
|
118
121
|
render: width => footer.render(width),
|
|
119
122
|
invalidate: () => footer.invalidate(),
|
|
@@ -184,7 +187,8 @@ function compactHeaderText() {
|
|
|
184
187
|
const onboarding = theme.fg("dim", "Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.");
|
|
185
188
|
return `${logo}\n${instructions}\n${compactOnboarding}\n\n${onboarding}`;
|
|
186
189
|
}
|
|
187
|
-
function expandedHeaderText() {
|
|
190
|
+
function expandedHeaderText(bindings) {
|
|
191
|
+
const keys = bindings === undefined ? undefined : KeybindingsManager.fromOwnedBindings(bindings);
|
|
188
192
|
const instructions = [
|
|
189
193
|
rawKeyHint("escape", "to interrupt"),
|
|
190
194
|
rawKeyHint("ctrl+c", "to clear"),
|
|
@@ -192,9 +196,9 @@ function expandedHeaderText() {
|
|
|
192
196
|
rawKeyHint("ctrl+d", "to exit (empty)"),
|
|
193
197
|
rawKeyHint(process.platform === "win32" ? "" : "ctrl+z", "to suspend"),
|
|
194
198
|
rawKeyHint("ctrl+k", "to delete to end"),
|
|
195
|
-
rawKeyHint("shift+tab", "to cycle thinking level"),
|
|
199
|
+
rawKeyHint(keys?.getKeys("app.thinking.cycle").join("/") ?? "shift+tab", "to cycle thinking level"),
|
|
196
200
|
rawKeyHint("ctrl+p/shift+ctrl+p", "to cycle models"),
|
|
197
|
-
rawKeyHint("ctrl+l", "to select model"),
|
|
201
|
+
rawKeyHint(keys === undefined ? "ctrl+l" : keys.getKeys("app.model.select").join("/") || "/model", "to select model"),
|
|
198
202
|
rawKeyHint("ctrl+o", "to expand tools"),
|
|
199
203
|
rawKeyHint("ctrl+t", "to expand thinking"),
|
|
200
204
|
rawKeyHint("ctrl+g", "for external editor"),
|
|
@@ -86,12 +86,17 @@ function shortcutDisplay(key) {
|
|
|
86
86
|
}
|
|
87
87
|
export function createPiShellHotkeys(bindings, getShortcuts = () => [], profile = "pi") {
|
|
88
88
|
ensureTheme();
|
|
89
|
-
const keys = new KeybindingsManager(bindings);
|
|
89
|
+
const keys = profile === "a1" ? KeybindingsManager.fromOwnedBindings(bindings) : new KeybindingsManager(bindings);
|
|
90
90
|
// Compatibility: the owned viewport consumes these physical chords before editor actions.
|
|
91
91
|
const display = (action) => keys.getKeys(action)
|
|
92
92
|
.filter(key => profile !== "a1" || (key !== "ctrl+home" && key !== "ctrl+end"))
|
|
93
93
|
.map(shortcutDisplay).join("/");
|
|
94
|
-
const row = (actions, description) => `| ${actions.map(action =>
|
|
94
|
+
const row = (actions, description) => `| ${actions.map(action => {
|
|
95
|
+
const label = display(action);
|
|
96
|
+
return profile === "a1" && label.length === 0
|
|
97
|
+
? action === "app.model.select" ? "Unbound (`/model`)" : "Unbound"
|
|
98
|
+
: `\`${label}\``;
|
|
99
|
+
}).join(" / ")} | ${description} |`;
|
|
95
100
|
let markdown = ["**Navigation**", "| Key | Action |", "|-----|--------|", row(["tui.editor.cursorUp", "tui.editor.cursorDown", "tui.editor.cursorLeft", "tui.editor.cursorRight"], "Move cursor / browse history"), row(["tui.editor.cursorWordLeft", "tui.editor.cursorWordRight"], "Move by word"), row(["tui.editor.cursorLineStart"], profile === "a1" ? "Start of prompt line" : "Start of line"), row(["tui.editor.cursorLineEnd"], profile === "a1" ? "End of prompt line" : "End of line"), ...(profile === "a1" ? ["| `Ctrl+Home` | Start of content |", "| `Ctrl+End` | End of content / follow output |"] : []), row(["tui.editor.jumpForward"], "Jump forward to character"), row(["tui.editor.jumpBackward"], "Jump backward to character"), row(["tui.editor.pageUp", "tui.editor.pageDown"], "Scroll by page"), "", "**Editing**", "| Key | Action |", "|-----|--------|", row(["tui.input.submit"], "Send message"), row(["tui.input.newLine"], `New line${process.platform === "win32" ? " (Ctrl+Enter on Windows Terminal)" : ""}`), row(["tui.editor.deleteWordBackward"], "Delete word backwards"), row(["tui.editor.deleteWordForward"], "Delete word forwards"), row(["tui.editor.deleteToLineStart"], "Delete to start of line"), row(["tui.editor.deleteToLineEnd"], "Delete to end of line"), row(["tui.editor.yank"], "Paste the most-recently-deleted text"), row(["tui.editor.yankPop"], "Cycle through the deleted text after pasting"), row(["tui.editor.undo"], "Undo"), "", "**Other**", "| Key | Action |", "|-----|--------|", row(["tui.input.tab"], "Path completion / accept autocomplete"), row(["app.interrupt"], "Cancel autocomplete / abort streaming"), row(["app.clear"], "Clear editor (first) / exit (second)"), row(["app.exit"], "Exit (when editor is empty)"), row(["app.suspend"], "Suspend to background"), row(["app.thinking.cycle"], "Cycle thinking level"), row(["app.model.cycleForward", "app.model.cycleBackward"], "Cycle models"), row(["app.model.select"], "Open model selector"), row(["app.tools.expand"], "Toggle tool output expansion"), row(["app.thinking.toggle"], "Toggle thinking block visibility"), row(["app.editor.external"], "Edit message in external editor"), row(["app.message.copy"], "Copy last assistant message"), row(["app.message.followUp"], "Queue follow-up message"), row(["app.message.dequeue"], "Restore queued messages"), row(["app.clipboard.pasteImage"], "Paste image or text from clipboard"), "| `/` | Slash commands |", "| `!` | Run bash command |", "| `!!` | Run bash command (excluded from context) |"].join("\n");
|
|
96
101
|
const shortcuts = getShortcuts(bindings ?? keys.getEffectiveConfig());
|
|
97
102
|
if (shortcuts.length > 0) {
|
|
@@ -2,9 +2,13 @@ import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { type MermaidRenderingMode } from "./upstream/components/mermaid.js";
|
|
3
3
|
import type { OwnedUiTranscriptBlock } from "../../../contracts/owned-ui/index.js";
|
|
4
4
|
import { type PiShellSubmittedPromptComposer } from "./submitted-prompt-adapter.js";
|
|
5
|
-
export type
|
|
5
|
+
export { isPiPromptStyleCompaction, type PiShellSubmittedPromptComposer } from "./submitted-prompt-adapter.js";
|
|
6
6
|
import { type PiShellExtensionRendererResolver, type PiShellImageAssetResolver, type PiShellTranscriptComponentPort } from "./shell-shared-facade.js";
|
|
7
|
-
export declare function createPiShellTranscriptComponent(initial: OwnedUiTranscriptBlock, cwd: string, extensions?: PiShellExtensionRendererResolver, submittedPrompt?: PiShellSubmittedPromptComposer, initialOutputPad?: 0 | 1, initialHideThinkingBlock?: boolean, initialMermaidRenderingMode?: MermaidRenderingMode, initialShowImages?: boolean, initialImageWidthCells?: number, imageAssets?: PiShellImageAssetResolver
|
|
7
|
+
export declare function createPiShellTranscriptComponent(initial: OwnedUiTranscriptBlock, cwd: string, extensions?: PiShellExtensionRendererResolver, submittedPrompt?: PiShellSubmittedPromptComposer, initialOutputPad?: 0 | 1, initialHideThinkingBlock?: boolean, initialMermaidRenderingMode?: MermaidRenderingMode, initialShowImages?: boolean, initialImageWidthCells?: number, imageAssets?: PiShellImageAssetResolver, presentation?: {
|
|
8
|
+
readonly getColumns: () => number;
|
|
9
|
+
readonly getRows: () => number;
|
|
10
|
+
readonly changed: () => void;
|
|
11
|
+
}): PiShellTranscriptComponentPort;
|
|
8
12
|
export declare function renderPiShellTranscriptBlock(block: OwnedUiTranscriptBlock, width: number, cwd: string): readonly string[];
|
|
9
13
|
/**
|
|
10
14
|
* Pinned Pi's CLI prints startup diagnostics with `reportDiagnostics` before
|