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

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 (81) 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 +70 -388
  13. package/dist/app/session-shell/session-terminal-presentation.d.ts +16 -0
  14. package/dist/app/session-shell/session-terminal-presentation.js +35 -0
  15. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -10
  16. package/dist/app/session-shell/session-viewport-controller.js +40 -318
  17. package/dist/app/session-shell/settings-bindings.d.ts +1 -1
  18. package/dist/app/session-shell/settings-bindings.js +2 -2
  19. package/dist/app/session-shell/viewport-pointer.d.ts +83 -0
  20. package/dist/app/session-shell/viewport-pointer.js +361 -0
  21. package/dist/composition/owned-ui.js +63 -59
  22. package/dist/contracts/owned-ui/index.d.ts +1 -1
  23. package/dist/contracts/owned-ui/index.js +1 -1
  24. package/dist/contracts/owned-ui/validation.d.ts +1 -11
  25. package/dist/contracts/owned-ui/validation.js +8 -104
  26. package/dist/contracts/presentation/index.d.ts +1 -1
  27. package/dist/contracts/presentation/terminal.d.ts +0 -6
  28. package/dist/features/owned-ui/settings-app.js +13 -208
  29. package/dist/features/owned-ui/settings-model-dialog.d.ts +41 -0
  30. package/dist/features/owned-ui/settings-model-dialog.js +194 -0
  31. package/dist/foundation/release/automatic-update.js +1 -8
  32. package/dist/foundation/release/dependency-layer.js +3 -3
  33. package/dist/foundation/release/latest-release.js +1 -1
  34. package/dist/foundation/release/managed-path.d.ts +10 -0
  35. package/dist/foundation/release/managed-path.js +26 -0
  36. package/dist/foundation/release/release-gc.js +6 -8
  37. package/dist/foundation/release/restart-certification.js +3 -4
  38. package/dist/foundation/release/update-activation.js +1 -1
  39. package/dist/foundation/release/update-execution-lease.js +1 -7
  40. package/dist/foundation/release/update-installer.d.ts +45 -0
  41. package/dist/foundation/release/update-installer.js +57 -0
  42. package/dist/foundation/release/update-recovery.js +2 -9
  43. package/dist/foundation/release/update.d.ts +4 -23
  44. package/dist/foundation/release/update.js +160 -179
  45. package/dist/integrations/pi/components/bracketed-paste.d.ts +33 -0
  46. package/dist/integrations/pi/components/bracketed-paste.js +123 -0
  47. package/dist/integrations/pi/components/owned-editor-ux.js +46 -140
  48. package/dist/integrations/pi/engine/adapter.d.ts +1 -8
  49. package/dist/integrations/pi/engine/adapter.js +6 -49
  50. package/dist/integrations/pi/engine/host.js +1 -1
  51. package/dist/integrations/pi/engine/repository-metadata.d.ts +49 -0
  52. package/dist/integrations/pi/engine/repository-metadata.js +189 -0
  53. package/dist/integrations/pi/engine/session-events.js +8 -6
  54. package/dist/integrations/pi/engine/session-runtime.d.ts +6 -12
  55. package/dist/integrations/pi/engine/session-runtime.js +21 -178
  56. package/dist/integrations/pi/engine/view-state.d.ts +21 -2
  57. package/dist/integrations/pi/engine/view-state.js +129 -1
  58. package/dist/integrations/pi/engine/workflow-runner.d.ts +0 -1
  59. package/dist/integrations/pi/engine/workflow-runner.js +254 -365
  60. package/dist/integrations/pi/engine/workflow-share.d.ts +9 -0
  61. package/dist/integrations/pi/engine/workflow-share.js +168 -0
  62. package/dist/integrations/pi/engine/workflow-support.d.ts +0 -14
  63. package/dist/integrations/pi/engine/workflow-support.js +0 -47
  64. package/dist/integrations/pi/tui-runtime/adapter.d.ts +9 -19
  65. package/dist/integrations/pi/tui-runtime/adapter.js +35 -144
  66. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +4 -4
  67. package/dist/integrations/pi/tui-runtime/index.d.ts +0 -1
  68. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +2 -2
  69. package/dist/integrations/pi/tui-runtime/presentation-adapter.d.ts +2 -2
  70. package/dist/ui/components/transcript-viewport-frame.d.ts +120 -0
  71. package/dist/ui/components/transcript-viewport-frame.js +314 -0
  72. package/dist/ui/components/transcript-viewport-types.d.ts +147 -0
  73. package/dist/ui/components/transcript-viewport.d.ts +2 -147
  74. package/dist/ui/components/transcript-viewport.js +87 -315
  75. package/package.json +3 -3
  76. package/dist/integrations/pi/engine/diagnostics-state.d.ts +0 -9
  77. package/dist/integrations/pi/engine/diagnostics-state.js +0 -34
  78. package/dist/integrations/pi/engine/usage-view.d.ts +0 -4
  79. package/dist/integrations/pi/engine/usage-view.js +0 -56
  80. package/dist/integrations/pi/tui-runtime/contracts.d.ts +0 -22
  81. /package/dist/{integrations/pi/tui-runtime/contracts.js → ui/components/transcript-viewport-types.js} +0 -0
