@timurproko/a1 0.1.8-dev.137 → 0.1.8-dev.151

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.
Files changed (105) hide show
  1. package/README.md +2 -4
  2. package/dist/cli/capabilities.d.ts +2 -11
  3. package/dist/cli/capabilities.js +1 -1
  4. package/dist/cli/dispatch.js +3 -6
  5. package/dist/composition/owned-ui.js +26 -5
  6. package/dist/composition/settings-route-host.d.ts +1 -1
  7. package/dist/composition/settings-route-host.js +1 -1
  8. package/dist/contracts/owned-ui/model.d.ts +28 -0
  9. package/dist/contracts/owned-ui/validation.js +14 -0
  10. package/dist/features/launch/development-launch.js +11 -3
  11. package/dist/features/launch/index.d.ts +0 -1
  12. package/dist/features/launch/index.js +0 -1
  13. package/dist/features/launch/intent.d.ts +3 -2
  14. package/dist/features/launch/intent.js +1 -2
  15. package/dist/features/launch/prepare-launch.d.ts +0 -2
  16. package/dist/features/launch/prepare-launch.js +2 -9
  17. package/dist/features/launch/profile-paths.d.ts +1 -2
  18. package/dist/features/launch/profile-paths.js +2 -8
  19. package/dist/features/launch/runtime-selection.d.ts +2 -5
  20. package/dist/features/owned-ui/settings-app.js +38 -14
  21. package/dist/foundation/lifecycle/model.d.ts +1 -1
  22. package/dist/foundation/lifecycle/model.js +1 -1
  23. package/dist/foundation/release/bootstrap.d.ts +1 -3
  24. package/dist/foundation/release/bootstrap.js +1 -1
  25. package/dist/foundation/storage/control-store.js +37 -3
  26. package/dist/integrations/pi/components/owned-editor-ux.d.ts +55 -0
  27. package/dist/integrations/pi/components/owned-editor-ux.js +778 -0
  28. package/dist/integrations/pi/components/shell-editor-autocomplete.js +44 -6
  29. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  30. package/dist/integrations/pi/components/shell-footer-status.js +14 -5
  31. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +3 -2
  32. package/dist/integrations/pi/components/shell-presenters-transcript.js +9 -8
  33. package/dist/integrations/pi/components/shell-shared-facade.d.ts +41 -7
  34. package/dist/integrations/pi/components/shell-shared-facade.js +3 -1
  35. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +13 -0
  36. package/dist/integrations/pi/components/submitted-prompt-adapter.js +27 -0
  37. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
  38. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +36 -3
  39. package/dist/integrations/pi/engine/adapter.js +21 -3
  40. package/dist/integrations/pi/engine/session-integration.d.ts +2 -1
  41. package/dist/integrations/pi/engine/session-integration.js +17 -3
  42. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +23 -0
  43. package/dist/integrations/pi/session-ui/prompt-chips.js +242 -0
  44. package/dist/integrations/pi/session-ui/route-host.d.ts +5 -0
  45. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.d.ts +34 -5
  46. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.js +294 -45
  47. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.d.ts +1 -1
  48. package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.js +167 -24
  49. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +39 -0
  50. package/dist/integrations/pi/session-ui/session-viewport-controller.js +369 -0
  51. package/dist/integrations/pi/session-ui/system-clipboard.d.ts +11 -0
  52. package/dist/integrations/pi/session-ui/system-clipboard.js +101 -0
  53. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -1
  54. package/dist/integrations/pi/tui-runtime/adapter.js +51 -2
  55. package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -6
  56. package/dist/native/darwin-arm64/manifest.json +1 -1
  57. package/dist/native/linux-x64/manifest.json +1 -1
  58. package/dist/native/win32-x64/manifest.json +3 -3
  59. package/dist/native/win32-x64/process-guardian.exe +0 -0
  60. package/dist/product-identity.d.ts +2 -2
  61. package/dist/product-identity.js +3 -4
  62. package/dist/product-identity.json +3 -5
  63. package/dist/ui/apps/contracts.d.ts +23 -1
  64. package/dist/ui/apps/host.js +9 -1
  65. package/dist/ui/components/index.d.ts +3 -0
  66. package/dist/ui/components/index.js +3 -0
  67. package/dist/ui/components/mouse.d.ts +10 -0
  68. package/dist/ui/components/mouse.js +23 -0
  69. package/dist/ui/components/prompt-row.d.ts +14 -0
  70. package/dist/ui/components/prompt-row.js +20 -0
  71. package/dist/ui/components/scrollbar.d.ts +26 -0
  72. package/dist/ui/components/scrollbar.js +35 -0
  73. package/dist/ui/components/spans.d.ts +21 -0
  74. package/dist/ui/components/spans.js +178 -0
  75. package/dist/ui/components/submitted-prompt.d.ts +23 -0
  76. package/dist/ui/components/submitted-prompt.js +50 -0
  77. package/dist/ui/components/text-selection.d.ts +53 -0
  78. package/dist/ui/components/text-selection.js +168 -0
  79. package/dist/ui/components/text.d.ts +14 -0
  80. package/dist/ui/components/text.js +40 -0
  81. package/dist/ui/components/transcript-viewport.d.ts +104 -0
  82. package/dist/ui/components/transcript-viewport.js +381 -0
  83. package/dist/ui/settings/declarations.d.ts +8 -1
  84. package/dist/ui/settings/declarations.js +33 -5
  85. package/dist/ui/settings/migrations.d.ts +1 -1
  86. package/dist/ui/settings/migrations.js +21 -2
  87. package/dist/ui/settings/sections.js +15 -8
  88. package/dist/ui/settings/session.d.ts +2 -0
  89. package/dist/ui/settings/session.js +7 -5
  90. package/docs/architecture/boundaries.md +3 -3
  91. package/docs/architecture/project-structure.md +2 -2
  92. package/docs/architecture/resource-and-data-policy.md +1 -1
  93. package/docs/architecture/toolchain.md +5 -5
  94. package/docs/architecture/ui-reference-provenance.md +11 -2
  95. package/docs/features/launch-profiles.md +7 -14
  96. package/docs/manual-launch-instance-acceptance.md +12 -35
  97. package/docs/manual-owned-ui-checkpoint.md +2 -3
  98. package/docs/manual-pi-boundary-candidate.md +4 -5
  99. package/package.json +4 -2
  100. package/dist/features/launch/profiles.d.ts +0 -12
  101. package/dist/features/launch/profiles.js +0 -26
  102. package/dist/integrations/pi/owned-ui/route-host.d.ts +0 -29
  103. /package/dist/integrations/pi/{owned-ui → session-ui}/index.d.ts +0 -0
  104. /package/dist/integrations/pi/{owned-ui → session-ui}/index.js +0 -0
  105. /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 = KeybindingsManager.create(options.agentDir);
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
- handleInput: data => editor.handleInput(data),
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 => editor.setText(text),
119
- insertText: text => editor.insertTextAtCursor(text),
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) {
@@ -4,4 +4,4 @@ export declare function createPiShellHeader(options?: PiShellHeaderOptions): PiS
4
4
  export declare function createPiShellLoadedResources(resources: readonly PiShellResourceEntry[], initialExpanded?: boolean): PiShellLoadedResourcesPort;
5
5
  export declare function createPiShellStatus(view: OwnedUiSessionViewModel, runtime?: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">): PiShellStatusPort;
6
6
  export declare function createPiShellFooter(view: OwnedUiSessionViewModel, cwd: string): PiShellViewComponentPort;
7
- export declare function createPiQueuedInputStatus(submissions: readonly string[]): PiShellQueuedInputPort;
7
+ export declare function createPiQueuedInputStatus(submissions: readonly string[], presentation?: "pinned" | "custom-viewport"): PiShellQueuedInputPort;
@@ -110,14 +110,14 @@ export function createPiShellFooter(view, cwd) {
110
110
  dispose: () => footer.dispose(),
111
111
  };
112
112
  }
113
- export function createPiQueuedInputStatus(submissions) {
114
- const text = new Text(queuedInputText(submissions), 1, 0);
113
+ export function createPiQueuedInputStatus(submissions, presentation = "pinned") {
114
+ const text = new Text(queuedInputText(submissions, presentation), 1, 0);
115
115
  return {
116
116
  render: width => submissions.length === 0 ? [] : text.render(width),
117
117
  invalidate: () => text.invalidate(),
118
118
  update(next) {
119
119
  submissions = next;
120
- text.setText(queuedInputText(next));
120
+ text.setText(queuedInputText(next, presentation));
121
121
  },
122
122
  };
123
123
  }
@@ -135,9 +135,18 @@ function statusComponent(view, ui, workingOverride) {
135
135
  function statusSignature(view, workingOverride) {
136
136
  return `${view.lifecycle}\u0000${workingOverride ?? ""}\u0000${view.status.workingMessage ?? ""}\u0000${view.status.diagnostics.at(-1) ?? ""}`;
137
137
  }
138
- function queuedInputText(submissions) {
138
+ function queuedInputText(submissions, presentation) {
139
+ if (submissions.length === 0)
140
+ return "";
139
141
  const theme = piTheme();
140
- return submissions.map(submission => theme.fg("muted", `Steering: ${submission.replaceAll("\n", " ⏎ ")}`)).join("\n");
142
+ if (presentation === "pinned") {
143
+ return submissions.map(submission => theme.fg("muted", `Steering: ${submission.replaceAll("\n", " ⏎ ")}`)).join("\n");
144
+ }
145
+ const messages = submissions.map(submission => theme.fg("dim", `Steering: ${submission.replaceAll("\n", " ⏎ ")}`));
146
+ const dequeueHint = theme.fg("dim", "↳ Alt+Up to edit all queued messages");
147
+ // The custom viewport matches Pi's interactive queue presentation while the
148
+ // comparison shell remains byte-for-byte compatible with its pinned fixture.
149
+ return ["", ...messages, dequeueHint].join("\n");
141
150
  }
142
151
  function compactHeaderText() {
143
152
  const theme = piTheme();
@@ -1,5 +1,7 @@
1
1
  import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
2
2
  import type { OwnedUiTranscriptBlock } from "../../../contracts/owned-ui/index.js";
3
+ import { type PiShellSubmittedPromptComposer } from "./submitted-prompt-adapter.js";
4
+ export type { PiShellSubmittedPromptComposer } from "./submitted-prompt-adapter.js";
3
5
  import { type PiShellComponentPort, type PiShellExtensionRendererResolver, type PiShellTranscriptComponentPort } from "./shell-shared-facade.js";
4
6
  export interface PiShellSessionInfoPresentation {
5
7
  readonly sessionName?: string;
@@ -35,7 +37,7 @@ export declare function renderPiShellStatusText(message: string, width: number):
35
37
  export declare function createPiShellSessionInfo(presentation: PiShellSessionInfoPresentation): PiShellComponentPort;
36
38
  export declare function createPiShellChangelog(markdown: string): PiShellComponentPort;
37
39
  export declare function createPiShellHotkeys(): PiShellComponentPort;
38
- export declare function createPiShellTranscriptComponent(initial: OwnedUiTranscriptBlock, cwd: string, extensions?: PiShellExtensionRendererResolver): PiShellTranscriptComponentPort;
40
+ export declare function createPiShellTranscriptComponent(initial: OwnedUiTranscriptBlock, cwd: string, extensions?: PiShellExtensionRendererResolver, submittedPrompt?: PiShellSubmittedPromptComposer): PiShellTranscriptComponentPort;
39
41
  export declare function renderPiShellTranscriptBlock(block: OwnedUiTranscriptBlock, width: number, cwd: string): readonly string[];
40
42
  /**
41
43
  * Pinned Pi's CLI prints startup diagnostics with `reportDiagnostics` before
@@ -54,4 +56,3 @@ export declare function renderPiShellStartupDiagnostic(diagnostic: {
54
56
  export declare function renderPiShellPackageUpdateNotice(packages: readonly string[], width: number): readonly string[];
55
57
  type PiAssistantMessage = NonNullable<ConstructorParameters<typeof AssistantMessageComponent>[0]>;
56
58
  export declare function validatedAssistantMessage(block: OwnedUiTranscriptBlock): PiAssistantMessage;
57
- export {};
@@ -1,8 +1,9 @@
1
1
  import { AssistantMessageComponent, BashExecutionComponent, CompactionSummaryMessageComponent, CustomMessageComponent, DynamicBorder, getMarkdownTheme, parseSkillBlock, ToolExecutionComponent, UserMessageComponent, } from "@earendil-works/pi-coding-agent";
2
- import { SkillInvocationMessageComponent, } from "./upstream/components/skill-invocation-message.js";
2
+ import { SkillInvocationMessageComponent } from "./upstream/components/skill-invocation-message.js";
3
3
  import { Container, Markdown, Spacer, Text, } from "#pi-tui";
4
4
  import { PRODUCT_TEXT } from "../../../product-identity.js";
5
- import { KeybindingsManager, } from "./upstream/adjacent/core/keybindings.js";
5
+ import { createPiSubmittedPromptComponent, } from "./submitted-prompt-adapter.js";
6
+ import { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
6
7
  import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
7
8
  import { componentPort, createTuiFacade, ensureTheme, formatSessionTokens, isRecord, } from "./shell-shared-facade.js";
8
9
  export function renderPiShellStatusText(message, width) {
@@ -106,11 +107,11 @@ export function createPiShellHotkeys() {
106
107
  container.addChild(new DynamicBorder());
107
108
  return componentPort(container);
108
109
  }
109
- export function createPiShellTranscriptComponent(initial, cwd, extensions) {
110
+ export function createPiShellTranscriptComponent(initial, cwd, extensions, submittedPrompt) {
110
111
  ensureTheme();
111
112
  let block = initial;
112
113
  let expanded = false;
113
- let component = transcriptComponent(block, cwd, expanded, extensions);
114
+ let component = transcriptComponent(block, cwd, expanded, extensions, submittedPrompt);
114
115
  return {
115
116
  get id() { return block.id; },
116
117
  get revision() { return block.revision; },
@@ -122,7 +123,7 @@ export function createPiShellTranscriptComponent(initial, cwd, extensions) {
122
123
  const previous = block;
123
124
  block = next;
124
125
  if (!updateTranscriptComponent(component, previous, next, expanded)) {
125
- component = transcriptComponent(block, cwd, expanded, extensions);
126
+ component = transcriptComponent(block, cwd, expanded, extensions, submittedPrompt);
126
127
  }
127
128
  },
128
129
  setExpanded(next) {
@@ -133,7 +134,7 @@ export function createPiShellTranscriptComponent(initial, cwd, extensions) {
133
134
  component.setExpanded(expanded);
134
135
  }
135
136
  else {
136
- component = transcriptComponent(block, cwd, expanded, extensions);
137
+ component = transcriptComponent(block, cwd, expanded, extensions, submittedPrompt);
137
138
  }
138
139
  },
139
140
  };
@@ -175,12 +176,12 @@ export function renderPiShellPackageUpdateNotice(packages, width) {
175
176
  container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
176
177
  return container.render(width);
177
178
  }
178
- function transcriptComponent(block, cwd, expanded, extensions) {
179
+ function transcriptComponent(block, cwd, expanded, extensions, submittedPrompt) {
179
180
  switch (block.kind) {
180
181
  case "user": {
181
182
  const skill = parseSkillBlock(block.text);
182
183
  if (!skill)
183
- return new UserMessageComponent(block.text);
184
+ return submittedPrompt ? createPiSubmittedPromptComponent(block, submittedPrompt) : new UserMessageComponent(block.text);
184
185
  const invocation = new SkillInvocationMessageComponent(skill, getMarkdownTheme());
185
186
  invocation.setExpanded(expanded);
186
187
  if (!skill.userMessage)
@@ -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
- export interface PiShellComponentPort {
4
- render(width: number): readonly string[];
5
- handleInput?(data: string): void;
6
- invalidate(): void;
7
- setFocused?(focused: boolean): void;
8
- dispose?(): void;
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 = [];
@@ -0,0 +1,13 @@
1
+ import { type Component } from "#pi-tui";
2
+ import type { OwnedUiTranscriptBlock } from "../../../contracts/owned-ui/index.js";
3
+ export interface PiShellSubmittedPromptComposer {
4
+ readonly layout: (width: number, source?: Date | number | null) => {
5
+ readonly contentWidth: number;
6
+ };
7
+ readonly compose: (contentRows: readonly string[], width: number, source?: Date | number | null, style?: {
8
+ readonly prefix?: (text: string) => string;
9
+ readonly timestamp?: (text: string) => string;
10
+ }) => readonly string[];
11
+ }
12
+ /** Pi owns Markdown and theme adaptation; the injected composer owns neutral row geometry. */
13
+ export declare function createPiSubmittedPromptComponent(block: OwnedUiTranscriptBlock, composer: PiShellSubmittedPromptComposer): Component;
@@ -0,0 +1,27 @@
1
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Markdown } from "#pi-tui";
3
+ import { piTheme } from "./theme.js";
4
+ /** Pi owns Markdown and theme adaptation; the injected composer owns neutral row geometry. */
5
+ export function createPiSubmittedPromptComponent(block, composer) {
6
+ const timestamp = numericTimestamp(block);
7
+ const markdown = new Markdown(block.text, 0, 0, getMarkdownTheme(), { color: content => piTheme().fg("userMessageText", content) }, { preserveOrderedListMarkers: true, preserveBackslashEscapes: true });
8
+ const content = {
9
+ render(width) {
10
+ const layout = composer.layout(width, timestamp);
11
+ return [...composer.compose(markdown.render(layout.contentWidth), width, timestamp, {
12
+ prefix: text => piTheme().fg("muted", text),
13
+ timestamp: text => piTheme().fg("dim", text),
14
+ })];
15
+ },
16
+ invalidate: () => markdown.invalidate(),
17
+ };
18
+ const box = new Box(0, 0, line => piTheme().bg("userMessageBg", line));
19
+ box.addChild(content);
20
+ return box;
21
+ }
22
+ function numericTimestamp(block) {
23
+ if (typeof block.payload !== "object" || block.payload === null)
24
+ return null;
25
+ const value = block.payload.timestamp;
26
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
27
+ }
@@ -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(KEYBINDINGS, userBindings);
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)
@@ -16,7 +16,10 @@ const execFileAsync = promisify(execFile);
16
16
  * that a streaming burst never holds input, large enough that an ordinary turn is one
17
17
  * batch.
18
18
  */
19
- const EVENT_DELIVERY_BATCH = 16;
19
+ // Deliver at most one engine event per event-loop turn. Transcript updates can
20
+ // be expensive in long sessions; a larger synchronous batch starves terminal
21
+ // input and makes an in-progress mouse selection appear frozen.
22
+ const EVENT_DELIVERY_BATCH = 1;
20
23
  const TOOL_UPDATE_COALESCE_MS = 50;
21
24
  const DEFAULT_SURFACE = {
22
25
  columns: 100,
@@ -1317,7 +1320,11 @@ export class PiEngineAdapter {
1317
1320
  case "retry":
1318
1321
  case "compact": {
1319
1322
  const result = await this.#sessionCommands?.execute(command.type === "prompt" || command.type === "steer" || command.type === "follow-up"
1320
- ? { type: command.type, text: command.text }
1323
+ ? {
1324
+ type: command.type,
1325
+ text: command.text,
1326
+ ...(command.images === undefined ? {} : { images: [...command.images] }),
1327
+ }
1321
1328
  : { type: command.type });
1322
1329
  if (!result || result.outcome === "rejected" || result.outcome === "failed") {
1323
1330
  throw new Error(command.type === "retry" ? "no previous prompt is available to retry" : `Pi session command failed: ${command.type}`);
@@ -1484,6 +1491,7 @@ export class PiEngineAdapter {
1484
1491
  switch (event.type) {
1485
1492
  case "agent_start":
1486
1493
  this.#agentRunActive = true;
1494
+ this.#emitEvent({ type: "agent-run-started" });
1487
1495
  this.#enterWorkState("working", "Working...");
1488
1496
  return;
1489
1497
  case "message_start":
@@ -1505,6 +1513,12 @@ export class PiEngineAdapter {
1505
1513
  }
1506
1514
  case "message_end":
1507
1515
  this.#upsertMessageBlock(event.message, "finalized");
1516
+ // Preserve the same semantic boundary v2 counted. Transcript block
1517
+ // finalization is intentionally not a substitute: rebuilds, retries,
1518
+ // thinking parts, and tool rows can all finalize independently.
1519
+ if (isRecord(event.message) && event.message.role === "assistant") {
1520
+ this.#emitEvent({ type: "assistant-message-completed" });
1521
+ }
1508
1522
  return;
1509
1523
  case "turn_end":
1510
1524
  this.#upsertMessageBlock(event.message, "finalized");
@@ -1688,7 +1702,11 @@ export class PiEngineAdapter {
1688
1702
  revision: this.#nextBlockRevision(baseId),
1689
1703
  title: "User",
1690
1704
  text: textFromContent(message.content),
1691
- payload: { role: "user", imageCount: contentImageCount(message.content) },
1705
+ payload: {
1706
+ role: "user",
1707
+ imageCount: contentImageCount(message.content),
1708
+ timestamp: typeof message.timestamp === "number" && Number.isFinite(message.timestamp) ? message.timestamp : null,
1709
+ },
1692
1710
  }];
1693
1711
  }
1694
1712
  if (message.role === "bashExecution") {
@@ -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,15 +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, this.session.isStreaming ? { streamingBehavior: "followUp" } : undefined);
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
- await this.session.steer(command.text);
21
+ // Match interactive Pi: prompt() owns template/extension expansion and
22
+ // turns the accepted steering message into the visible user row while
23
+ // later messages remain in the pending queue.
24
+ await this.session.prompt(command.text, {
25
+ streamingBehavior: "steer",
26
+ ...(command.images === undefined ? {} : { images: [...command.images] }),
27
+ });
17
28
  return { outcome: "completed" };
18
29
  case "follow-up":
19
30
  this.#lastPrompt = command.text;
20
- await this.session.followUp(command.text);
31
+ await this.session.prompt(command.text, {
32
+ streamingBehavior: "followUp",
33
+ ...(command.images === undefined ? {} : { images: [...command.images] }),
34
+ });
21
35
  return { outcome: "completed" };
22
36
  case "abort":
23
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
+ }