@timurproko/a1 0.2.7-dev.1255 → 0.2.7-dev.1264

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 (78) hide show
  1. package/dist/app/session-shell/prompt-submission.d.ts +1 -1
  2. package/dist/app/session-shell/prompt-submission.js +9 -9
  3. package/dist/app/session-shell/resource-labels.d.ts +2 -0
  4. package/dist/app/session-shell/resource-labels.js +158 -0
  5. package/dist/app/session-shell/session-prompt-history.d.ts +29 -0
  6. package/dist/app/session-shell/session-prompt-history.js +80 -0
  7. package/dist/app/session-shell/session-selectors.d.ts +46 -0
  8. package/dist/app/session-shell/session-selectors.js +269 -0
  9. package/dist/app/session-shell/session-shell-root.d.ts +6 -28
  10. package/dist/app/session-shell/session-shell-root.js +26 -220
  11. package/dist/app/session-shell/session-shell.d.ts +3 -23
  12. package/dist/app/session-shell/session-shell.js +54 -365
  13. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -10
  14. package/dist/app/session-shell/session-viewport-controller.js +40 -318
  15. package/dist/app/session-shell/settings-bindings.d.ts +1 -1
  16. package/dist/app/session-shell/settings-bindings.js +2 -2
  17. package/dist/app/session-shell/viewport-pointer.d.ts +83 -0
  18. package/dist/app/session-shell/viewport-pointer.js +361 -0
  19. package/dist/composition/owned-ui.js +63 -59
  20. package/dist/contracts/owned-ui/index.d.ts +1 -1
  21. package/dist/contracts/owned-ui/index.js +1 -1
  22. package/dist/contracts/owned-ui/validation.d.ts +1 -11
  23. package/dist/contracts/owned-ui/validation.js +8 -104
  24. package/dist/contracts/presentation/index.d.ts +1 -1
  25. package/dist/contracts/presentation/terminal.d.ts +0 -6
  26. package/dist/features/owned-ui/settings-app.js +13 -208
  27. package/dist/features/owned-ui/settings-model-dialog.d.ts +41 -0
  28. package/dist/features/owned-ui/settings-model-dialog.js +194 -0
  29. package/dist/foundation/release/automatic-update.js +1 -8
  30. package/dist/foundation/release/dependency-layer.js +3 -3
  31. package/dist/foundation/release/latest-release.js +1 -1
  32. package/dist/foundation/release/managed-path.d.ts +10 -0
  33. package/dist/foundation/release/managed-path.js +26 -0
  34. package/dist/foundation/release/release-gc.js +6 -8
  35. package/dist/foundation/release/restart-certification.js +3 -4
  36. package/dist/foundation/release/update-activation.js +1 -1
  37. package/dist/foundation/release/update-execution-lease.js +1 -7
  38. package/dist/foundation/release/update-installer.d.ts +45 -0
  39. package/dist/foundation/release/update-installer.js +57 -0
  40. package/dist/foundation/release/update-recovery.js +2 -9
  41. package/dist/foundation/release/update.d.ts +4 -23
  42. package/dist/foundation/release/update.js +160 -179
  43. package/dist/integrations/pi/components/bracketed-paste.d.ts +33 -0
  44. package/dist/integrations/pi/components/bracketed-paste.js +123 -0
  45. package/dist/integrations/pi/components/owned-editor-ux.js +46 -140
  46. package/dist/integrations/pi/engine/adapter.d.ts +1 -8
  47. package/dist/integrations/pi/engine/adapter.js +6 -49
  48. package/dist/integrations/pi/engine/host.js +1 -1
  49. package/dist/integrations/pi/engine/repository-metadata.d.ts +49 -0
  50. package/dist/integrations/pi/engine/repository-metadata.js +189 -0
  51. package/dist/integrations/pi/engine/session-runtime.d.ts +6 -12
  52. package/dist/integrations/pi/engine/session-runtime.js +21 -178
  53. package/dist/integrations/pi/engine/view-state.d.ts +21 -2
  54. package/dist/integrations/pi/engine/view-state.js +129 -1
  55. package/dist/integrations/pi/engine/workflow-runner.d.ts +0 -1
  56. package/dist/integrations/pi/engine/workflow-runner.js +254 -365
  57. package/dist/integrations/pi/engine/workflow-share.d.ts +9 -0
  58. package/dist/integrations/pi/engine/workflow-share.js +168 -0
  59. package/dist/integrations/pi/engine/workflow-support.d.ts +0 -14
  60. package/dist/integrations/pi/engine/workflow-support.js +0 -47
  61. package/dist/integrations/pi/tui-runtime/adapter.d.ts +9 -19
  62. package/dist/integrations/pi/tui-runtime/adapter.js +35 -144
  63. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +4 -4
  64. package/dist/integrations/pi/tui-runtime/index.d.ts +0 -1
  65. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +2 -2
  66. package/dist/integrations/pi/tui-runtime/presentation-adapter.d.ts +2 -2
  67. package/dist/ui/components/transcript-viewport-frame.d.ts +120 -0
  68. package/dist/ui/components/transcript-viewport-frame.js +314 -0
  69. package/dist/ui/components/transcript-viewport-types.d.ts +147 -0
  70. package/dist/ui/components/transcript-viewport.d.ts +2 -147
  71. package/dist/ui/components/transcript-viewport.js +87 -315
  72. package/package.json +3 -3
  73. package/dist/integrations/pi/engine/diagnostics-state.d.ts +0 -9
  74. package/dist/integrations/pi/engine/diagnostics-state.js +0 -34
  75. package/dist/integrations/pi/engine/usage-view.d.ts +0 -4
  76. package/dist/integrations/pi/engine/usage-view.js +0 -56
  77. package/dist/integrations/pi/tui-runtime/contracts.d.ts +0 -22
  78. /package/dist/{integrations/pi/tui-runtime/contracts.js → ui/components/transcript-viewport-types.js} +0 -0
