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

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 (70) hide show
  1. package/README.md +53 -9
  2. package/bin/cli.js +1 -0
  3. package/bin/ui.js +17 -2
  4. package/dist/cli/dispatch.d.ts +6 -0
  5. package/dist/cli/dispatch.js +107 -81
  6. package/dist/cli/version-stats.js +7 -1
  7. package/dist/composition/owned-ui.d.ts +4 -1
  8. package/dist/composition/owned-ui.js +8 -5
  9. package/dist/contracts/agent-engine/capability-ports.d.ts +2 -2
  10. package/dist/contracts/agent-engine/domain-validation.js +22 -4
  11. package/dist/contracts/agent-engine/domain.d.ts +17 -0
  12. package/dist/contracts/owned-ui/model.d.ts +8 -0
  13. package/dist/contracts/owned-ui/validation.js +11 -0
  14. package/dist/features/owned-ui/index.d.ts +1 -0
  15. package/dist/features/owned-ui/index.js +1 -0
  16. package/dist/features/owned-ui/project-trust-prompt.d.ts +20 -0
  17. package/dist/features/owned-ui/project-trust-prompt.js +32 -0
  18. package/dist/features/owned-ui/settings-app.js +37 -9
  19. package/dist/integrations/pi/components/shell-components.d.ts +1 -0
  20. package/dist/integrations/pi/components/shell-components.js +1 -0
  21. package/dist/integrations/pi/components/shell-editor-autocomplete.js +10 -0
  22. package/dist/integrations/pi/components/shell-footer-status.js +14 -9
  23. package/dist/integrations/pi/components/shell-presenters-info.d.ts +36 -0
  24. package/dist/integrations/pi/components/shell-presenters-info.js +78 -0
  25. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +3 -36
  26. package/dist/integrations/pi/components/shell-presenters-transcript.js +88 -128
  27. package/dist/integrations/pi/components/shell-shared-facade.d.ts +11 -1
  28. package/dist/integrations/pi/engine/adapter.d.ts +15 -4
  29. package/dist/integrations/pi/engine/adapter.js +185 -37
  30. package/dist/integrations/pi/engine/http-dispatcher.d.ts +4 -0
  31. package/dist/integrations/pi/engine/http-dispatcher.js +25 -0
  32. package/dist/integrations/pi/engine/index.d.ts +3 -0
  33. package/dist/integrations/pi/engine/index.js +3 -0
  34. package/dist/integrations/pi/engine/project-trust-preflight.d.ts +22 -0
  35. package/dist/integrations/pi/engine/project-trust-preflight.js +50 -0
  36. package/dist/integrations/pi/engine/runtime-integration.d.ts +16 -1
  37. package/dist/integrations/pi/engine/runtime-integration.js +34 -4
  38. package/dist/integrations/pi/engine/settings-effects.d.ts +55 -0
  39. package/dist/integrations/pi/engine/settings-effects.js +229 -0
  40. package/dist/integrations/pi/engine/settings-integration.d.ts +14 -26
  41. package/dist/integrations/pi/engine/settings-integration.js +102 -108
  42. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  43. package/dist/integrations/pi/session-ui/clipboard-image.d.ts +11 -0
  44. package/dist/integrations/pi/session-ui/clipboard-image.js +29 -0
  45. package/dist/integrations/pi/session-ui/prompt-chips.js +5 -1
  46. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +9 -2
  47. package/dist/integrations/pi/session-ui/session-shell-root.js +83 -9
  48. package/dist/integrations/pi/session-ui/session-shell.d.ts +2 -0
  49. package/dist/integrations/pi/session-ui/session-shell.js +151 -15
  50. package/dist/integrations/pi/session-ui/session-viewport-controller.js +34 -2
  51. package/dist/integrations/pi/session-ui/system-clipboard.d.ts +9 -0
  52. package/dist/integrations/pi/session-ui/system-clipboard.js +34 -5
  53. package/dist/integrations/pi/tui-runtime/adapter.d.ts +4 -0
  54. package/dist/integrations/pi/tui-runtime/adapter.js +28 -0
  55. package/dist/native/darwin-arm64/manifest.json +1 -1
  56. package/dist/native/linux-x64/manifest.json +1 -1
  57. package/dist/native/win32-x64/manifest.json +2 -2
  58. package/dist/native/win32-x64/process-guardian.exe +0 -0
  59. package/dist/ui/components/spans.d.ts +2 -0
  60. package/dist/ui/components/spans.js +25 -0
  61. package/dist/ui/components/text.js +24 -2
  62. package/dist/ui/settings/sections.d.ts +8 -19
  63. package/dist/ui/settings/sections.js +12 -4
  64. package/dist/ui/settings/session.d.ts +8 -18
  65. package/dist/ui/settings/session.js +59 -52
  66. package/docs/architecture/project-structure.md +14 -0
  67. package/docs/ci-release-runbook.md +75 -9
  68. package/docs/features/launch-profiles.md +3 -3
  69. package/docs/repository-governance-live-acceptance.md +77 -0
  70. package/package.json +5 -2