@@ -1,11 +1,14 @@
1
+ import { SessionTerminalPresentation } from "./session-terminal-presentation.js";
1
2
  import { ShellClipboard } from "./shell-clipboard.js";
2
- import { PromptHistoryController } from "./prompt-history-controller.js";
3
+ import { SessionPromptHistory } from "./session-prompt-history.js";
4
+ import { shellResourceEntries } from "./resource-labels.js";
3
5
  import { SessionDialogCoordinator } from "./dialog-coordinator.js";
4
6
  import { LoginInteraction } from "./login-interaction.js";
5
7
  import { PromptSubmission } from "./prompt-submission.js";
6
8
  import { SessionConcerns } from "./session-concern.js";
7
9
  import { composeExitText, SessionStopLatch } from "./session-lifecycle.js";
8
10
  import { openWorkflowSelector } from "./workflow-router.js";
11
+ import { SessionSelectors } from "./session-selectors.js";
9
12
  import { bindSessionSettings, bindShellSettings } from "./settings-bindings.js";
10
13
  import { promptSuggestionSurface, suggestionPresentationBlockReason, } from "./suggestion-surface.js";
11
14
  import { CLEAR_EXIT_INTERVAL_MS, boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
@@ -14,7 +17,7 @@ import { copyAcknowledgementText } from "../../ui/components/selection-copy.js";
14
17
  import { ShellPromptSuggestions } from "./shell-prompt-suggestions.js";
15
18
  import { OwnedUiTerminalHost, componentRuntime, sessionTerminalHostOptions, } from "./shell-host.js";
16
19
  import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
17
- import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
20
+ import { OwnedUiSessionShellRoot, } from "./session-shell-root.js";
18
21
  // Invariant: a disposed shell leaves the engine a host that holds no reference back to it.
19
22
  const DETACHED_WORKFLOW_INTERACTION = { prompt: async () => null, notify() { } };
20
23
  export { OwnedUiSessionShellRoot, } from "./session-shell-root.js";
@@ -27,21 +30,19 @@ export { formatSessionResumeCommand, quoteCommandArgument } from "./session-life
27
30
  export class OwnedUiSessionPresenter {
28
31
  backend;
29
32
  #presenters;
30
- #dialogHost;
33
+ selectors;
31
34
  root;
32
35
  #host;
33
36
  #terminal;
34
37
  #submission;
35
38
  #login;
36
39
  #concerns;
37
- #listeners = new Set();
38
40
  #unsubscribe;
39
41
  #promptSuggestions;
40
42
  #promptImages;
41
43
  #skills;
42
44
  #installedCommandSignature = "";
43
- #promptHistory = null;
44
- #historyBinding;
45
+ #history = null;
45
46
  #extensionBridge;
46
47
  #stop = new SessionStopLatch();
47
48
  #dialogs;
@@ -64,7 +65,11 @@ export class OwnedUiSessionPresenter {
64
65
  #inputCoordination;
65
66
  #unbindShellSettings;
66
67
  #unbindSessionSettings;
67
- #terminalProgressEnabled = false;
68
+ #terminalPresentation = new SessionTerminalPresentation({
69
+ terminal: () => this.#terminal,
70
+ programStatus: () => this.#programStatus,
71
+ titleEnabled: () => this.#customViewport && this.#started && !this.#disposed,
72
+ });
68
73
  #programStatus;
69
74
  #fullscreenExitOutput = "transcript";
70
75
  #lastClearTime = 0;
@@ -85,8 +90,6 @@ export class OwnedUiSessionPresenter {
85
90
  this.#inputCoordination = inputPresentation?.coordination !== false;
86
91
  this.#host = host;
87
92
  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
93
  // Invariant: owned prompt limits and collapsed skills are bare-A1 replacements; comparison profiles keep pinned behavior.
91
94
  this.#promptImages = this.#customViewport ? options.promptImages ?? null : null;
92
95
  this.#skills = this.#customViewport ? options.skills ?? null : null;
@@ -124,7 +127,7 @@ export class OwnedUiSessionPresenter {
124
127
  onInterrupt: () => { void this.interrupt(); },
125
128
  onClear: () => { void this.clearOrExit(); },
126
129
  onExit: () => { void this.shutdown(); },
127
- onModelSelect: () => this.#customViewport ? this.showModelsDialog() : this.showModelSelector(),
130
+ onModelSelect: () => this.#customViewport ? this.selectors.showModelsDialog() : this.selectors.showModelSelector(),
128
131
  onModelCycle: direction => { void this.cycleModel(direction); },
129
132
  onThinkingCycle: () => { void this.cycleThinkingLevel(); },
130
133
  onThinkingToggle: () => {
@@ -150,7 +153,7 @@ export class OwnedUiSessionPresenter {
150
153
  },
151
154
  onInputSurfaceChanged: () => {
152
155
  this.#promptSuggestions?.invalidate();
153
- this.#promptHistory?.synchronize();
156
+ this.#history?.synchronize();
154
157
  },
155
158
  onCopyText: (text, cut) => {
156
159
  const delivery = this.#clipboard.copyText(text);
@@ -183,11 +186,12 @@ export class OwnedUiSessionPresenter {
183
186
  ...(startupRoute === undefined ? {} : { startupRoute }),
184
187
  });
185
188
  constructed.push(() => this.#dialogs.dispose());
189
+ this.selectors = this.#createSelectors(options.presenters);
186
190
  const initialPiSettings = this.backend.settings.snapshot();
187
191
  this.#programStatus = options.presenters.components.chrome.programStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
188
192
  this.#login = new LoginInteraction({
189
193
  terminal: this.#terminal, root: this.root, programStatus: this.#programStatus, dialogs: options.presenters.components.dialogs,
190
- syncProgress: () => this.#syncTerminalProgress(this.view()),
194
+ syncProgress: () => this.#terminalPresentation.syncProgress(this.view()),
191
195
  });
192
196
  this.#submission = this.#createSubmission(cwd);
193
197
  this.#reloadPresentation = reloadPresentation;
@@ -217,24 +221,15 @@ export class OwnedUiSessionPresenter {
217
221
  terminal: this.#terminal,
218
222
  root: this.root,
219
223
  setTerminalProgress: enabled => {
220
- this.#terminalProgressEnabled = enabled;
224
+ this.#terminalPresentation.progressEnabled = enabled;
221
225
  if (this.#terminal.running)
222
- this.#syncTerminalProgress(this.view());
226
+ this.#terminalPresentation.syncProgress(this.view());
223
227
  },
224
228
  }, { customViewport: this.#customViewport });
225
229
  constructed.push(this.#unbindSessionSettings);
226
230
  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); });
231
+ this.#history = this.#createHistory(promptHistory);
232
+ constructed.push(() => { void this.#history?.close().catch(() => false); });
238
233
  }
239
234
  this.#extensionBridge = this.#createExtensionBridge();
240
235
  constructed.push(() => this.#extensionBridge.dispose());
@@ -291,7 +286,7 @@ export class OwnedUiSessionPresenter {
291
286
  return this.root.inputCoordinationSurface();
292
287
  }
293
288
  setOverlaySurfaces(surfaces) {
294
- this.root.setViewportOverlaySurfaces(surfaces);
289
+ this.root.viewport.setOverlaySurfaces(surfaces);
295
290
  }
296
291
  noteImmediatePresentation() {
297
292
  this.#streamPresentation.noteImmediatePresentation();
@@ -301,7 +296,9 @@ export class OwnedUiSessionPresenter {
301
296
  return this.#layoutParts[part];
302
297
  }
303
298
  activated() {
304
- this.#syncTerminalProgress(this.view());
299
+ const view = this.view();
300
+ this.#terminalPresentation.syncProgress(view);
301
+ this.#terminalPresentation.syncTitle(view, true);
305
302
  }
306
303
  handleViewportInput(data) {
307
304
  if (!this.#inputCoordination)
@@ -354,7 +351,7 @@ export class OwnedUiSessionPresenter {
354
351
  // resynchronizes the view, which is cheap next to re-reading the transcript.
355
352
  if (event.type === "agent-run-started") {
356
353
  this.#promptSuggestions?.invalidate();
357
- this.root.resumeViewportFollowing();
354
+ this.root.viewport.resumeFollowing();
358
355
  }
359
356
  if (event.type === "assistant-message-completed" && !event.successful) {
360
357
  this.#programStatus.setErrorMessage(this.view().status.diagnostics.at(-1));
@@ -363,14 +360,14 @@ export class OwnedUiSessionPresenter {
363
360
  this.#programStatus.handleEvent(event);
364
361
  }
365
362
  if (event.type === "assistant-message-completed") {
366
- this.root.noteCompletedAssistantMessage();
363
+ this.root.viewport.noteCompletedAssistantMessage();
367
364
  this.#promptSuggestions?.route(event, () => this.root.promptSuggestionPrepareBlockReason());
368
365
  }
369
366
  const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
370
367
  const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.identity.sessionGeneration
371
368
  ? this.#syncBlock(event.block)
372
369
  : semanticOnly ? this.view() : this.#syncView();
373
- this.#syncTerminalProgress(view);
370
+ this.#terminalPresentation.syncProgress(view);
374
371
  this.#promptSuggestions?.noteView(view, this.backend.identity.sessionGeneration !== this.#sessionGeneration);
375
372
  if (event.type === "status" && /^(Retrying|Compacting)/.test(event.status.workingMessage ?? "")) {
376
373
  this.#promptSuggestions?.invalidate();
@@ -395,8 +392,9 @@ export class OwnedUiSessionPresenter {
395
392
  return;
396
393
  this.#started = true;
397
394
  this.#terminal.start();
398
- this.#promptHistory?.start();
399
- this.#syncTerminalProgress(this.view());
395
+ this.#terminalPresentation.syncTitle(this.view());
396
+ this.#history?.start();
397
+ this.#terminalPresentation.syncProgress(this.view());
400
398
  void this.backend.extensions.bindExtensionUi(this.#extensionBridge.context, () => { void this.shutdown(); });
401
399
  this.#syncView();
402
400
  // Performance: the spare clipboard helpers fork after the first frame is out, so startup never waits on them.
@@ -405,15 +403,10 @@ export class OwnedUiSessionPresenter {
405
403
  return;
406
404
  this.#dialogs.presentStartupRoute();
407
405
  void this.#presenters.components.dialogs.lazySelectors().then(selectors => selectors.prepare()).catch(() => { });
408
- this.root.warmPastePreparation();
406
+ this.root.promptChips.warm();
409
407
  this.#clipboard.warm();
410
408
  });
411
409
  }
412
- onView(listener) {
413
- this.#listeners.add(listener);
414
- listener(this.view());
415
- return () => this.#listeners.delete(listener);
416
- }
417
410
  waitUntilStopped() {
418
411
  return this.#stop.stopped;
419
412
  }
@@ -450,9 +443,9 @@ export class OwnedUiSessionPresenter {
450
443
  if (now - this.#lastEscapeTime < 500) {
451
444
  this.#lastEscapeTime = 0;
452
445
  if (action === "tree")
453
- await this.showTreeSelector();
446
+ await this.selectors.showTreeSelector();
454
447
  else
455
- this.showForkSelector();
448
+ this.selectors.showForkSelector();
456
449
  return { outcome: "completed", diagnostic: null };
457
450
  }
458
451
  this.#lastEscapeTime = now;
@@ -461,36 +454,6 @@ export class OwnedUiSessionPresenter {
461
454
  async abort() {
462
455
  return this.#submission.execute(this.#simple("abort"));
463
456
  }
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
457
  async setThinkingLevel(thinkingLevel) {
495
458
  return this.#submission.execute({
496
459
  type: "set-thinking-level",
@@ -519,185 +482,6 @@ export class OwnedUiSessionPresenter {
519
482
  showSelector(title, options, onSelect, onCancel) {
520
483
  return this.#dialogs.showSelector(title, options, onSelect, onCancel);
521
484
  }
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
485
  shutdown() {
702
486
  this.#shutdownPromise ??= this.#shutdown().finally(() => {
703
487
  // Invariant: a failed quit is retryable; ordinary shutdown has disposed the shell already.
@@ -757,7 +541,7 @@ export class OwnedUiSessionPresenter {
757
541
  return { outcome: "completed", diagnostic: null };
758
542
  }
759
543
  }
760
- const selected = openWorkflowSelector(request, this);
544
+ const selected = openWorkflowSelector(request, this.selectors);
761
545
  if (selected !== undefined)
762
546
  return selected;
763
547
  if (request.command === "reload") {
@@ -807,7 +591,7 @@ export class OwnedUiSessionPresenter {
807
591
  for (const message of result.messages)
808
592
  this.root.workflow.appendMessage(message);
809
593
  }
810
- this.showModelSelector((result.detail ?? request.argument.trim()) || undefined);
594
+ this.selectors.showModelSelector((result.detail ?? request.argument.trim()) || undefined);
811
595
  return { outcome: "completed", diagnostic: null };
812
596
  }
813
597
  if (result.outcome === "requires-confirmation" && request.command === "resume") {
@@ -836,7 +620,7 @@ export class OwnedUiSessionPresenter {
836
620
  if (request.command === "tree" && result.outcome === "completed") {
837
621
  if (result.detail && !this.root.editor.getText().trim())
838
622
  this.root.editor.setText(result.detail);
839
- this.root.resumeViewportFollowing();
623
+ this.root.viewport.resumeFollowing();
840
624
  }
841
625
  if (shareDialog !== undefined && this.#customViewport && result.outcome === "completed" && result.detail !== undefined) {
842
626
  this.root.workflow.appendStatus(shareDialog.renderShellShareResult(result.message, result.detail, this.#presenters.theme.current(), url => this.#presenters.components.text.hyperlink(url)));
@@ -855,40 +639,11 @@ export class OwnedUiSessionPresenter {
855
639
  const sleep = this.#reloadPresentation?.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
856
640
  await sleep(remaining);
857
641
  }
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
642
  // Invariant: bare A1 routes `models`; the comparison profile keeps the pinned `model`/`scoped-models` pair. Hidden routes are shared.
884
643
  #isWorkflowRoute(value) {
885
644
  return this.#presenters.components.workflows.commandNames(this.backend.settings.productMode).includes(value)
886
645
  || OWNED_UI_HIDDEN_COMMAND_NAMES.includes(value);
887
646
  }
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
647
  // Invariant: one teardown per presenter, starting on a microtask so never inside the event listener that
893
648
  // requested it. Callers during the teardown share its outcome; a later call finds it done. Closing the
894
649
  // terminal host's last presenter ends the terminal.
@@ -923,9 +678,9 @@ export class OwnedUiSessionPresenter {
923
678
  this.#submission.cancelWaiting();
924
679
  let pasteCleanup = Promise.resolve();
925
680
  let historyCleanup = Promise.resolve(true);
926
- attempt(() => { historyCleanup = this.#promptHistory?.close() ?? Promise.resolve(true); });
681
+ attempt(() => { historyCleanup = this.#history?.close() ?? Promise.resolve(true); });
927
682
  attempt(() => { pasteCleanup = this.root.disposePendingPastes(); });
928
- attempt(() => this.root.clearViewportPointerState());
683
+ attempt(() => this.root.viewport.clearPointerState());
929
684
  attempt(() => this.#streamPresentation.dispose());
930
685
  attempt(() => this.#promptSuggestions?.dispose());
931
686
  attempt(() => this.#unbindShellSettings());
@@ -975,8 +730,6 @@ export class OwnedUiSessionPresenter {
975
730
  this.#streamPresentation.presentImmediate();
976
731
  }
977
732
  const view = this.view();
978
- for (const listener of this.#listeners)
979
- listener(view);
980
733
  return view;
981
734
  }
982
735
  // Rationale: extensions reach the shell only through this bridge; every surface they draw lands in the root.
@@ -1005,14 +758,40 @@ export class OwnedUiSessionPresenter {
1005
758
  setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
1006
759
  });
1007
760
  }
761
+ #createSelectors(presenters) {
762
+ return new SessionSelectors({
763
+ root: this.root,
764
+ terminal: this.#terminal,
765
+ dialogs: this.#dialogs,
766
+ presenters,
767
+ backend: this.backend,
768
+ customViewport: this.#customViewport,
769
+ disposed: () => this.#disposed,
770
+ runWorkflow: request => this.runWorkflow(request),
771
+ sendSlashPrompt: (text, typed) => this.#submission.sendSlashPrompt(text, typed),
772
+ reportError: () => this.#submission.reportError(),
773
+ input: (title, placeholder, inputOptions) => this.#extensionBridge.input(title, placeholder, inputOptions),
774
+ shutdown: () => { void this.shutdown(); },
775
+ });
776
+ }
777
+ #createHistory(promptHistory) {
778
+ return new SessionPromptHistory(promptHistory, {
779
+ editor: this.root.editor,
780
+ usesDefaultInputSurface: () => this.root.usesDefaultInputSurface(),
781
+ rehydrate: (text, resolveImage) => this.root.promptChips.rehydrateHistoryText(text, resolveImage),
782
+ requestRender: () => this.#terminal.requestRender(),
783
+ transcript: () => this.view().transcript,
784
+ currentHistorySessionId: () => this.backend.identity.currentHistorySessionId(),
785
+ });
786
+ }
1008
787
  #createSubmission(cwd) {
1009
788
  return new PromptSubmission({
1010
789
  backend: this.backend,
1011
790
  root: this.root,
1012
791
  terminal: this.#terminal,
1013
792
  cwd, statusText: this.#presenters.components.info.statusText,
1014
- promptHistory: () => this.#promptHistory,
1015
- imageSidecar: () => this.#historyBinding?.imageSidecar,
793
+ promptHistory: () => this.#history?.controller ?? null,
794
+ imageSidecar: () => this.#history?.imageSidecar,
1016
795
  disposed: () => this.#disposed,
1017
796
  correlation: prefix => this.#correlation(prefix),
1018
797
  lifecycle: () => this.view().lifecycle,
@@ -1033,7 +812,7 @@ export class OwnedUiSessionPresenter {
1033
812
  this.#login,
1034
813
  { onSessionReplaced: () => this.#extensionBridge.reset() },
1035
814
  // Invariant: a replaced session takes its transient viewport and owned-route state with it.
1036
- { onSessionReplaced: () => this.root.resetViewport() },
815
+ { onSessionReplaced: () => this.root.viewport.reset() },
1037
816
  this.#dialogs,
1038
817
  { onSessionReplaced: () => this.root.setInputSurface(null) },
1039
818
  { onSessionReplaced: () => this.root.resetExtensionUi() },
@@ -1049,64 +828,7 @@ export class OwnedUiSessionPresenter {
1049
828
  this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
1050
829
  this.root.transcript.reset();
1051
830
  }
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] : []));
1094
- }
1095
- #syncTerminalProgress(view) {
1096
- if (!this.#terminal.running)
1097
- return;
1098
- this.#terminal.setTerminalProgress(this.#terminalProgressEnabled && view.lifecycle === "busy");
1099
- if (view.lifecycle === "stopped" || view.lifecycle === "stopping") {
1100
- this.#programStatus.clear();
1101
- return;
1102
- }
1103
- this.#programStatus.setBlocked("dialog", view.dialog === null ? undefined : {
1104
- kind: "question",
1105
- message: view.dialog.title,
1106
- });
1107
- if (view.lifecycle === "failed")
1108
- this.#programStatus.setErrorMessage(view.status.diagnostics.at(-1));
1109
- this.#programStatus.report();
831
+ this.#history?.rebind(replaced, scope);
1110
832
  }
1111
833
  #syncView() {
1112
834
  this.#streamPresentation.noteImmediatePresentation();
@@ -1114,11 +836,10 @@ export class OwnedUiSessionPresenter {
1114
836
  if (this.backend.identity.sessionGeneration !== this.#sessionGeneration)
1115
837
  this.#concerns.replaceSession();
1116
838
  this.root.update(view);
839
+ this.#terminalPresentation.syncTitle(view);
1117
840
  this.#dialogs.syncBackend(view.dialog);
1118
841
  this.#presentStartupTrustWarning(view);
1119
842
  this.#terminal.requestRender();
1120
- for (const listener of this.#listeners)
1121
- listener(view);
1122
843
  return view;
1123
844
  }
1124
845
  // Rationale: a cancelled or failed trust choice explains the restricted session where the
@@ -1188,7 +909,7 @@ export class OwnedUiSessionPresenter {
1188
909
  const skills = this.#skillSummaries();
1189
910
  const trimmed = argument.trim();
1190
911
  if (trimmed.length === 0) {
1191
- this.showSkillsSelector(skills);
912
+ this.selectors.showSkillsSelector(skills);
1192
913
  return { outcome: "completed", diagnostic: null };
1193
914
  }
1194
915
  const separator = trimmed.search(/\s/u);
@@ -1202,45 +923,6 @@ export class OwnedUiSessionPresenter {
1202
923
  }
1203
924
  return this.#submission.sendSlashPrompt(this.#presenters.components.skills.prompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
1204
925
  }
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
926
  #simple(type) {
1245
927
  return { type, correlationId: this.#correlation(type), sessionId: this.backend.identity.sessionId };
1246
928
  }