@@ -1,11 +1,13 @@
1
1
  import { ShellClipboard } from "./shell-clipboard.js";
2
- import { PromptHistoryController } from "./prompt-history-controller.js";
2
+ import { SessionPromptHistory } from "./session-prompt-history.js";
3
+ import { shellResourceEntries } from "./resource-labels.js";
3
4
  import { SessionDialogCoordinator } from "./dialog-coordinator.js";
4
5
  import { LoginInteraction } from "./login-interaction.js";
5
6
  import { PromptSubmission } from "./prompt-submission.js";
6
7
  import { SessionConcerns } from "./session-concern.js";
7
8
  import { composeExitText, SessionStopLatch } from "./session-lifecycle.js";
8
9
  import { openWorkflowSelector } from "./workflow-router.js";
10
+ import { SessionSelectors } from "./session-selectors.js";
9
11
  import { bindSessionSettings, bindShellSettings } from "./settings-bindings.js";
10
12
  import { promptSuggestionSurface, suggestionPresentationBlockReason, } from "./suggestion-surface.js";
11
13
  import { CLEAR_EXIT_INTERVAL_MS, boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
@@ -14,7 +16,7 @@ import { copyAcknowledgementText } from "../../ui/components/selection-copy.js";
14
16
  import { ShellPromptSuggestions } from "./shell-prompt-suggestions.js";
15
17
  import { OwnedUiTerminalHost, componentRuntime, sessionTerminalHostOptions, } from "./shell-host.js";
16
18
  import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
17
- import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
19
+ import { OwnedUiSessionShellRoot, } from "./session-shell-root.js";
18
20
  // Invariant: a disposed shell leaves the engine a host that holds no reference back to it.
19
21
  const DETACHED_WORKFLOW_INTERACTION = { prompt: async () => null, notify() { } };
20
22
  export { OwnedUiSessionShellRoot, } from "./session-shell-root.js";
@@ -27,21 +29,19 @@ export { formatSessionResumeCommand, quoteCommandArgument } from "./session-life
27
29
  export class OwnedUiSessionPresenter {
28
30
  backend;
29
31
  #presenters;
30
- #dialogHost;
32
+ selectors;
31
33
  root;
32
34
  #host;
33
35
  #terminal;
34
36
  #submission;
35
37
  #login;
36
38
  #concerns;
37
- #listeners = new Set();
38
39
  #unsubscribe;
39
40
  #promptSuggestions;
40
41
  #promptImages;
41
42
  #skills;
42
43
  #installedCommandSignature = "";
43
- #promptHistory = null;
44
- #historyBinding;
44
+ #history = null;
45
45
  #extensionBridge;
46
46
  #stop = new SessionStopLatch();
47
47
  #dialogs;
@@ -85,8 +85,6 @@ export class OwnedUiSessionPresenter {
85
85
  this.#inputCoordination = inputPresentation?.coordination !== false;
86
86
  this.#host = host;
87
87
  this.#terminal = host.connect(this);
88
- // Invariant: the host's optional members follow the layout, so it is built only once the layout is known.
89
- this.#dialogHost = this.#createDialogHost();
90
88
  // Invariant: owned prompt limits and collapsed skills are bare-A1 replacements; comparison profiles keep pinned behavior.
91
89
  this.#promptImages = this.#customViewport ? options.promptImages ?? null : null;
92
90
  this.#skills = this.#customViewport ? options.skills ?? null : null;
@@ -124,7 +122,7 @@ export class OwnedUiSessionPresenter {
124
122
  onInterrupt: () => { void this.interrupt(); },
125
123
  onClear: () => { void this.clearOrExit(); },
126
124
  onExit: () => { void this.shutdown(); },
127
- onModelSelect: () => this.#customViewport ? this.showModelsDialog() : this.showModelSelector(),
125
+ onModelSelect: () => this.#customViewport ? this.selectors.showModelsDialog() : this.selectors.showModelSelector(),
128
126
  onModelCycle: direction => { void this.cycleModel(direction); },
129
127
  onThinkingCycle: () => { void this.cycleThinkingLevel(); },
130
128
  onThinkingToggle: () => {
@@ -150,7 +148,7 @@ export class OwnedUiSessionPresenter {
150
148
  },
151
149
  onInputSurfaceChanged: () => {
152
150
  this.#promptSuggestions?.invalidate();
153
- this.#promptHistory?.synchronize();
151
+ this.#history?.synchronize();
154
152
  },
155
153
  onCopyText: (text, cut) => {
156
154
  const delivery = this.#clipboard.copyText(text);
@@ -183,6 +181,7 @@ export class OwnedUiSessionPresenter {
183
181
  ...(startupRoute === undefined ? {} : { startupRoute }),
184
182
  });
185
183
  constructed.push(() => this.#dialogs.dispose());
184
+ this.selectors = this.#createSelectors(options.presenters);
186
185
  const initialPiSettings = this.backend.settings.snapshot();
187
186
  this.#programStatus = options.presenters.components.chrome.programStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
188
187
  this.#login = new LoginInteraction({
@@ -224,17 +223,8 @@ export class OwnedUiSessionPresenter {
224
223
  }, { customViewport: this.#customViewport });
225
224
  constructed.push(this.#unbindSessionSettings);
226
225
  if (this.#customViewport && promptHistory !== undefined) {
227
- this.#historyBinding = { options: promptHistory, scope: promptHistory.scope ?? "unique", sessionId: promptHistory.sessionId, imageSidecar: promptHistory.imageSidecar };
228
- this.#promptHistory = new PromptHistoryController({
229
- editor: this.root.editor,
230
- store: promptHistory.store,
231
- limit: promptHistory.limit,
232
- fallback: this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []),
233
- active: () => this.root.usesDefaultInputSurface(),
234
- render: () => this.#terminal.requestRender(),
235
- rehydrate: value => this.root.rehydrateHistoryText(value, id => this.#historyBinding?.imageSidecar?.readAttachment(id) ?? null),
236
- });
237
- constructed.push(() => { void this.#promptHistory?.close().catch(() => false); });
226
+ this.#history = this.#createHistory(promptHistory);
227
+ constructed.push(() => { void this.#history?.close().catch(() => false); });
238
228
  }
239
229
  this.#extensionBridge = this.#createExtensionBridge();
240
230
  constructed.push(() => this.#extensionBridge.dispose());
@@ -291,7 +281,7 @@ export class OwnedUiSessionPresenter {
291
281
  return this.root.inputCoordinationSurface();
292
282
  }
293
283
  setOverlaySurfaces(surfaces) {
294
- this.root.setViewportOverlaySurfaces(surfaces);
284
+ this.root.viewport.setOverlaySurfaces(surfaces);
295
285
  }
296
286
  noteImmediatePresentation() {
297
287
  this.#streamPresentation.noteImmediatePresentation();
@@ -354,7 +344,7 @@ export class OwnedUiSessionPresenter {
354
344
  // resynchronizes the view, which is cheap next to re-reading the transcript.
355
345
  if (event.type === "agent-run-started") {
356
346
  this.#promptSuggestions?.invalidate();
357
- this.root.resumeViewportFollowing();
347
+ this.root.viewport.resumeFollowing();
358
348
  }
359
349
  if (event.type === "assistant-message-completed" && !event.successful) {
360
350
  this.#programStatus.setErrorMessage(this.view().status.diagnostics.at(-1));
@@ -363,7 +353,7 @@ export class OwnedUiSessionPresenter {
363
353
  this.#programStatus.handleEvent(event);
364
354
  }
365
355
  if (event.type === "assistant-message-completed") {
366
- this.root.noteCompletedAssistantMessage();
356
+ this.root.viewport.noteCompletedAssistantMessage();
367
357
  this.#promptSuggestions?.route(event, () => this.root.promptSuggestionPrepareBlockReason());
368
358
  }
369
359
  const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
@@ -395,7 +385,7 @@ export class OwnedUiSessionPresenter {
395
385
  return;
396
386
  this.#started = true;
397
387
  this.#terminal.start();
398
- this.#promptHistory?.start();
388
+ this.#history?.start();
399
389
  this.#syncTerminalProgress(this.view());
400
390
  void this.backend.extensions.bindExtensionUi(this.#extensionBridge.context, () => { void this.shutdown(); });
401
391
  this.#syncView();
@@ -405,15 +395,10 @@ export class OwnedUiSessionPresenter {
405
395
  return;
406
396
  this.#dialogs.presentStartupRoute();
407
397
  void this.#presenters.components.dialogs.lazySelectors().then(selectors => selectors.prepare()).catch(() => { });
408
- this.root.warmPastePreparation();
398
+ this.root.promptChips.warm();
409
399
  this.#clipboard.warm();
410
400
  });
411
401
  }
412
- onView(listener) {
413
- this.#listeners.add(listener);
414
- listener(this.view());
415
- return () => this.#listeners.delete(listener);
416
- }
417
402
  waitUntilStopped() {
418
403
  return this.#stop.stopped;
419
404
  }
@@ -450,9 +435,9 @@ export class OwnedUiSessionPresenter {
450
435
  if (now - this.#lastEscapeTime < 500) {
451
436
  this.#lastEscapeTime = 0;
452
437
  if (action === "tree")
453
- await this.showTreeSelector();
438
+ await this.selectors.showTreeSelector();
454
439
  else
455
- this.showForkSelector();
440
+ this.selectors.showForkSelector();
456
441
  return { outcome: "completed", diagnostic: null };
457
442
  }
458
443
  this.#lastEscapeTime = now;
@@ -461,36 +446,6 @@ export class OwnedUiSessionPresenter {
461
446
  async abort() {
462
447
  return this.#submission.execute(this.#simple("abort"));
463
448
  }
464
- async retry() {
465
- this.#promptSuggestions?.invalidate();
466
- return this.#submission.execute(this.#simple("retry"));
467
- }
468
- async compact() {
469
- this.#promptSuggestions?.invalidate();
470
- return this.#submission.execute(this.#simple("compact"));
471
- }
472
- async newSession() {
473
- this.#promptSuggestions?.invalidate();
474
- return this.#submission.execute(this.#simple("new-session"));
475
- }
476
- async resumeSession(sessionPath) {
477
- this.#promptSuggestions?.invalidate();
478
- return this.#submission.execute({
479
- type: "resume-session",
480
- correlationId: this.#correlation("resume"),
481
- sessionId: this.backend.identity.sessionId,
482
- sessionPath,
483
- });
484
- }
485
- async setModel(providerId, modelId) {
486
- this.#promptSuggestions?.invalidate();
487
- return this.#submission.execute({
488
- type: "set-model",
489
- correlationId: this.#correlation("model"),
490
- sessionId: this.backend.identity.sessionId,
491
- model: { providerId, modelId, displayName: modelId },
492
- });
493
- }
494
449
  async setThinkingLevel(thinkingLevel) {
495
450
  return this.#submission.execute({
496
451
  type: "set-thinking-level",
@@ -519,185 +474,6 @@ export class OwnedUiSessionPresenter {
519
474
  showSelector(title, options, onSelect, onCancel) {
520
475
  return this.#dialogs.showSelector(title, options, onSelect, onCancel);
521
476
  }
522
- // Rationale: presenters see the shell only as a dialog host; selectors open in the input dock and run choices as workflows.
523
- #createDialogHost() {
524
- const isDisposed = () => this.#disposed;
525
- return {
526
- get disposed() { return isDisposed(); },
527
- setInputSurface: surface => this.root.setInputSurface(surface),
528
- requestRender: () => this.#terminal.requestRender(),
529
- viewport: () => this.#terminal.viewport(),
530
- appendWorkflowStatus: text => this.root.workflow.appendStatus(text),
531
- appendWorkflowResult: result => this.root.workflow.appendResult(result),
532
- runWorkflow: request => this.runWorkflow(request),
533
- // Invariant: the custom bare-A1 surface holds frames while optional selectors load and is permanently
534
- // fullscreen; the pinned comparison profiles render immediately and keep Pi's TUI mode switch.
535
- ...(this.#customViewport
536
- ? { beginPresentationHold: () => this.#terminal.beginPresentationHold() }
537
- : { switchTuiMode: mode => this.#terminal.switchMode(mode) }),
538
- };
539
- }
540
- async showThinkingSelector() {
541
- const snapshot = this.backend.settings.snapshot();
542
- const close = () => {
543
- this.root.setFooterLevel(true);
544
- this.root.setInputSurface(null);
545
- this.#terminal.requestRender();
546
- };
547
- const select = (level) => {
548
- close();
549
- void this.runWorkflow({ command: "thinking", argument: "", selection: level });
550
- };
551
- const releasePresentation = this.#customViewport ? this.#terminal.beginPresentationHold() : undefined;
552
- try {
553
- const selectors = await this.#presenters.components.dialogs.lazySelectors();
554
- const component = await selectors.createThinking({
555
- currentLevel: snapshot.thinkingLevel,
556
- availableLevels: snapshot.availableThinkingLevels,
557
- onSelect: select,
558
- onCancel: close,
559
- onSelectAsDefault: level => this.backend.settings.setDefaultThinkingLevel(level),
560
- defaultLevel: snapshot.defaultThinkingLevel,
561
- ...(this.backend.settings.productMode === "bare" ? {
562
- presentation: {
563
- profile: "bare",
564
- cycleBinding: this.root.editor.keybindingConfig()["app.thinking.cycle"] ?? [],
565
- },
566
- } : {}),
567
- });
568
- if (this.#disposed)
569
- return;
570
- this.root.setFooterLevel(false);
571
- this.root.setInputSurface(component);
572
- this.#terminal.requestRender();
573
- }
574
- finally {
575
- releasePresentation?.();
576
- }
577
- }
578
- /** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
579
- showSkillsSelector(skills = this.#skillSummaries()) {
580
- const close = this.#dialogs.closeInputSurface;
581
- const component = this.#presenters.components.dialogs.skillsSelector({
582
- skills,
583
- onSelect: name => {
584
- close();
585
- void this.#submission.sendSlashPrompt(this.#presenters.components.skills.prompt(name), this.#presenters.components.skills.prompt(name)).catch(() => this.#submission.reportError());
586
- },
587
- onCancel: close,
588
- });
589
- this.root.setInputSurface(component);
590
- this.#terminal.requestRender();
591
- }
592
- showModelSelector(initialSearchInput) {
593
- this.#presenters.openModelSelector(this.#dialogHost, initialSearchInput);
594
- }
595
- showForkSelector() {
596
- const options = this.backend.catalog.pinnedForkOptions();
597
- if (options.length === 0) {
598
- this.root.workflow.appendResult({ command: "fork", outcome: "completed", message: "No messages to fork from", messageKind: "status" });
599
- this.#terminal.requestRender();
600
- return;
601
- }
602
- const close = this.#dialogs.closeInputSurface;
603
- const component = this.#presenters.components.dialogs.userMessageSelector(options, selection => {
604
- close();
605
- void this.runWorkflow({ command: "fork", argument: "", selection });
606
- }, close);
607
- this.root.setInputSurface(component);
608
- this.#terminal.requestRender();
609
- }
610
- async showLogoutSelector() {
611
- let options;
612
- try {
613
- options = await this.backend.catalog.pinnedLogoutOptions();
614
- }
615
- catch (error) {
616
- this.root.workflow.appendResult({
617
- command: "logout",
618
- outcome: "failed",
619
- message: `Could not read stored credentials: ${error instanceof Error ? error.message : String(error)}`,
620
- });
621
- this.#terminal.requestRender();
622
- return;
623
- }
624
- if (options.length === 0) {
625
- this.root.workflow.appendStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
626
- this.#terminal.requestRender();
627
- return;
628
- }
629
- const close = this.#dialogs.closeInputSurface;
630
- const component = this.#presenters.components.dialogs.authProviderSelector("logout", options, selection => {
631
- close();
632
- void this.runWorkflow({ command: "logout", argument: "", selection });
633
- }, close);
634
- this.root.setInputSurface(component);
635
- this.#terminal.requestRender();
636
- }
637
- showLoginMethodSelector(providerReference) {
638
- const method = this.backend.catalog.pinnedLoginMethodOptions(providerReference);
639
- if (method.options.length === 0) {
640
- this.showLoginProviderSelector(undefined, providerReference);
641
- return;
642
- }
643
- if (method.options.length === 1) {
644
- const selection = method.options[0]?.id;
645
- if (selection)
646
- void this.runWorkflow({ command: "login", argument: "", selection });
647
- return;
648
- }
649
- const close = this.#dialogs.closeInputSurface;
650
- const labels = method.options.map(option => option.label);
651
- const component = this.#presenters.components.dialogs.extensionSelector(method.title, labels, label => {
652
- const selection = method.options.find(option => option.label === label)?.id;
653
- if (!selection)
654
- return;
655
- close();
656
- void this.runWorkflow({ command: "login", argument: "", selection });
657
- }, close);
658
- this.root.setInputSurface(component);
659
- this.#terminal.requestRender();
660
- }
661
- showTreeSelector(initialSelectedId) {
662
- return this.#presenters.openTreeSelector(this.#dialogHost, (entryId, skipSummaryPrompt) => {
663
- void this.#completeTreeSelection(entryId, skipSummaryPrompt);
664
- }, initialSelectedId);
665
- }
666
- showLoginAuthTypeSelector() {
667
- const close = this.#dialogs.closeInputSurface;
668
- const labels = ["Sign in with an account", "Sign in with an API key"];
669
- const component = this.#presenters.components.dialogs.extensionSelector("Select authentication method:", labels, label => {
670
- close();
671
- this.showLoginProviderSelector(label === labels[0] ? "oauth" : "api_key");
672
- }, close);
673
- this.root.setInputSurface(component);
674
- this.#terminal.requestRender();
675
- }
676
- showLoginProviderSelector(authType, initialSearchInput) {
677
- const options = this.backend.catalog.pinnedLoginOptions(authType);
678
- if (options.length === 0) {
679
- this.root.workflow.appendStatus(authType === "oauth" ? "No account providers available." : authType === "api_key" ? "No API key providers available." : "No login providers available.");
680
- this.#terminal.requestRender();
681
- return;
682
- }
683
- const close = this.#dialogs.closeInputSurface;
684
- const component = this.#presenters.components.dialogs.authProviderSelector("login", options, id => {
685
- close();
686
- void this.runWorkflow({ command: "login", argument: "", selection: id });
687
- }, () => {
688
- close();
689
- if (authType !== undefined)
690
- this.showLoginAuthTypeSelector();
691
- }, initialSearchInput);
692
- this.root.setInputSurface(component);
693
- this.#terminal.requestRender();
694
- }
695
- showSessionSelector() {
696
- return this.#presenters.openSessionSelector(this.#dialogHost, () => { void this.shutdown(); });
697
- }
698
- showSettingsSelector() {
699
- this.#presenters.openSettingsSelector(this.#dialogHost);
700
- }
701
477
  shutdown() {
702
478
  this.#shutdownPromise ??= this.#shutdown().finally(() => {
703
479
  // Invariant: a failed quit is retryable; ordinary shutdown has disposed the shell already.
@@ -757,7 +533,7 @@ export class OwnedUiSessionPresenter {
757
533
  return { outcome: "completed", diagnostic: null };
758
534
  }
759
535
  }
760
- const selected = openWorkflowSelector(request, this);
536
+ const selected = openWorkflowSelector(request, this.selectors);
761
537
  if (selected !== undefined)
762
538
  return selected;
763
539
  if (request.command === "reload") {
@@ -807,7 +583,7 @@ export class OwnedUiSessionPresenter {
807
583
  for (const message of result.messages)
808
584
  this.root.workflow.appendMessage(message);
809
585
  }
810
- this.showModelSelector((result.detail ?? request.argument.trim()) || undefined);
586
+ this.selectors.showModelSelector((result.detail ?? request.argument.trim()) || undefined);
811
587
  return { outcome: "completed", diagnostic: null };
812
588
  }
813
589
  if (result.outcome === "requires-confirmation" && request.command === "resume") {
@@ -836,7 +612,7 @@ export class OwnedUiSessionPresenter {
836
612
  if (request.command === "tree" && result.outcome === "completed") {
837
613
  if (result.detail && !this.root.editor.getText().trim())
838
614
  this.root.editor.setText(result.detail);
839
- this.root.resumeViewportFollowing();
615
+ this.root.viewport.resumeFollowing();
840
616
  }
841
617
  if (shareDialog !== undefined && this.#customViewport && result.outcome === "completed" && result.detail !== undefined) {
842
618
  this.root.workflow.appendStatus(shareDialog.renderShellShareResult(result.message, result.detail, this.#presenters.theme.current(), url => this.#presenters.components.text.hyperlink(url)));
@@ -855,40 +631,11 @@ export class OwnedUiSessionPresenter {
855
631
  const sleep = this.#reloadPresentation?.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
856
632
  await sleep(remaining);
857
633
  }
858
- showTrustSelector() {
859
- const context = this.backend.catalog.pinnedProjectTrustContext();
860
- const close = this.#dialogs.closeInputSurface;
861
- const component = this.#presenters.components.dialogs.trustSelector({
862
- ...context,
863
- onSelect: selection => {
864
- try {
865
- this.backend.catalog.persistProjectTrust(selection.updates);
866
- close();
867
- this.root.workflow.appendStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart pi for this to take effect.`);
868
- }
869
- catch (error) {
870
- close();
871
- this.root.workflow.appendResult({ command: "trust", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
872
- }
873
- this.#terminal.requestRender();
874
- },
875
- onCancel: close,
876
- });
877
- this.root.setInputSurface(component);
878
- this.#terminal.requestRender();
879
- }
880
- showScopedModelsSelector() {
881
- this.#presenters.openScopedModelsSelector(this.#dialogHost);
882
- }
883
634
  // Invariant: bare A1 routes `models`; the comparison profile keeps the pinned `model`/`scoped-models` pair. Hidden routes are shared.
884
635
  #isWorkflowRoute(value) {
885
636
  return this.#presenters.components.workflows.commandNames(this.backend.settings.productMode).includes(value)
886
637
  || OWNED_UI_HIDDEN_COMMAND_NAMES.includes(value);
887
638
  }
888
- /** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
889
- showModelsDialog(initialQuery) {
890
- this.#presenters.openModelsDialog(this.#dialogHost, initialQuery);
891
- }
892
639
  // Invariant: one teardown per presenter, starting on a microtask so never inside the event listener that
893
640
  // requested it. Callers during the teardown share its outcome; a later call finds it done. Closing the
894
641
  // terminal host's last presenter ends the terminal.
@@ -923,9 +670,9 @@ export class OwnedUiSessionPresenter {
923
670
  this.#submission.cancelWaiting();
924
671
  let pasteCleanup = Promise.resolve();
925
672
  let historyCleanup = Promise.resolve(true);
926
- attempt(() => { historyCleanup = this.#promptHistory?.close() ?? Promise.resolve(true); });
673
+ attempt(() => { historyCleanup = this.#history?.close() ?? Promise.resolve(true); });
927
674
  attempt(() => { pasteCleanup = this.root.disposePendingPastes(); });
928
- attempt(() => this.root.clearViewportPointerState());
675
+ attempt(() => this.root.viewport.clearPointerState());
929
676
  attempt(() => this.#streamPresentation.dispose());
930
677
  attempt(() => this.#promptSuggestions?.dispose());
931
678
  attempt(() => this.#unbindShellSettings());
@@ -975,8 +722,6 @@ export class OwnedUiSessionPresenter {
975
722
  this.#streamPresentation.presentImmediate();
976
723
  }
977
724
  const view = this.view();
978
- for (const listener of this.#listeners)
979
- listener(view);
980
725
  return view;
981
726
  }
982
727
  // Rationale: extensions reach the shell only through this bridge; every surface they draw lands in the root.
@@ -1005,14 +750,40 @@ export class OwnedUiSessionPresenter {
1005
750
  setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
1006
751
  });
1007
752
  }
753
+ #createSelectors(presenters) {
754
+ return new SessionSelectors({
755
+ root: this.root,
756
+ terminal: this.#terminal,
757
+ dialogs: this.#dialogs,
758
+ presenters,
759
+ backend: this.backend,
760
+ customViewport: this.#customViewport,
761
+ disposed: () => this.#disposed,
762
+ runWorkflow: request => this.runWorkflow(request),
763
+ sendSlashPrompt: (text, typed) => this.#submission.sendSlashPrompt(text, typed),
764
+ reportError: () => this.#submission.reportError(),
765
+ input: (title, placeholder, inputOptions) => this.#extensionBridge.input(title, placeholder, inputOptions),
766
+ shutdown: () => { void this.shutdown(); },
767
+ });
768
+ }
769
+ #createHistory(promptHistory) {
770
+ return new SessionPromptHistory(promptHistory, {
771
+ editor: this.root.editor,
772
+ usesDefaultInputSurface: () => this.root.usesDefaultInputSurface(),
773
+ rehydrate: (text, resolveImage) => this.root.promptChips.rehydrateHistoryText(text, resolveImage),
774
+ requestRender: () => this.#terminal.requestRender(),
775
+ transcript: () => this.view().transcript,
776
+ currentHistorySessionId: () => this.backend.identity.currentHistorySessionId(),
777
+ });
778
+ }
1008
779
  #createSubmission(cwd) {
1009
780
  return new PromptSubmission({
1010
781
  backend: this.backend,
1011
782
  root: this.root,
1012
783
  terminal: this.#terminal,
1013
784
  cwd, statusText: this.#presenters.components.info.statusText,
1014
- promptHistory: () => this.#promptHistory,
1015
- imageSidecar: () => this.#historyBinding?.imageSidecar,
785
+ promptHistory: () => this.#history?.controller ?? null,
786
+ imageSidecar: () => this.#history?.imageSidecar,
1016
787
  disposed: () => this.#disposed,
1017
788
  correlation: prefix => this.#correlation(prefix),
1018
789
  lifecycle: () => this.view().lifecycle,
@@ -1033,7 +804,7 @@ export class OwnedUiSessionPresenter {
1033
804
  this.#login,
1034
805
  { onSessionReplaced: () => this.#extensionBridge.reset() },
1035
806
  // Invariant: a replaced session takes its transient viewport and owned-route state with it.
1036
- { onSessionReplaced: () => this.root.resetViewport() },
807
+ { onSessionReplaced: () => this.root.viewport.reset() },
1037
808
  this.#dialogs,
1038
809
  { onSessionReplaced: () => this.root.setInputSurface(null) },
1039
810
  { onSessionReplaced: () => this.root.resetExtensionUi() },
@@ -1049,48 +820,7 @@ export class OwnedUiSessionPresenter {
1049
820
  this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
1050
821
  this.root.transcript.reset();
1051
822
  }
1052
- const binding = this.#historyBinding;
1053
- if (binding?.options.forSession !== undefined || binding?.options.forScope !== undefined) {
1054
- const sessionId = this.backend.identity.currentHistorySessionId();
1055
- const selectedScope = scope ?? binding.scope;
1056
- if (selectedScope !== binding.scope || sessionId !== binding.sessionId) {
1057
- if (!replaced)
1058
- this.root.editor.recall?.restoreDraft?.();
1059
- const previous = this.#promptHistory;
1060
- this.#promptHistory = null;
1061
- binding.imageSidecar = undefined;
1062
- binding.scope = selectedScope;
1063
- binding.sessionId = sessionId ?? undefined;
1064
- void previous?.close().catch(() => false);
1065
- const destination = binding.options.forScope !== undefined
1066
- ? binding.options.forScope(selectedScope, sessionId)
1067
- : sessionId === null ? null : binding.options.forSession(sessionId);
1068
- if (destination !== null) {
1069
- binding.imageSidecar = destination.imageSidecar;
1070
- const controller = new PromptHistoryController({
1071
- editor: this.root.editor,
1072
- store: destination.store,
1073
- limit: binding.options.limit,
1074
- fallback: replaced ? this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []) : [],
1075
- active: () => this.root.usesDefaultInputSurface(),
1076
- render: () => this.#terminal.requestRender(),
1077
- rehydrate: value => this.root.rehydrateHistoryText(value, id => this.#historyBinding?.imageSidecar?.readAttachment(id) ?? null),
1078
- });
1079
- this.#promptHistory = controller;
1080
- if (this.#started)
1081
- controller.start();
1082
- }
1083
- else {
1084
- // Security: missing identity must not reopen Shared or retain another session's recall.
1085
- const fallback = replaced ? this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []).reverse() : [];
1086
- this.root.editor.recall?.reset();
1087
- this.root.editor.recall?.replace(fallback);
1088
- }
1089
- return;
1090
- }
1091
- }
1092
- if (replaced)
1093
- this.#promptHistory?.reset(this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
823
+ this.#history?.rebind(replaced, scope);
1094
824
  }
1095
825
  #syncTerminalProgress(view) {
1096
826
  if (!this.#terminal.running)
@@ -1117,8 +847,6 @@ export class OwnedUiSessionPresenter {
1117
847
  this.#dialogs.syncBackend(view.dialog);
1118
848
  this.#presentStartupTrustWarning(view);
1119
849
  this.#terminal.requestRender();
1120
- for (const listener of this.#listeners)
1121
- listener(view);
1122
850
  return view;
1123
851
  }
1124
852
  // Rationale: a cancelled or failed trust choice explains the restricted session where the
@@ -1188,7 +916,7 @@ export class OwnedUiSessionPresenter {
1188
916
  const skills = this.#skillSummaries();
1189
917
  const trimmed = argument.trim();
1190
918
  if (trimmed.length === 0) {
1191
- this.showSkillsSelector(skills);
919
+ this.selectors.showSkillsSelector(skills);
1192
920
  return { outcome: "completed", diagnostic: null };
1193
921
  }
1194
922
  const separator = trimmed.search(/\s/u);
@@ -1202,45 +930,6 @@ export class OwnedUiSessionPresenter {
1202
930
  }
1203
931
  return this.#submission.sendSlashPrompt(this.#presenters.components.skills.prompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
1204
932
  }
1205
- #treeSummary() {
1206
- return new Promise(resolve => {
1207
- this.root.setInputSurface(this.#presenters.components.dialogs.extensionSelector("Summarize Branch?", ["No summary", "Summarize", "Summarize with custom prompt"], resolve, () => resolve(undefined)), true, "opaque");
1208
- this.#terminal.requestRender();
1209
- });
1210
- }
1211
- async #completeTreeSelection(entryId, skipSummaryPrompt) {
1212
- let summarize = false;
1213
- let customInstructions;
1214
- if (!skipSummaryPrompt) {
1215
- while (true) {
1216
- const choice = await this.#treeSummary();
1217
- if (choice === undefined) {
1218
- await this.showTreeSelector(entryId);
1219
- return;
1220
- }
1221
- summarize = choice !== "No summary";
1222
- if (choice === "Summarize with custom prompt") {
1223
- customInstructions = await this.#extensionBridge.input("Custom Summarization Instructions", "", { retainSurfaceOnSettle: true });
1224
- if (customInstructions === undefined)
1225
- continue;
1226
- }
1227
- break;
1228
- }
1229
- }
1230
- this.root.setInputSurface(null);
1231
- this.#terminal.requestRender();
1232
- const result = await this.runWorkflow({
1233
- command: "tree",
1234
- argument: "",
1235
- selection: entryId,
1236
- treeSummary: {
1237
- summarize,
1238
- ...(customInstructions === undefined ? {} : { customInstructions }),
1239
- },
1240
- });
1241
- if (result.diagnostic === "Branch summarization cancelled")
1242
- await this.showTreeSelector(entryId);
1243
- }
1244
933
  #simple(type) {
1245
934
  return { type, correlationId: this.#correlation(type), sessionId: this.backend.identity.sessionId };
1246
935
  }
@@ -4,6 +4,7 @@ import type { OwnedUiViewportSettings } from "../../contracts/owned-ui/index.js"
4
4
  import { type TranscriptViewportFrame, type TranscriptViewportFrameInput } from "../../ui/components/transcript-viewport.js";
5
5
  import type { SelectionCopySnapshot } from "../../ui/components/selection-copy.js";
6
6
  import type { PasteEvent } from "./paste-protocol.js";
7
+ import { type TailControl, type TailControlRegion } from "./viewport-pointer.js";
7
8
  interface SessionViewportControllerOptions {
8
9
  readonly enabled: boolean;
9
10
  readonly editor: OwnedUiShellEditorPort;
@@ -15,13 +16,6 @@ interface SessionViewportControllerOptions {
15
16
  readonly pasteDiagnostics?: (event: PasteEvent) => void;
16
17
  readonly nextPasteDiagnosticRequest?: () => number;
17
18
  }
18
- /** One-based, inclusive terminal cells of a clickable viewport-tail control. */
19
- export interface TailControlRegion {
20
- readonly rowStart: number;
21
- readonly rowEnd: number;
22
- readonly columnStart: number;
23
- readonly columnEnd: number;
24
- }
25
19
  export interface SessionViewportInputResult {
26
20
  readonly data: string;
27
21
  readonly consumed: boolean;
@@ -54,9 +48,7 @@ export declare class SessionViewportController {
54
48
  readonly rowEnd: number;
55
49
  } | undefined): void;
56
50
  /** The visible close control of a viewport-tail notice, or undefined when none is on screen. */
57
- setTailControl(control: (TailControlRegion & {
58
- readonly activate: () => void;
59
- }) | undefined): void;
51
+ setTailControl(control: TailControl | undefined): void;
60
52
  /** Whether the pointer currently rests on the tail control; drives its hover paint. */
61
53
  tailControlHovered(target: TailControlRegion | undefined): boolean;
62
54
  /** Replacement input changed before its new bounds have been painted. */