@@ -16,5 +16,5 @@ export declare class PiWorkflowControllerPort implements AgentWorkflowPort {
16
16
  listWorkflows(): Promise<readonly AgentWorkflowDescriptor[]>;
17
17
  executeWorkflow(workflowId: string, input: AgentJsonValue, signal?: AbortSignal): Promise<AgentJsonValue>;
18
18
  }
19
- export declare const PI_BUILTIN_WORKFLOW_ROUTES: readonly ("name" | "share" | "model" | "login" | "logout" | "settings" | "reload" | "compact" | "new" | "resume" | "scoped-models" | "export" | "import" | "copy" | "session" | "changelog" | "hotkeys" | "fork" | "clone" | "tree" | "trust" | "quit" | "debug" | "arminsayshi" | "dementedelves")[];
19
+ export declare const PI_BUILTIN_WORKFLOW_ROUTES: readonly ("name" | "share" | "model" | "login" | "logout" | "settings" | "reload" | "compact" | "new" | "resume" | "session" | "scoped-models" | "export" | "import" | "copy" | "changelog" | "hotkeys" | "fork" | "clone" | "tree" | "trust" | "quit" | "debug" | "arminsayshi" | "dementedelves")[];
20
20
  export declare const PI_WORKFLOW_CAPABILITIES: readonly WorkflowCapability[];
@@ -0,0 +1,11 @@
1
+ export interface ClipboardImageData {
2
+ readonly data: string;
3
+ readonly mimeType: string;
4
+ }
5
+ /**
6
+ * Converts valid padded or unpadded standard base64 into its unique padded form.
7
+ * Clipboard adapters are untrusted at this boundary, so reject representations
8
+ * that Node's permissive base64 decoder would otherwise partially accept.
9
+ */
10
+ export declare function canonicalizeClipboardImage(image: ClipboardImageData): ClipboardImageData | null;
11
+ export declare function canonicalizeStandardBase64(value: string): string | null;
@@ -0,0 +1,29 @@
1
+ const STANDARD_BASE64 = /^[A-Za-z0-9+/]*={0,2}$/u;
2
+ /**
3
+ * Converts valid padded or unpadded standard base64 into its unique padded form.
4
+ * Clipboard adapters are untrusted at this boundary, so reject representations
5
+ * that Node's permissive base64 decoder would otherwise partially accept.
6
+ */
7
+ export function canonicalizeClipboardImage(image) {
8
+ const data = canonicalizeStandardBase64(image.data);
9
+ return data === null ? null : { data, mimeType: image.mimeType };
10
+ }
11
+ export function canonicalizeStandardBase64(value) {
12
+ if (value.length === 0 || !STANDARD_BASE64.test(value))
13
+ return null;
14
+ const paddingStart = value.indexOf("=");
15
+ const core = paddingStart < 0 ? value : value.slice(0, paddingStart);
16
+ const suppliedPadding = value.length - core.length;
17
+ const remainder = core.length % 4;
18
+ if (remainder === 1)
19
+ return null;
20
+ const requiredPadding = remainder === 0 ? 0 : 4 - remainder;
21
+ if (suppliedPadding !== 0 && suppliedPadding !== requiredPadding)
22
+ return null;
23
+ const padded = `${core}${"=".repeat(requiredPadding)}`;
24
+ const decoded = Buffer.from(padded, "base64");
25
+ if (decoded.length === 0)
26
+ return null;
27
+ const canonical = decoded.toString("base64");
28
+ return canonical === padded ? canonical : null;
29
+ }
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
  import { existsSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { canonicalizeClipboardImage } from "./clipboard-image.js";
5
6
  const CHIP_PATTERN = /\[(?:📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
6
7
  const IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|bmp|tiff?)$/iu;
7
8
  const URL_PATTERN = /^https?:\/\/[^\s\u0000-\u001f\u007f]+$/iu;
@@ -11,12 +12,15 @@ export class PromptChipStore {
11
12
  #chips = new Map();
12
13
  transformPastedContent(content) {
13
14
  if (content.kind === "image") {
15
+ const image = canonicalizeClipboardImage(content);
16
+ if (image === null)
17
+ return "";
14
18
  const id = randomBytes(5).toString("hex");
15
19
  const tag = `[📷 screenshot-${id}.png]`;
16
20
  this.#chips.set(tag, {
17
21
  kind: "image",
18
22
  tag,
19
- image: { type: "image", data: content.data, mimeType: content.mimeType },
23
+ image: { type: "image", data: image.data, mimeType: image.mimeType },
20
24
  });
21
25
  return tag;
22
26
  }
@@ -1,7 +1,7 @@
1
1
  import type { OwnedUiSessionViewModel, OwnedUiViewportSettings, OwnedUiViewportSettingsPort } from "../../../contracts/owned-ui/index.js";
2
2
  import type { UiRouteHost } from "../../../ui/apps/index.js";
3
3
  import { type PiEngineAdapter, type PiWorkflowResult } from "../engine/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";
4
+ import { type PiShellClipboardContent, type PiShellComponentPort, type PiShellEditorPort, type PiShellExtensionRendererResolver, type PiShellHeaderOptions, type PiShellHeaderPort, type PiShellImageAssetResolver, 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
6
  import { type PreparedPrompt } from "./prompt-chips.js";
7
7
  export type OwnedUiBackendPort = PiEngineAdapter;
@@ -54,7 +54,13 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
54
54
  readonly onDequeue?: () => void;
55
55
  readonly onCopyText?: (text: string) => void;
56
56
  readonly readClipboardContent?: () => Promise<PiShellClipboardContent | null>;
57
- }, startup?: PiShellHeaderOptions, agentDir?: string, extensionRenderers?: PiShellExtensionRendererResolver, sessionLayout?: "pinned" | "custom-viewport");
57
+ }, startup?: PiShellHeaderOptions, agentDir?: string, extensionRenderers?: PiShellExtensionRendererResolver, sessionLayout?: "pinned" | "custom-viewport", imageAssets?: PiShellImageAssetResolver);
58
+ setEditorPaddingX(padding: number): void;
59
+ setAutocompleteMaxVisible(maxVisible: number): void;
60
+ setOutputPad(padding: 0 | 1): void;
61
+ setHideThinkingBlock(hidden: boolean): void;
62
+ setMermaidRenderingMode(mode: "off" | "final" | "streaming"): void;
63
+ setImagePresentation(showImages: boolean, imageWidthCells: number): void;
58
64
  preparePromptSubmission(text: string): PreparedPrompt;
