@timurproko/a1 0.1.8-dev.139 → 0.1.8-dev.157
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 +21 -9
- package/bin/cli.js +1 -0
- package/dist/cli/dispatch.d.ts +6 -0
- package/dist/cli/dispatch.js +107 -81
- package/dist/cli/version-stats.js +7 -1
- package/dist/composition/owned-ui.js +10 -7
- package/dist/composition/settings-route-host.d.ts +1 -1
- package/dist/composition/settings-route-host.js +1 -1
- package/dist/contracts/owned-ui/model.d.ts +8 -1
- package/dist/contracts/owned-ui/validation.js +11 -0
- package/dist/features/owned-ui/settings-app.js +2 -1
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +55 -0
- package/dist/integrations/pi/components/owned-editor-ux.js +778 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +44 -6
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +41 -7
- package/dist/integrations/pi/components/shell-shared-facade.js +3 -1
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +36 -3
- package/dist/integrations/pi/engine/adapter.js +5 -1
- package/dist/integrations/pi/engine/session-integration.d.ts +2 -1
- package/dist/integrations/pi/engine/session-integration.js +14 -3
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +23 -0
- package/dist/integrations/pi/session-ui/prompt-chips.js +242 -0
- package/dist/integrations/pi/session-ui/route-host.d.ts +5 -0
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.d.ts +17 -3
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.js +100 -260
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.d.ts +1 -1
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.js +52 -12
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +39 -0
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +369 -0
- package/dist/integrations/pi/session-ui/system-clipboard.d.ts +11 -0
- package/dist/integrations/pi/session-ui/system-clipboard.js +101 -0
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -6
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +3 -3
- package/dist/native/linux-x64/process-guardian +0 -0
- package/dist/native/win32-x64/manifest.json +3 -3
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/apps/contracts.d.ts +23 -1
- package/dist/ui/apps/host.js +9 -1
- package/dist/ui/components/scrollbar.d.ts +2 -2
- package/dist/ui/components/scrollbar.js +3 -3
- package/dist/ui/components/spans.d.ts +16 -1
- package/dist/ui/components/spans.js +144 -2
- package/dist/ui/components/text.js +24 -2
- package/dist/ui/components/transcript-viewport.d.ts +6 -0
- package/dist/ui/components/transcript-viewport.js +36 -8
- package/dist/ui/settings/declarations.d.ts +1 -1
- package/dist/ui/settings/declarations.js +3 -3
- package/dist/ui/settings/migrations.js +9 -0
- package/docs/architecture/boundaries.md +1 -1
- package/docs/architecture/project-structure.md +1 -1
- package/docs/ci-release-runbook.md +6 -5
- package/docs/features/launch-profiles.md +3 -3
- package/package.json +4 -1
- package/dist/integrations/pi/owned-ui/route-host.d.ts +0 -29
- /package/dist/integrations/pi/{owned-ui → session-ui}/index.d.ts +0 -0
- /package/dist/integrations/pi/{owned-ui → session-ui}/index.js +0 -0
- /package/dist/integrations/pi/{owned-ui → session-ui}/route-host.js +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { CombinedAutocompleteProvider, setKeybindings, } from "#pi-tui";
|
|
2
|
+
import { CombinedAutocompleteProvider, matchesKey, setKeybindings, } from "#pi-tui";
|
|
3
3
|
import { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
|
|
4
4
|
import { OwnedEditor } from "./upstream/components/owned-editor.js";
|
|
5
|
+
import { OwnedEditorUxInterception, createPromptSelectionInterceptor, } from "./owned-editor-ux.js";
|
|
5
6
|
import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
|
|
6
7
|
import { createTuiFacade, ensureTheme, isAutocompleteProvider, } from "./shell-shared-facade.js";
|
|
7
8
|
export const PINNED_PI_BUILTIN_SLASH_COMMANDS = [
|
|
@@ -31,7 +32,9 @@ export const PINNED_PI_BUILTIN_SLASH_COMMANDS = [
|
|
|
31
32
|
export function createPiShellEditor(options) {
|
|
32
33
|
ensureTheme();
|
|
33
34
|
const tui = createTuiFacade(options);
|
|
34
|
-
const keybindings =
|
|
35
|
+
const keybindings = options.keybindingProfile === "a1"
|
|
36
|
+
? KeybindingsManager.createForOwnedInput(options.agentDir)
|
|
37
|
+
: KeybindingsManager.create(options.agentDir);
|
|
35
38
|
setKeybindings(keybindings);
|
|
36
39
|
const editor = new OwnedEditor(tui, {
|
|
37
40
|
borderColor: (value) => piTheme().fg("borderMuted", value),
|
|
@@ -40,6 +43,24 @@ export function createPiShellEditor(options) {
|
|
|
40
43
|
paddingX: PINNED_PI_LAYOUT.editorPaddingX,
|
|
41
44
|
autocompleteMaxVisible: PINNED_PI_LAYOUT.autocompleteMaxVisible,
|
|
42
45
|
});
|
|
46
|
+
const editorUx = options.keybindingProfile === "a1"
|
|
47
|
+
? new OwnedEditorUxInterception([
|
|
48
|
+
createPromptSelectionInterceptor(editor, keybindings, {
|
|
49
|
+
copyText: options.onCopyText ?? (() => { }),
|
|
50
|
+
readClipboardContent: options.readClipboardContent ?? (async () => null),
|
|
51
|
+
transformPastedContent: options.transformPastedContent ?? (content => content.kind === "text" ? content.text : ""),
|
|
52
|
+
atomicRanges: options.editorAtomicRanges ?? (() => []),
|
|
53
|
+
expandCopiedText: options.expandCopiedEditorText ?? (text => text),
|
|
54
|
+
paintSelection: options.paintEditorSelection ?? (line => line),
|
|
55
|
+
decorateRow: options.decorateEditorRow ?? (row => row),
|
|
56
|
+
requestRender: options.requestRender,
|
|
57
|
+
getRows: options.getRows,
|
|
58
|
+
}),
|
|
59
|
+
], {
|
|
60
|
+
render: width => editor.render(width),
|
|
61
|
+
handleInput: data => editor.handleInput(data),
|
|
62
|
+
})
|
|
63
|
+
: undefined;
|
|
43
64
|
let thinkingLevel = "off";
|
|
44
65
|
let isBashMode = false;
|
|
45
66
|
const updateBorderColor = () => {
|
|
@@ -108,15 +129,28 @@ export function createPiShellEditor(options) {
|
|
|
108
129
|
if (options.onDequeue !== undefined)
|
|
109
130
|
editor.onAction("app.message.dequeue", options.onDequeue);
|
|
110
131
|
return {
|
|
111
|
-
render: width => editor.render(width),
|
|
112
|
-
|
|
132
|
+
render: width => editorUx?.render(width) ?? editor.render(width),
|
|
133
|
+
activateKeybindings: () => setKeybindings(keybindings),
|
|
134
|
+
matchesTerminalKey: (data, key) => matchesKey(data, key),
|
|
135
|
+
handleInput: data => {
|
|
136
|
+
if (editorUx === undefined)
|
|
137
|
+
editor.handleInput(data);
|
|
138
|
+
else
|
|
139
|
+
editorUx.handleInput(data);
|
|
140
|
+
},
|
|
113
141
|
invalidate: () => editor.invalidate(),
|
|
114
142
|
setFocused: focused => {
|
|
115
143
|
editor.focused = focused;
|
|
116
144
|
},
|
|
117
145
|
getText: () => editor.getExpandedText(),
|
|
118
|
-
setText: text =>
|
|
119
|
-
|
|
146
|
+
setText: text => {
|
|
147
|
+
editorUx?.reset();
|
|
148
|
+
editor.setText(text);
|
|
149
|
+
},
|
|
150
|
+
insertText: text => {
|
|
151
|
+
editorUx?.reset();
|
|
152
|
+
editor.insertTextAtCursor(text);
|
|
153
|
+
},
|
|
120
154
|
addToHistory: text => editor.addToHistory(text),
|
|
121
155
|
setSubmitEnabled: enabled => {
|
|
122
156
|
editor.disableSubmit = !enabled;
|
|
@@ -139,6 +173,10 @@ export function createPiShellEditor(options) {
|
|
|
139
173
|
thinkingLevel = level;
|
|
140
174
|
updateBorderColor();
|
|
141
175
|
},
|
|
176
|
+
hasSelection: () => editorUx?.hasSelection() ?? false,
|
|
177
|
+
ownsPointer: () => editorUx?.ownsPointer() ?? false,
|
|
178
|
+
handlePointer: event => editorUx?.handlePointer(event) ?? false,
|
|
179
|
+
pasteClipboard: () => editorUx?.pasteClipboard() ?? false,
|
|
142
180
|
};
|
|
143
181
|
}
|
|
144
182
|
function autocompleteCommand(command, addition) {
|
|
@@ -1,13 +1,34 @@
|
|
|
1
|
-
import { type AutocompleteProvider, type Component, type TUI } from "#pi-tui";
|
|
1
|
+
import { visibleWidth, type AutocompleteProvider, type Component, type TUI } from "#pi-tui";
|
|
2
2
|
import type { OwnedUiSessionViewModel, OwnedUiThinkingLevel, OwnedUiTranscriptBlock } from "../../../contracts/owned-ui/index.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import type { PresentationComponentPort } from "../../../contracts/presentation/index.js";
|
|
4
|
+
/** Terminal-cell width authority used by Pi-rendered component rows. */
|
|
5
|
+
export declare const piShellVisibleWidth: typeof visibleWidth;
|
|
6
|
+
export interface PiShellComponentPort extends PresentationComponentPort {
|
|
7
|
+
}
|
|
8
|
+
export type PiShellClipboardContent = {
|
|
9
|
+
readonly kind: "text";
|
|
10
|
+
readonly text: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: "image";
|
|
13
|
+
readonly data: string;
|
|
14
|
+
readonly mimeType: string;
|
|
15
|
+
};
|
|
16
|
+
export interface PiShellEditorTextRange {
|
|
17
|
+
readonly start: number;
|
|
18
|
+
readonly end: number;
|
|
19
|
+
}
|
|
20
|
+
export interface PiShellEditorPointerEvent {
|
|
21
|
+
readonly kind: "press" | "motion" | "release";
|
|
22
|
+
readonly button: number;
|
|
23
|
+
readonly column: number;
|
|
24
|
+
/** One-based row relative to the top of the editor component. */
|
|
25
|
+
readonly row: number;
|
|
9
26
|
}
|
|
10
27
|
export interface PiShellEditorPort extends PiShellComponentPort {
|
|
28
|
+
/** Restores this editor's profile after another Pi component changed the global manager. */
|
|
29
|
+
activateKeybindings(): void;
|
|
30
|
+
/** Uses Pi's terminal decoder rather than assuming one terminal escape spelling. */
|
|
31
|
+
matchesTerminalKey(data: string, key: "home" | "end" | "ctrl+v"): boolean;
|
|
11
32
|
getText(): string;
|
|
12
33
|
setText(text: string): void;
|
|
13
34
|
insertText(text: string): void;
|
|
@@ -18,6 +39,10 @@ export interface PiShellEditorPort extends PiShellComponentPort {
|
|
|
18
39
|
setAutocompleteCommands(commands: readonly PiShellAutocompleteCommand[]): void;
|
|
19
40
|
addAutocompleteProvider(factory: unknown): void;
|
|
20
41
|
setThinkingLevel(level: OwnedUiThinkingLevel): void;
|
|
42
|
+
hasSelection(): boolean;
|
|
43
|
+
ownsPointer(): boolean;
|
|
44
|
+
handlePointer(event: PiShellEditorPointerEvent): boolean;
|
|
45
|
+
pasteClipboard(): boolean;
|
|
21
46
|
}
|
|
22
47
|
export interface PiShellAutocompleteCommand {
|
|
23
48
|
readonly name: string;
|
|
@@ -69,6 +94,8 @@ export interface PiShellHeaderOptions {
|
|
|
69
94
|
readonly resources?: readonly PiShellResourceEntry[];
|
|
70
95
|
}
|
|
71
96
|
export interface PiShellEditorOptions {
|
|
97
|
+
/** Bare A1 adds ergonomic aliases while comparison profiles retain Pi defaults. */
|
|
98
|
+
readonly keybindingProfile?: "pi" | "a1";
|
|
72
99
|
readonly getColumns: () => number;
|
|
73
100
|
readonly getRows: () => number;
|
|
74
101
|
readonly requestRender: () => void;
|
|
@@ -89,6 +116,13 @@ export interface PiShellEditorOptions {
|
|
|
89
116
|
readonly onMessageCopy?: (() => void) | undefined;
|
|
90
117
|
readonly onFollowUp?: (() => void) | undefined;
|
|
91
118
|
readonly onDequeue?: (() => void) | undefined;
|
|
119
|
+
readonly onCopyText?: (text: string) => void;
|
|
120
|
+
readonly readClipboardContent?: () => Promise<PiShellClipboardContent | null>;
|
|
121
|
+
readonly transformPastedContent?: (content: PiShellClipboardContent) => string;
|
|
122
|
+
readonly editorAtomicRanges?: (line: string) => readonly PiShellEditorTextRange[];
|
|
123
|
+
readonly expandCopiedEditorText?: (text: string) => string;
|
|
124
|
+
readonly paintEditorSelection?: (line: string, from: number, to: number, atomic: boolean) => string;
|
|
125
|
+
readonly decorateEditorRow?: (row: string, width: number) => string;
|
|
92
126
|
readonly cwd?: string;
|
|
93
127
|
readonly agentDir?: string;
|
|
94
128
|
readonly autocompleteCommands?: readonly PiShellAutocompleteCommand[];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { setKeybindings, } from "#pi-tui";
|
|
1
|
+
import { setKeybindings, visibleWidth, } from "#pi-tui";
|
|
2
2
|
import { KeybindingsManager, } from "./upstream/adjacent/core/keybindings.js";
|
|
3
3
|
import { ensurePiTheme, } from "./theme.js";
|
|
4
|
+
/** Terminal-cell width authority used by Pi-rendered component rows. */
|
|
5
|
+
export const piShellVisibleWidth = visibleWidth;
|
|
4
6
|
// Pinned from packages/coding-agent/src/core/slash-commands.ts at 914cf14.
|
|
5
7
|
export function createTuiFacade(options) {
|
|
6
8
|
const children = [];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Source-synchronized from Pi 0.84.2 packages/coding-agent/src/core/keybindings.ts. */
|
|
2
|
-
import { type Keybinding, type KeybindingsConfig, type KeyId, KeybindingsManager as TuiKeybindingsManager } from "#pi-tui";
|
|
2
|
+
import { type Keybinding, type KeybindingDefinitions, type KeybindingsConfig, type KeyId, KeybindingsManager as TuiKeybindingsManager } from "#pi-tui";
|
|
3
3
|
export interface AppKeybindings {
|
|
4
4
|
"app.interrupt": true;
|
|
5
5
|
"app.clear": true;
|
|
@@ -43,6 +43,12 @@ export interface AppKeybindings {
|
|
|
43
43
|
"app.tree.filter.all": true;
|
|
44
44
|
"app.tree.filter.cycleForward": true;
|
|
45
45
|
"app.tree.filter.cycleBackward": true;
|
|
46
|
+
"owned.editor.selectAll": true;
|
|
47
|
+
"owned.editor.cut": true;
|
|
48
|
+
"owned.editor.paste": true;
|
|
49
|
+
"owned.editor.redo": true;
|
|
50
|
+
"owned.editor.extendLeft": true;
|
|
51
|
+
"owned.editor.extendRight": true;
|
|
46
52
|
}
|
|
47
53
|
export type AppKeybinding = keyof AppKeybindings;
|
|
48
54
|
declare module "#pi-tui" {
|
|
@@ -413,8 +419,10 @@ export declare function migrateKeybindingsConfig(rawConfig: Record<string, unkno
|
|
|
413
419
|
};
|
|
414
420
|
export declare class KeybindingsManager extends TuiKeybindingsManager {
|
|
415
421
|
private configPath;
|
|
416
|
-
constructor(userBindings?: KeybindingsConfig, configPath?: string);
|
|
422
|
+
constructor(userBindings?: KeybindingsConfig, configPath?: string, definitions?: KeybindingDefinitions);
|
|
417
423
|
static create(agentDir?: string): KeybindingsManager;
|
|
424
|
+
static createForOwnedInput(agentDir?: string): KeybindingsManager;
|
|
425
|
+
private static createWithDefinitions;
|
|
418
426
|
reload(): void;
|
|
419
427
|
getEffectiveConfig(): KeybindingsConfig;
|
|
420
428
|
private static loadFromFile;
|
|
@@ -147,6 +147,33 @@ export const KEYBINDINGS = {
|
|
|
147
147
|
description: "Tree filter: cycle backward",
|
|
148
148
|
},
|
|
149
149
|
};
|
|
150
|
+
/** Pi actions with bare-A1 aliases; the comparison profile keeps KEYBINDINGS unchanged. */
|
|
151
|
+
const OWNED_INPUT_KEYBINDINGS = {
|
|
152
|
+
...KEYBINDINGS,
|
|
153
|
+
"tui.editor.deleteWordBackward": {
|
|
154
|
+
...KEYBINDINGS["tui.editor.deleteWordBackward"],
|
|
155
|
+
defaultKeys: [...KEYBINDINGS["tui.editor.deleteWordBackward"].defaultKeys, "ctrl+backspace"],
|
|
156
|
+
},
|
|
157
|
+
"tui.editor.deleteWordForward": {
|
|
158
|
+
...KEYBINDINGS["tui.editor.deleteWordForward"],
|
|
159
|
+
defaultKeys: [...KEYBINDINGS["tui.editor.deleteWordForward"].defaultKeys, "ctrl+delete"],
|
|
160
|
+
},
|
|
161
|
+
"tui.editor.undo": {
|
|
162
|
+
...KEYBINDINGS["tui.editor.undo"],
|
|
163
|
+
defaultKeys: [KEYBINDINGS["tui.editor.undo"].defaultKeys, "ctrl+z"],
|
|
164
|
+
},
|
|
165
|
+
// Ctrl+Z edits the prompt in bare A1 instead of suspending the process.
|
|
166
|
+
"app.suspend": { ...KEYBINDINGS["app.suspend"], defaultKeys: [] },
|
|
167
|
+
// Owned prompt-selection actions are intercepted before vanilla Pi editor
|
|
168
|
+
// actions. Keeping them in the keybinding manager makes the UX declarative,
|
|
169
|
+
// configurable, and independent of terminal escape-sequence spellings.
|
|
170
|
+
"owned.editor.selectAll": { defaultKeys: "ctrl+a", description: "Select all prompt text" },
|
|
171
|
+
"owned.editor.cut": { defaultKeys: "ctrl+x", description: "Cut selected prompt text" },
|
|
172
|
+
"owned.editor.paste": { defaultKeys: "ctrl+v", description: "Paste clipboard text" },
|
|
173
|
+
"owned.editor.redo": { defaultKeys: "ctrl+y", description: "Redo prompt edit" },
|
|
174
|
+
"owned.editor.extendLeft": { defaultKeys: "shift+left", description: "Extend prompt selection left" },
|
|
175
|
+
"owned.editor.extendRight": { defaultKeys: "shift+right", description: "Extend prompt selection right" },
|
|
176
|
+
};
|
|
150
177
|
const KEYBINDING_NAME_MIGRATIONS = {
|
|
151
178
|
cursorUp: "tui.editor.cursorUp",
|
|
152
179
|
cursorDown: "tui.editor.cursorDown",
|
|
@@ -270,14 +297,20 @@ function loadRawConfig(path) {
|
|
|
270
297
|
}
|
|
271
298
|
export class KeybindingsManager extends TuiKeybindingsManager {
|
|
272
299
|
configPath;
|
|
273
|
-
constructor(userBindings = {}, configPath) {
|
|
274
|
-
super(
|
|
300
|
+
constructor(userBindings = {}, configPath, definitions = KEYBINDINGS) {
|
|
301
|
+
super(definitions, userBindings);
|
|
275
302
|
this.configPath = configPath;
|
|
276
303
|
}
|
|
277
304
|
static create(agentDir = getAgentDir()) {
|
|
305
|
+
return KeybindingsManager.createWithDefinitions(agentDir, KEYBINDINGS);
|
|
306
|
+
}
|
|
307
|
+
static createForOwnedInput(agentDir = getAgentDir()) {
|
|
308
|
+
return KeybindingsManager.createWithDefinitions(agentDir, OWNED_INPUT_KEYBINDINGS);
|
|
309
|
+
}
|
|
310
|
+
static createWithDefinitions(agentDir, definitions) {
|
|
278
311
|
const configPath = join(agentDir, "keybindings.json");
|
|
279
312
|
const userBindings = KeybindingsManager.loadFromFile(configPath);
|
|
280
|
-
return new KeybindingsManager(userBindings, configPath);
|
|
313
|
+
return new KeybindingsManager(userBindings, configPath, definitions);
|
|
281
314
|
}
|
|
282
315
|
reload() {
|
|
283
316
|
if (!this.configPath)
|
|
@@ -1320,7 +1320,11 @@ export class PiEngineAdapter {
|
|
|
1320
1320
|
case "retry":
|
|
1321
1321
|
case "compact": {
|
|
1322
1322
|
const result = await this.#sessionCommands?.execute(command.type === "prompt" || command.type === "steer" || command.type === "follow-up"
|
|
1323
|
-
? {
|
|
1323
|
+
? {
|
|
1324
|
+
type: command.type,
|
|
1325
|
+
text: command.text,
|
|
1326
|
+
...(command.images === undefined ? {} : { images: [...command.images] }),
|
|
1327
|
+
}
|
|
1324
1328
|
: { type: command.type });
|
|
1325
1329
|
if (!result || result.outcome === "rejected" || result.outcome === "failed") {
|
|
1326
1330
|
throw new Error(command.type === "retry" ? "no previous prompt is available to retry" : `Pi session command failed: ${command.type}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { AgentSession, AgentSessionEvent, PromptOptions } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type AgentCommandOutcome, type AgentEvent } from "../../../contracts/agent-engine/index.js";
|
|
3
3
|
export interface PiDocumentedSessionCommands {
|
|
4
4
|
readonly isStreaming: AgentSession["isStreaming"];
|
|
@@ -18,6 +18,7 @@ export interface PiDocumentedSessionCommands {
|
|
|
18
18
|
export type PiSessionCommand = {
|
|
19
19
|
readonly type: "prompt" | "steer" | "follow-up";
|
|
20
20
|
readonly text: string;
|
|
21
|
+
readonly images?: NonNullable<PromptOptions["images"]>;
|
|
21
22
|
} | {
|
|
22
23
|
readonly type: "abort" | "retry" | "compact";
|
|
23
24
|
} | {
|
|
@@ -9,18 +9,29 @@ export class PiSessionCommandIntegration {
|
|
|
9
9
|
switch (command.type) {
|
|
10
10
|
case "prompt":
|
|
11
11
|
this.#lastPrompt = command.text;
|
|
12
|
-
await this.session.prompt(command.text,
|
|
12
|
+
await this.session.prompt(command.text, command.images === undefined
|
|
13
|
+
? this.session.isStreaming ? { streamingBehavior: "followUp" } : undefined
|
|
14
|
+
: {
|
|
15
|
+
...(this.session.isStreaming ? { streamingBehavior: "followUp" } : {}),
|
|
16
|
+
images: [...command.images],
|
|
17
|
+
});
|
|
13
18
|
return { outcome: "completed" };
|
|
14
19
|
case "steer":
|
|
15
20
|
this.#lastPrompt = command.text;
|
|
16
21
|
// Match interactive Pi: prompt() owns template/extension expansion and
|
|
17
22
|
// turns the accepted steering message into the visible user row while
|
|
18
23
|
// later messages remain in the pending queue.
|
|
19
|
-
await this.session.prompt(command.text, {
|
|
24
|
+
await this.session.prompt(command.text, {
|
|
25
|
+
streamingBehavior: "steer",
|
|
26
|
+
...(command.images === undefined ? {} : { images: [...command.images] }),
|
|
27
|
+
});
|
|
20
28
|
return { outcome: "completed" };
|
|
21
29
|
case "follow-up":
|
|
22
30
|
this.#lastPrompt = command.text;
|
|
23
|
-
await this.session.prompt(command.text, {
|
|
31
|
+
await this.session.prompt(command.text, {
|
|
32
|
+
streamingBehavior: "followUp",
|
|
33
|
+
...(command.images === undefined ? {} : { images: [...command.images] }),
|
|
34
|
+
});
|
|
24
35
|
return { outcome: "completed" };
|
|
25
36
|
case "abort":
|
|
26
37
|
if (this.session.isRetrying)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PiShellClipboardContent, PiShellEditorTextRange } from "../components/index.js";
|
|
2
|
+
export interface PromptImageAttachment {
|
|
3
|
+
readonly type: "image";
|
|
4
|
+
readonly data: string;
|
|
5
|
+
readonly mimeType: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PreparedPrompt {
|
|
8
|
+
readonly text: string;
|
|
9
|
+
readonly images: readonly PromptImageAttachment[];
|
|
10
|
+
}
|
|
11
|
+
/** Owns semantic clipboard records while the prompt displays compact chips. */
|
|
12
|
+
export declare class PromptChipStore {
|
|
13
|
+
#private;
|
|
14
|
+
transformPastedContent(content: PiShellClipboardContent): string;
|
|
15
|
+
atomicRanges(line: string): readonly PiShellEditorTextRange[];
|
|
16
|
+
hyperlinkRanges(text: string): readonly {
|
|
17
|
+
start: number;
|
|
18
|
+
end: number;
|
|
19
|
+
target: string;
|
|
20
|
+
}[];
|
|
21
|
+
expandCopiedText(text: string): string;
|
|
22
|
+
prepareSubmission(text: string): PreparedPrompt;
|
|
23
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const CHIP_PATTERN = /\[(?:📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
|
|
6
|
+
const IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|bmp|tiff?)$/iu;
|
|
7
|
+
const URL_PATTERN = /^https?:\/\/[^\s\u0000-\u001f\u007f]+$/iu;
|
|
8
|
+
const URL_DISPLAY_LENGTH = 40;
|
|
9
|
+
/** Owns semantic clipboard records while the prompt displays compact chips. */
|
|
10
|
+
export class PromptChipStore {
|
|
11
|
+
#chips = new Map();
|
|
12
|
+
transformPastedContent(content) {
|
|
13
|
+
if (content.kind === "image") {
|
|
14
|
+
const id = randomBytes(5).toString("hex");
|
|
15
|
+
const tag = `[📷 screenshot-${id}.png]`;
|
|
16
|
+
this.#chips.set(tag, {
|
|
17
|
+
kind: "image",
|
|
18
|
+
tag,
|
|
19
|
+
image: { type: "image", data: content.data, mimeType: content.mimeType },
|
|
20
|
+
});
|
|
21
|
+
return tag;
|
|
22
|
+
}
|
|
23
|
+
const text = content.text;
|
|
24
|
+
if (this.#chips.has(text.trim()))
|
|
25
|
+
return text.trim();
|
|
26
|
+
const url = text.trim();
|
|
27
|
+
if (URL_PATTERN.test(url)) {
|
|
28
|
+
const label = url.length <= URL_DISPLAY_LENGTH ? url : `${url.slice(0, URL_DISPLAY_LENGTH)}…`;
|
|
29
|
+
return this.#recordUnique({ kind: "url", tag: `[🔗 ${label}]`, label, url });
|
|
30
|
+
}
|
|
31
|
+
const paths = pathsFromClipboard(text);
|
|
32
|
+
if (paths.length === 0)
|
|
33
|
+
return text;
|
|
34
|
+
return paths.map(item => {
|
|
35
|
+
const label = pathLabel(item.fullPath);
|
|
36
|
+
if (item.kind === "folder") {
|
|
37
|
+
return this.#recordUnique({ kind: "folder", tag: `[📁 ${label}]`, path: item.fullPath });
|
|
38
|
+
}
|
|
39
|
+
const icon = IMAGE_EXTENSION.test(item.fullPath) ? "🖼 " : "📄";
|
|
40
|
+
return this.#recordUnique({ kind: "file", tag: `[${icon} ${label}]`, path: item.fullPath });
|
|
41
|
+
}).join("");
|
|
42
|
+
}
|
|
43
|
+
atomicRanges(line) {
|
|
44
|
+
return [...line.matchAll(CHIP_PATTERN)].map(match => ({
|
|
45
|
+
start: match.index,
|
|
46
|
+
end: match.index + match[0].length,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
hyperlinkRanges(text) {
|
|
50
|
+
const ranges = [];
|
|
51
|
+
for (const chip of this.#chips.values()) {
|
|
52
|
+
if (chip.kind !== "url")
|
|
53
|
+
continue;
|
|
54
|
+
const labelOffset = chip.tag.indexOf(chip.label);
|
|
55
|
+
if (labelOffset < 0)
|
|
56
|
+
continue;
|
|
57
|
+
let searchFrom = 0;
|
|
58
|
+
while (searchFrom <= text.length - chip.tag.length) {
|
|
59
|
+
const tagStart = text.indexOf(chip.tag, searchFrom);
|
|
60
|
+
if (tagStart < 0)
|
|
61
|
+
break;
|
|
62
|
+
const start = tagStart + labelOffset;
|
|
63
|
+
ranges.push({ start, end: start + chip.label.length, target: chip.url });
|
|
64
|
+
searchFrom = tagStart + chip.tag.length;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return ranges.sort((left, right) => right.start - left.start);
|
|
68
|
+
}
|
|
69
|
+
expandCopiedText(text) {
|
|
70
|
+
return this.#replaceResolvable(text, false).text;
|
|
71
|
+
}
|
|
72
|
+
prepareSubmission(text) {
|
|
73
|
+
const expanded = this.#replaceResolvable(text, true);
|
|
74
|
+
return { text: expanded.text, images: expanded.images };
|
|
75
|
+
}
|
|
76
|
+
#replaceResolvable(text, includeImages) {
|
|
77
|
+
let expanded = text;
|
|
78
|
+
const images = [];
|
|
79
|
+
const seenImages = new Set();
|
|
80
|
+
for (const [tag, chip] of this.#chips) {
|
|
81
|
+
if (!expanded.includes(tag))
|
|
82
|
+
continue;
|
|
83
|
+
if (chip.kind === "image") {
|
|
84
|
+
if (includeImages && !seenImages.has(tag)) {
|
|
85
|
+
images.push(chip.image);
|
|
86
|
+
seenImages.add(tag);
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const value = chip.kind === "url" ? chip.url : chip.path;
|
|
91
|
+
expanded = expanded.replaceAll(tag, value);
|
|
92
|
+
}
|
|
93
|
+
return { text: expanded, images };
|
|
94
|
+
}
|
|
95
|
+
#recordUnique(chip) {
|
|
96
|
+
const existing = this.#chips.get(chip.tag);
|
|
97
|
+
if (existing === undefined || sameChipValue(existing, chip)) {
|
|
98
|
+
this.#chips.set(chip.tag, chip);
|
|
99
|
+
return chip.tag;
|
|
100
|
+
}
|
|
101
|
+
const suffix = randomBytes(2).toString("hex");
|
|
102
|
+
const uniqueTag = `${chip.tag.slice(0, -1)} #${suffix}]`;
|
|
103
|
+
const unique = { ...chip, tag: uniqueTag };
|
|
104
|
+
this.#chips.set(uniqueTag, unique);
|
|
105
|
+
return uniqueTag;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function pathsFromClipboard(text) {
|
|
109
|
+
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
110
|
+
if (normalized.length === 0)
|
|
111
|
+
return [];
|
|
112
|
+
const paths = [];
|
|
113
|
+
for (const line of normalized.split("\n").filter(value => value.trim().length > 0)) {
|
|
114
|
+
const wholeLine = existingClipboardPath(line);
|
|
115
|
+
if (wholeLine !== null) {
|
|
116
|
+
paths.push(wholeLine);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const tokens = tokenizePathLine(line)?.filter(token => token.quoted || token.text !== "&");
|
|
120
|
+
if (tokens === undefined || tokens.length === 0)
|
|
121
|
+
return [];
|
|
122
|
+
for (let index = 0; index < tokens.length;) {
|
|
123
|
+
const token = tokens[index];
|
|
124
|
+
if (token === undefined)
|
|
125
|
+
return [];
|
|
126
|
+
if (token.quoted) {
|
|
127
|
+
const quoted = existingClipboardPath(token.text);
|
|
128
|
+
if (quoted === null)
|
|
129
|
+
return [];
|
|
130
|
+
paths.push(quoted);
|
|
131
|
+
index += 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (normalizePath(token.text) === null)
|
|
135
|
+
return [];
|
|
136
|
+
let runEnd = index;
|
|
137
|
+
while (runEnd < tokens.length && tokens[runEnd]?.quoted !== true)
|
|
138
|
+
runEnd += 1;
|
|
139
|
+
let matched = null;
|
|
140
|
+
let matchedEnd = index;
|
|
141
|
+
for (let end = runEnd; end > index; end -= 1) {
|
|
142
|
+
matched = existingClipboardPath(tokens.slice(index, end).map(candidate => candidate.text).join(" "));
|
|
143
|
+
if (matched !== null) {
|
|
144
|
+
matchedEnd = end;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (matched === null)
|
|
149
|
+
return [];
|
|
150
|
+
paths.push(matched);
|
|
151
|
+
index = matchedEnd;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return paths;
|
|
155
|
+
}
|
|
156
|
+
function existingClipboardPath(value) {
|
|
157
|
+
const fullPath = normalizePath(unquote(value.trim()));
|
|
158
|
+
if (fullPath === null || !existsSync(fullPath))
|
|
159
|
+
return null;
|
|
160
|
+
try {
|
|
161
|
+
const stat = statSync(fullPath);
|
|
162
|
+
if (stat.isDirectory())
|
|
163
|
+
return { fullPath, kind: "folder" };
|
|
164
|
+
if (stat.isFile())
|
|
165
|
+
return { fullPath, kind: "file" };
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Clipboard path probing is best-effort.
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
function tokenizePathLine(line) {
|
|
173
|
+
const tokens = [];
|
|
174
|
+
let index = 0;
|
|
175
|
+
while (index < line.length) {
|
|
176
|
+
while (index < line.length && /\s/u.test(line[index] ?? ""))
|
|
177
|
+
index += 1;
|
|
178
|
+
if (index >= line.length)
|
|
179
|
+
break;
|
|
180
|
+
const quote = line[index] === '"' || line[index] === "'" ? line[index] : undefined;
|
|
181
|
+
if (quote !== undefined) {
|
|
182
|
+
index += 1;
|
|
183
|
+
const start = index;
|
|
184
|
+
while (index < line.length && line[index] !== quote)
|
|
185
|
+
index += 1;
|
|
186
|
+
if (index >= line.length)
|
|
187
|
+
return undefined;
|
|
188
|
+
tokens.push({ text: line.slice(start, index), quoted: true });
|
|
189
|
+
index += 1;
|
|
190
|
+
if (index < line.length && !/\s/u.test(line[index] ?? ""))
|
|
191
|
+
return undefined;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const start = index;
|
|
195
|
+
while (index < line.length && !/\s/u.test(line[index] ?? ""))
|
|
196
|
+
index += 1;
|
|
197
|
+
tokens.push({ text: line.slice(start, index), quoted: false });
|
|
198
|
+
}
|
|
199
|
+
return tokens;
|
|
200
|
+
}
|
|
201
|
+
function normalizePath(value) {
|
|
202
|
+
let candidate = value;
|
|
203
|
+
if (/^file:\/\//iu.test(candidate)) {
|
|
204
|
+
try {
|
|
205
|
+
candidate = fileURLToPath(candidate);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (process.platform === "win32") {
|
|
212
|
+
const msys = /^\/([A-Za-z])\/(.*)$/u.exec(candidate);
|
|
213
|
+
if (msys !== null)
|
|
214
|
+
candidate = `${msys[1]}:\\${msys[2]?.replaceAll("/", "\\") ?? ""}`;
|
|
215
|
+
}
|
|
216
|
+
return /^(?:[A-Za-z]:\\|\/)/u.test(candidate) ? path.normalize(candidate) : null;
|
|
217
|
+
}
|
|
218
|
+
function unquote(value) {
|
|
219
|
+
let result = value.startsWith("& ") ? value.slice(2).trim() : value;
|
|
220
|
+
if ((result.startsWith('"') && result.endsWith('"')) || (result.startsWith("'") && result.endsWith("'"))) {
|
|
221
|
+
result = result.slice(1, -1).trim();
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
function pathLabel(fullPath) {
|
|
226
|
+
const basename = path.basename(fullPath);
|
|
227
|
+
if (basename.length > 0)
|
|
228
|
+
return basename;
|
|
229
|
+
return path.parse(fullPath).root.replace(/[\\/]+$/u, "") || fullPath;
|
|
230
|
+
}
|
|
231
|
+
function sameChipValue(left, right) {
|
|
232
|
+
if (left.kind !== right.kind)
|
|
233
|
+
return false;
|
|
234
|
+
if (left.kind === "url" && right.kind === "url")
|
|
235
|
+
return left.url === right.url;
|
|
236
|
+
if ((left.kind === "file" || left.kind === "folder") && (right.kind === "file" || right.kind === "folder")) {
|
|
237
|
+
return path.normalize(left.path) === path.normalize(right.path);
|
|
238
|
+
}
|
|
239
|
+
if (left.kind === "image" && right.kind === "image")
|
|
240
|
+
return left.image.data === right.image.data;
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { OwnedUiSessionViewModel, OwnedUiViewportSettings, OwnedUiViewportSettingsPort } from "../../../contracts/owned-ui/index.js";
|
|
2
|
-
import type { UiRouteHost } from "
|
|
2
|
+
import type { UiRouteHost } from "../../../ui/apps/index.js";
|
|
3
3
|
import { type PiEngineAdapter, type PiWorkflowResult } from "../engine/index.js";
|
|
4
|
-
import { type PiShellComponentPort, type PiShellEditorPort, type PiShellExtensionRendererResolver, type PiShellHeaderOptions, type PiShellHeaderPort, type PiShellLoadedResourcesPort, type PiShellResourceEntry, type PiShellTranscriptComponentPort } from "../components/index.js";
|
|
4
|
+
import { type PiShellClipboardContent, type PiShellComponentPort, type PiShellEditorPort, type PiShellExtensionRendererResolver, type PiShellHeaderOptions, type PiShellHeaderPort, type PiShellLoadedResourcesPort, type PiShellResourceEntry, type PiShellTranscriptComponentPort } from "../components/index.js";
|
|
5
5
|
import { type PiTuiComponentPort, type PiTuiLayoutNode, type PiTuiTerminalPort } from "../tui-runtime/index.js";
|
|
6
|
+
import { type PreparedPrompt } from "./prompt-chips.js";
|
|
6
7
|
export type OwnedUiBackendPort = PiEngineAdapter;
|
|
7
8
|
export type OwnedUiTerminalPort = PiTuiTerminalPort;
|
|
8
9
|
type OwnedUiStartupOptions = PiShellHeaderOptions;
|
|
10
|
+
export interface OwnedUiClipboardPort {
|
|
11
|
+
readText(): Promise<string | null>;
|
|
12
|
+
readImage?(): Promise<{
|
|
13
|
+
readonly data: string;
|
|
14
|
+
readonly mimeType: string;
|
|
15
|
+
} | null>;
|
|
16
|
+
writeText?(text: string): Promise<void>;
|
|
17
|
+
}
|
|
9
18
|
export interface OwnedUiSessionShellOptions {
|
|
10
19
|
readonly backend: OwnedUiBackendPort;
|
|
11
20
|
readonly cwd: string;
|
|
@@ -20,6 +29,8 @@ export interface OwnedUiSessionShellOptions {
|
|
|
20
29
|
readonly sessionLayout?: "pinned" | "custom-viewport";
|
|
21
30
|
/** Live profile-local settings, supplied only to the bare-A1 composition. */
|
|
22
31
|
readonly viewportSettings?: OwnedUiViewportSettingsPort;
|
|
32
|
+
/** Optional platform seam; production uses A1's system clipboard adapter. */
|
|
33
|
+
readonly clipboard?: OwnedUiClipboardPort;
|
|
23
34
|
}
|
|
24
35
|
export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
|
|
25
36
|
#private;
|
|
@@ -29,7 +40,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
|
|
|
29
40
|
constructor(view: OwnedUiSessionViewModel, cwd: string, handlers: {
|
|
30
41
|
readonly getColumns: () => number;
|
|
31
42
|
readonly getRows: () => number;
|
|
32
|
-
readonly requestRender: () => void;
|
|
43
|
+
readonly requestRender: (force?: boolean) => void;
|
|
33
44
|
readonly onSubmit: (text: string) => void;
|
|
34
45
|
readonly onInterrupt: () => void;
|
|
35
46
|
readonly onClear?: () => void;
|
|
@@ -41,7 +52,10 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
|
|
|
41
52
|
readonly onMessageCopy?: () => void;
|
|
42
53
|
readonly onFollowUp?: () => void;
|
|
43
54
|
readonly onDequeue?: () => void;
|
|
55
|
+
readonly onCopyText?: (text: string) => void;
|
|
56
|
+
readonly readClipboardContent?: () => Promise<PiShellClipboardContent | null>;
|
|
44
57
|
}, startup?: PiShellHeaderOptions, agentDir?: string, extensionRenderers?: PiShellExtensionRendererResolver, sessionLayout?: "pinned" | "custom-viewport");
|
|
58
|
+
preparePromptSubmission(text: string): PreparedPrompt;
|
|
45
59
|
update(view: OwnedUiSessionViewModel): void;
|
|
46
60
|
/**
|
|
47
61
|
* Applies one block: its component is created or updated in place and the order grows
|