59
65
  update(view: OwnedUiSessionViewModel): void;
60
66
  /**
@@ -76,6 +82,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
76
82
  };
77
83
  layoutRoot(): PiTuiLayoutNode;
78
84
  transcriptComponent(id: string): PiShellTranscriptComponentPort | undefined;
85
+ exitTranscript(width: number): string;
79
86
  appendWorkflowStatus(message: string): void;
80
87
  appendWorkflowResult(result: PiWorkflowResult): void;
81
88
  appendDaxnuts(): void;
@@ -1,6 +1,6 @@
1
1
  import { backgroundSgrSpan, composeSubmittedPromptRows, displayWidth, formatSubmittedPromptTime, heldNativeHyperlinkStyle, hyperlinkSgrSpan, nativeHyperlinkStyle, overlaySpan, submittedPromptLayout, stripAnsi, } from "../../../ui/components/index.js";
2
2
  import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../engine/index.js";
3
- import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piShellVisibleWidth, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
3
+ import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piShellVisibleWidth, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
4
4
  import { PiTuiRuntimeAdapter, } from "../tui-runtime/index.js";
5
5
  import { PromptChipStore } from "./prompt-chips.js";
6
6
  import { SessionViewportController } from "./session-viewport-controller.js";
@@ -21,6 +21,7 @@ export class OwnedUiSessionShellRoot {
21
21
  #footer;
22
22
  #queued;
23
23
  #extensionRenderers;
24
+ #imageAssets;
24
25
  #promptChips = new PromptChipStore();
25
26
  #customViewport;
26
27
  #submittedPromptComposer;
@@ -28,6 +29,10 @@ export class OwnedUiSessionShellRoot {
28
29
  #componentRuntime;
29
30
  #toolsExpanded = false;
30
31
  #thinkingVisible = true;
32
+ #mermaidRenderingMode = "off";
33
+ #showImages = true;
34
+ #imageWidthCells = 80;
35
+ #outputPad = 1;
31
36
  #workflowTranscriptSequence = 0;
32
37
  #workflowStatusAnchors = new Map();
33
38
  #workflowStatusMessages = new Map();
@@ -42,7 +47,7 @@ export class OwnedUiSessionShellRoot {
42
47
  constructor(view, cwd, handlers, startup = {}, agentDir, extensionRenderers = {
43
48
  getMessageRenderer: () => undefined,
44
49
  getToolDefinition: () => undefined,
45
- }, sessionLayout = "pinned") {
50
+ }, sessionLayout = "pinned", imageAssets) {
46
51
  this.#view = view;
47
52
  this.#customViewport = sessionLayout === "custom-viewport";
48
53
  this.#submittedPromptComposer = this.#customViewport
@@ -54,6 +59,7 @@ export class OwnedUiSessionShellRoot {
54
59
  : undefined;
55
60
  this.#cwd = cwd;
56
61
  this.#extensionRenderers = extensionRenderers;
62
+ this.#imageAssets = imageAssets;
57
63
  this.#componentRuntime = handlers;
58
64
  this.header = createPiShellHeader(startup);
59
65
  this.resources = createPiShellLoadedResources(startup.resources ?? [], startup.expanded ?? false);
@@ -109,6 +115,47 @@ export class OwnedUiSessionShellRoot {
109
115
  this.#documentLayouts.clear();
110
116
  });
111
117
  }
118
+ setEditorPaddingX(padding) {
119
+ this.editor.setPaddingX(padding);
120
+ this.#documentLayouts.clear();
121
+ }
122
+ setAutocompleteMaxVisible(maxVisible) {
123
+ this.editor.setAutocompleteMaxVisible(maxVisible);
124
+ }
125
+ setOutputPad(padding) {
126
+ if (this.#outputPad === padding)
127
+ return;
128
+ this.#outputPad = padding;
129
+ this.#status.setOutputPad(padding);
130
+ for (const component of this.#transcript.values())
131
+ component.setOutputPad(padding);
132
+ this.#invalidateTranscriptPresentation();
133
+ }
134
+ setHideThinkingBlock(hidden) {
135
+ if (this.#thinkingVisible === !hidden)
136
+ return;
137
+ this.#thinkingVisible = !hidden;
138
+ for (const component of this.#transcript.values())
139
+ component.setHideThinkingBlock(hidden);
140
+ this.#invalidateTranscriptPresentation();
141
+ }
142
+ setMermaidRenderingMode(mode) {
143
+ if (this.#mermaidRenderingMode === mode)
144
+ return;
145
+ this.#mermaidRenderingMode = mode;
146
+ for (const component of this.#transcript.values())
147
+ component.setMermaidRenderingMode(mode);
148
+ this.#invalidateTranscriptPresentation();
149
+ }
150
+ setImagePresentation(showImages, imageWidthCells) {
151
+ if (this.#showImages === showImages && this.#imageWidthCells === imageWidthCells)
152
+ return;
153
+ this.#showImages = showImages;
154
+ this.#imageWidthCells = imageWidthCells;
155
+ for (const component of this.#transcript.values())
156
+ component.setImagePresentation(showImages, imageWidthCells);
157
+ this.#invalidateTranscriptPresentation();
158
+ }
112
159
  preparePromptSubmission(text) {
113
160
  return this.#promptChips.prepareSubmission(text);
114
161
  }
@@ -135,7 +182,7 @@ export class OwnedUiSessionShellRoot {
135
182
  this.#blocksById.set(block.id, block);
136
183
  const component = this.#transcript.get(block.id);
137
184
  if (component === undefined) {
138
- const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer);
185
+ const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer, this.#outputPad, !this.#thinkingVisible, this.#mermaidRenderingMode, this.#showImages, this.#imageWidthCells, this.#imageAssets);
139
186
  created.setExpanded(this.#toolsExpanded);
140
187
  this.#transcript.set(block.id, created);
141
188
  this.#transcriptOrder.push(block.id);
@@ -363,7 +410,13 @@ export class OwnedUiSessionShellRoot {
363
410
  .filter(diagnostic => diagnostic.code === "package-updates")
364
411
  .flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width)));
365
412
  rows.push(...diagnostics
366
- .filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "package-updates")
413
+ .filter(diagnostic => diagnostic.code === "changelog-collapsed" || diagnostic.code === "changelog-expanded")
414
+ .flatMap(diagnostic => diagnostic.code === "changelog-collapsed"
415
+ ? createPiShellCollapsedChangelog().render(width)
416
+ : createPiShellChangelog(diagnostic.message).render(width)));
417
+ rows.push(...diagnostics
418
+ .filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "package-updates"
419
+ && diagnostic.code !== "changelog-collapsed" && diagnostic.code !== "changelog-expanded")
367
420
  .slice(-3)
368
421
  .flatMap(diagnostic => renderPiShellTranscriptBlock({
369
422
  id: `diagnostic-${diagnostic.sequence}`,
@@ -420,6 +473,20 @@ export class OwnedUiSessionShellRoot {
420
473
  transcriptComponent(id) {
421
474
  return this.#transcript.get(id);
422
475
  }
476
+ exitTranscript(width) {
477
+ const rows = [];
478
+ for (const id of this.#transcriptOrder) {
479
+ const block = this.#blocksById.get(id);
480
+ if (block === undefined || (!this.#thinkingVisible && block.kind === "thinking"))
481
+ continue;
482
+ if (rows.length > 0 && block.kind === "user")
483
+ rows.push("");
484
+ rows.push(...this.#blockRows(id, block, width).map(row => stripAnsi(row).trimEnd()));
485
+ }
486
+ while (rows.at(-1) === "")
487
+ rows.pop();
488
+ return rows.join("\n");
489
+ }
423
490
  appendWorkflowStatus(message) {
424
491
  const previousId = this.#lastWorkflowStatusId;
425
492
  if (previousId !== undefined
@@ -432,7 +499,7 @@ export class OwnedUiSessionShellRoot {
432
499
  }
433
500
  const id = this.#appendAnchoredWorkflowComponent(width => [
434
501
  "",
435
- ...renderPiShellStatusText(this.#workflowStatusMessages.get(id) ?? message, width),
502
+ ...renderPiShellStatusText(this.#workflowStatusMessages.get(id) ?? message, width, this.#outputPad),
436
503
  ]);
437
504
  this.#workflowStatusMessages.set(id, message);
438
505
  this.#lastWorkflowStatusId = id;
@@ -517,9 +584,7 @@ export class OwnedUiSessionShellRoot {
517
584
  this.#appendAnchoredWorkflowComponent(width => ["", ...daxnuts.render(width)], () => daxnuts.dispose?.());
518
585
  }
519
586
  toggleThinkingVisibility() {
520
- this.#thinkingVisible = !this.#thinkingVisible;
521
- this.#documentLayouts.clear();
522
- this.invalidate();
587
+ this.setHideThinkingBlock(this.#thinkingVisible);
523
588
  }
524
589
  get toolsExpanded() {
525
590
  return this.#toolsExpanded;
@@ -665,6 +730,11 @@ export class OwnedUiSessionShellRoot {
665
730
  this.#footer.dispose?.();
666
731
  this.#queued.dispose?.();
667
732
  }
733
+ #invalidateTranscriptPresentation() {
734
+ this.#renderedRows.clear();
735
+ this.#documentLayouts.clear();
736
+ this.invalidate();
737
+ }
668
738
  #syncTranscript(blocks) {
669
739
  const previousIds = this.#transcriptOrder.filter(id => !id.startsWith("workflow-status-"));
670
740
  const transcriptChanged = previousIds.length !== blocks.length || blocks.some((block, index) => {
@@ -687,7 +757,7 @@ export class OwnedUiSessionShellRoot {
687
757
  for (const block of blocks) {
688
758
  const component = this.#transcript.get(block.id);
689
759
  if (component === undefined) {
690
- const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer);
760
+ const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer, this.#outputPad, !this.#thinkingVisible, this.#mermaidRenderingMode, this.#showImages, this.#imageWidthCells, this.#imageAssets);
691
761
  created.setExpanded(this.#toolsExpanded);
692
762
  this.#transcript.set(block.id, created);
693
763
  }
@@ -724,6 +794,10 @@ export class OwnedUiSessionShellRoot {
724
794
  invalidate() { },
725
795
  update() { },
726
796
  setExpanded() { },
797
+ setOutputPad() { },
798
+ setHideThinkingBlock() { },
799
+ setMermaidRenderingMode() { },
800
+ setImagePresentation() { },
727
801
  ...(dispose === undefined ? {} : { dispose }),
728
802
  };
729
803
  this.#transcript.set(id, component);
@@ -44,3 +44,5 @@ export declare class OwnedUiSessionShell {
44
44
  showScopedModelsSelector(): void;
45
45
  dispose(): Promise<void>;
46
46
  }
47
+ export declare function formatSessionResumeCommand(sessionFile: string): string;
48
+ export declare function quoteCommandArgument(value: string): string;
@@ -1,7 +1,9 @@
1
+ import { PRODUCT_TEXT } from "../../../product-identity.js";
1
2
  import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../../ui/components/index.js";
2
3
  import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../engine/index.js";
3
4
  import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
4
5
  import { PiTuiRuntimeAdapter, } from "../tui-runtime/index.js";
6
+ import { canonicalizeClipboardImage } from "./clipboard-image.js";
5
7
  import { preloadSystemClipboard, readSystemClipboardContent, writeSystemClipboardText, } from "./system-clipboard.js";
6
8
  import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
7
9
  export { OwnedUiSessionShellRoot } from "./session-shell-root.js";
@@ -26,6 +28,13 @@ export class OwnedUiSessionShell {
26
28
  #customViewport;
27
29
  #removeViewportPreInput;
28
30
  #unsubscribeSettings;
31
+ #unbindPiSettings;
32
+ #unbindTerminalSettings;
33
+ #unbindShutdownSettings;
34
+ #terminalProgressEnabled = false;
35
+ #showImages = true;
36
+ #imageWidthCells = 80;
37
+ #fullscreenExitOutput = "transcript";
29
38
  #compactionQueue = [];
30
39
  #lastClearTime = 0;
31
40
  #lastEscapeTime = 0;
@@ -73,9 +82,17 @@ export class OwnedUiSessionShell {
73
82
  await pendingClipboardWrite;
74
83
  if (options.clipboard === undefined)
75
84
  return readSystemClipboardContent();
76
- const image = await options.clipboard.readImage?.();
77
- if (image !== null && image !== undefined)
78
- return { kind: "image", ...image };
85
+ try {
86
+ const image = await options.clipboard.readImage?.();
87
+ if (image !== null && image !== undefined) {
88
+ const canonical = canonicalizeClipboardImage(image);
89
+ if (canonical !== null)
90
+ return { kind: "image", ...canonical };
91
+ }
92
+ }
93
+ catch {
94
+ // Treat an unavailable or malformed image as text-capable clipboard input.
95
+ }
79
96
  const text = await options.clipboard.readText();
80
97
  return text === null ? null : { kind: "text", text };
81
98
  },
@@ -85,7 +102,9 @@ export class OwnedUiSessionShell {
85
102
  }, this.backend.agentDir, {
86
103
  getMessageRenderer: customType => this.backend.pinnedMessageRenderer(customType),
87
104
  getToolDefinition: toolName => this.backend.pinnedToolDefinition(toolName),
88
- }, options.sessionLayout);
105
+ }, options.sessionLayout, {
106
+ resolve: assetId => this.backend.resolveTranscriptImage(assetId),
107
+ });
89
108
  // Bare A1 owns a bounded viewport and therefore always runs on the alternate
90
109
  // fullscreen surface. The pinned comparison profiles still honor Pi's mode.
91
110
  const tuiMode = this.#customViewport
@@ -100,6 +119,32 @@ export class OwnedUiSessionShell {
100
119
  };
101
120
  runtime = new PiTuiRuntimeAdapter(runtimeOptions);
102
121
  this.runtime = runtime;
122
+ const initialPiSettings = this.backend.pinnedSettingsSnapshot();
123
+ this.runtime.setHardwareCursor(initialPiSettings.showHardwareCursor);
124
+ this.runtime.setClearOnShrink(initialPiSettings.clearOnShrink);
125
+ this.#terminalProgressEnabled = initialPiSettings.showTerminalProgress;
126
+ this.#fullscreenExitOutput = initialPiSettings.fullscreenExitOutput;
127
+ this.#unbindShutdownSettings = this.backend.bindSettingsOwner("shutdown", {
128
+ fullscreenExitOutput: { apply() { } },
129
+ });
130
+ this.#unbindTerminalSettings = this.backend.bindSettingsOwner("terminal", {
131
+ showHardwareCursor: { apply: value => {
132
+ if (typeof value !== "boolean")
133
+ throw new TypeError("Hardware cursor setting is invalid");
134
+ this.runtime.setHardwareCursor(value);
135
+ } },
136
+ clearOnShrink: { apply: value => {
137
+ if (typeof value !== "boolean")
138
+ throw new TypeError("Clear-on-shrink setting is invalid");
139
+ this.runtime.setClearOnShrink(value);
140
+ } },
141
+ showTerminalProgress: { apply: value => {
142
+ if (typeof value !== "boolean")
143
+ throw new TypeError("Terminal progress setting is invalid");
144
+ this.#terminalProgressEnabled = value;
145
+ this.#syncTerminalProgress(this.view());
146
+ } },
147
+ });
103
148
  this.#removeViewportPreInput = this.#customViewport
104
149
  ? this.runtime.addPreInputListener(data => {
105
150
  // An overlay or editor-replacement screen owns its entire pointer
@@ -129,6 +174,53 @@ export class OwnedUiSessionShell {
129
174
  this.#unsubscribeSettings = this.#customViewport && options.viewportSettings
130
175
  ? options.viewportSettings.onChange(settings => this.root.setViewportConfig(settings))
131
176
  : () => { };
177
+ this.root.setEditorPaddingX(initialPiSettings.editorPaddingX);
178
+ this.root.setAutocompleteMaxVisible(initialPiSettings.autocompleteMaxVisible);
179
+ this.root.setOutputPad(initialPiSettings.outputPad);
180
+ this.root.setHideThinkingBlock(initialPiSettings.hideThinkingBlock);
181
+ this.root.setMermaidRenderingMode(initialPiSettings.mermaidRenderingMode);
182
+ this.#showImages = initialPiSettings.showImages;
183
+ this.#imageWidthCells = initialPiSettings.imageWidthCells;
184
+ this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
185
+ this.#unbindPiSettings = this.backend.bindSettingsOwner("shell", {
186
+ editorPaddingX: { apply: value => {
187
+ if (typeof value !== "number")
188
+ throw new TypeError("Editor padding is invalid");
189
+ this.root.setEditorPaddingX(value);
190
+ } },
191
+ autocompleteMaxVisible: { apply: value => {
192
+ if (typeof value !== "number")
193
+ throw new TypeError("Autocomplete maximum is invalid");
194
+ this.root.setAutocompleteMaxVisible(value);
195
+ } },
196
+ outputPad: { apply: value => {
197
+ if (value !== 0 && value !== 1)
198
+ throw new TypeError("Output padding is invalid");
199
+ this.root.setOutputPad(value);
200
+ } },
201
+ hideThinkingBlock: { apply: value => {
202
+ if (typeof value !== "boolean")
203
+ throw new TypeError("Thinking-block visibility is invalid");
204
+ this.root.setHideThinkingBlock(value);
205
+ } },
206
+ mermaidRenderingMode: { apply: value => {
207
+ if (value !== "off" && value !== "final" && value !== "streaming")
208
+ throw new TypeError("Mermaid mode is invalid");
209
+ this.root.setMermaidRenderingMode(value);
210
+ } },
211
+ showImages: { apply: value => {
212
+ if (typeof value !== "boolean")
213
+ throw new TypeError("Image visibility is invalid");
214
+ this.#showImages = value;
215
+ this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
216
+ } },
217
+ imageWidthCells: { apply: value => {
218
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
219
+ throw new TypeError("Image width is invalid");
220
+ this.#imageWidthCells = value;
221
+ this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
222
+ } },
223
+ });
132
224
  this.#extensionBridge = createPiExtensionUiBridge({
133
225
  runtime: {
134
226
  getColumns: () => this.runtime.viewport().columns,
@@ -174,6 +266,7 @@ export class OwnedUiSessionShell {
174
266
  const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.sessionGeneration
175
267
  ? this.#syncBlock(event.block)
176
268
  : semanticOnly ? this.view() : this.#syncView();
269
+ this.#syncTerminalProgress(view);
177
270
  if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
178
271
  void this.#flushCompactionQueue();
179
272
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
@@ -190,6 +283,7 @@ export class OwnedUiSessionShell {
190
283
  return;
191
284
  this.#started = true;
192
285
  this.runtime.start();
286
+ this.#syncTerminalProgress(this.view());
193
287
  if (this.#customViewport)
194
288
  this.#setPointerReporting(true);
195
289
  void this.backend.bindExtensionUi(this.#extensionBridge.context, () => { void this.shutdown(); });
@@ -238,7 +332,11 @@ export class OwnedUiSessionShell {
238
332
  }
239
333
  if (this.view().status.workingMessage?.startsWith("Compacting") === true) {
240
334
  this.root.editor.addToHistory(displayInput);
241
- this.#compactionQueue.push({ text: input, type: "steer" });
335
+ this.#compactionQueue.push({
336
+ text: input,
337
+ type: "steer",
338
+ ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
339
+ });
242
340
  this.root.appendWorkflowResult({ command: "compact", outcome: "completed", message: `Queued during compaction: ${input}` });
243
341
  this.runtime.requestRender();
244
342
  return { outcome: "completed", diagnostic: null };
@@ -336,14 +434,20 @@ export class OwnedUiSessionShell {
336
434
  return workflowAdapterResult(result);
337
435
  }
338
436
  async queueFollowUp() {
339
- const text = this.root.editor.getText().trim();
340
- if (!text)
437
+ const displayInput = this.root.editor.getText().trim();
438
+ if (!displayInput)
341
439
  return rejected("nothing to queue");
342
- this.root.editor.addToHistory(text);
440
+ const prepared = this.root.preparePromptSubmission(displayInput);
441
+ const text = prepared.text.trim();
442
+ this.root.editor.addToHistory(displayInput);
343
443
  this.root.editor.setText("");
344
444
  this.root.resumeViewportFollowing();
345
445
  if (this.view().status.workingMessage?.startsWith("Compacting") === true) {
346
- this.#compactionQueue.push({ text, type: "follow-up" });
446
+ this.#compactionQueue.push({
447
+ text,
448
+ type: "follow-up",
449
+ ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
450
+ });
347
451
  return { outcome: "completed", diagnostic: null };
348
452
  }
349
453
  return this.#execute({
@@ -351,6 +455,7 @@ export class OwnedUiSessionShell {
351
455
  correlationId: this.#correlation("follow-up"),
352
456
  sessionId: this.backend.sessionId,
353
457
  text,
458
+ ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
354
459
  });
355
460
  }
356
461
  restoreQueuedInput() {
@@ -597,12 +702,13 @@ export class OwnedUiSessionShell {
597
702
  this.#setPointerReporting(true);
598
703
  }
599
704
  }
600
- const result = this.backend.applyPinnedSettingValue(callback, value);
601
- if (result.outcome === "failed")
602
- this.root.appendWorkflowResult(result);
603
- else if (callback === "onTuiModeChange")
604
- this.root.appendWorkflowStatus(`TUI mode: ${value}`);
605
- this.runtime.requestRender();
705
+ void this.backend.applyPinnedSettingValue(callback, value).then(result => {
706
+ if (result.outcome === "failed")
707
+ this.root.appendWorkflowResult(result);
708
+ else if (callback === "onTuiModeChange")
709
+ this.root.appendWorkflowStatus(`TUI mode: ${value}`);
710
+ this.runtime.requestRender();
711
+ });
606
712
  },
607
713
  onCancel: close,
608
714
  });
@@ -824,11 +930,27 @@ export class OwnedUiSessionShell {
824
930
  this.#setPointerReporting(false, true);
825
931
  this.#removeViewportPreInput();
826
932
  this.#unsubscribeSettings();
933
+ const exitMode = this.backend.disposed
934
+ ? this.#fullscreenExitOutput
935
+ : this.backend.pinnedSettingsSnapshot().fullscreenExitOutput;
936
+ const exitTranscript = this.root.exitTranscript(this.runtime.viewport().columns);
937
+ const sessionFile = this.backend.currentSessionFile();
938
+ const resumeHint = sessionFile === null ? "" : `Resume: ${formatSessionResumeCommand(sessionFile)}`;
939
+ const fullscreenExitText = this.runtime.mode !== "fullscreen"
940
+ ? ""
941
+ : exitMode === "resume-hint"
942
+ ? resumeHint
943
+ : [exitTranscript, resumeHint].filter(Boolean).join("\n\n");
944
+ this.#unbindPiSettings();
945
+ this.#unbindTerminalSettings();
946
+ this.#unbindShutdownSettings();
827
947
  this.#unsubscribe();
828
948
  this.#dialogHandle?.hide();
829
949
  await this.backend.unbindExtensionUi();
830
950
  this.#extensionBridge.dispose();
831
951
  await this.runtime.dispose();
952
+ if (fullscreenExitText.length > 0)
953
+ this.runtime.writeAfterStop(`${fullscreenExitText}\n`);
832
954
  }
833
955
  /**
834
956
  * Applies one transcript block without re-reading the session. The listeners still hear
@@ -842,6 +964,11 @@ export class OwnedUiSessionShell {
842
964
  listener(view);
843
965
  return view;
844
966
  }
967
+ #syncTerminalProgress(view) {
968
+ if (!this.runtime.active)
969
+ return;
970
+ this.runtime.setTerminalProgress(this.#terminalProgressEnabled && view.lifecycle === "busy");
971
+ }
845
972
  #syncView() {
846
973
  const view = this.view();
847
974
  if (this.backend.sessionGeneration !== this.#sessionGeneration) {
@@ -1117,6 +1244,7 @@ export class OwnedUiSessionShell {
1117
1244
  correlationId: this.#correlation(`compaction-${item.type}`),
1118
1245
  sessionId: this.backend.sessionId,
1119
1246
  text: item.text,
1247
+ ...(item.images === undefined ? {} : { images: item.images }),
1120
1248
  });
1121
1249
  }
1122
1250
  }
@@ -1131,6 +1259,14 @@ export class OwnedUiSessionShell {
1131
1259
  return `pi-shell-${prefix}-${this.#sequence}`;
1132
1260
  }
1133
1261
  }
1262
+ export function formatSessionResumeCommand(sessionFile) {
1263
+ return `${PRODUCT_TEXT.commandName} --session ${quoteCommandArgument(sessionFile)}`;
1264
+ }
1265
+ export function quoteCommandArgument(value) {
1266
+ if (process.platform === "win32")
1267
+ return `"${value.replaceAll("\"", "\"\"")}"`;
1268
+ return `'${value.replaceAll("'", `'\\''`)}'`;
1269
+ }
1134
1270
  function modelReference(model) {
1135
1271
  if (typeof model !== "object" || model === null)
1136
1272
  return "";