@timurproko/a1 0.1.8-dev.157 → 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 (64) hide show
  1. package/README.md +32 -0
  2. package/bin/ui.js +17 -2
  3. package/dist/composition/owned-ui.d.ts +4 -1
  4. package/dist/composition/owned-ui.js +8 -5
  5. package/dist/contracts/agent-engine/capability-ports.d.ts +2 -2
  6. package/dist/contracts/agent-engine/domain-validation.js +22 -4
  7. package/dist/contracts/agent-engine/domain.d.ts +17 -0
  8. package/dist/contracts/owned-ui/model.d.ts +8 -0
  9. package/dist/contracts/owned-ui/validation.js +11 -0
  10. package/dist/features/owned-ui/index.d.ts +1 -0
  11. package/dist/features/owned-ui/index.js +1 -0
  12. package/dist/features/owned-ui/project-trust-prompt.d.ts +20 -0
  13. package/dist/features/owned-ui/project-trust-prompt.js +32 -0
  14. package/dist/features/owned-ui/settings-app.js +37 -9
  15. package/dist/integrations/pi/components/shell-components.d.ts +1 -0
  16. package/dist/integrations/pi/components/shell-components.js +1 -0
  17. package/dist/integrations/pi/components/shell-editor-autocomplete.js +10 -0
  18. package/dist/integrations/pi/components/shell-footer-status.js +14 -9
  19. package/dist/integrations/pi/components/shell-presenters-info.d.ts +36 -0
  20. package/dist/integrations/pi/components/shell-presenters-info.js +78 -0
  21. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +3 -36
  22. package/dist/integrations/pi/components/shell-presenters-transcript.js +88 -128
  23. package/dist/integrations/pi/components/shell-shared-facade.d.ts +11 -1
  24. package/dist/integrations/pi/engine/adapter.d.ts +15 -4
  25. package/dist/integrations/pi/engine/adapter.js +185 -37
  26. package/dist/integrations/pi/engine/http-dispatcher.d.ts +4 -0
  27. package/dist/integrations/pi/engine/http-dispatcher.js +25 -0
  28. package/dist/integrations/pi/engine/index.d.ts +3 -0
  29. package/dist/integrations/pi/engine/index.js +3 -0
  30. package/dist/integrations/pi/engine/project-trust-preflight.d.ts +22 -0
  31. package/dist/integrations/pi/engine/project-trust-preflight.js +50 -0
  32. package/dist/integrations/pi/engine/runtime-integration.d.ts +16 -1
  33. package/dist/integrations/pi/engine/runtime-integration.js +34 -4
  34. package/dist/integrations/pi/engine/settings-effects.d.ts +55 -0
  35. package/dist/integrations/pi/engine/settings-effects.js +229 -0
  36. package/dist/integrations/pi/engine/settings-integration.d.ts +14 -26
  37. package/dist/integrations/pi/engine/settings-integration.js +102 -108
  38. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  39. package/dist/integrations/pi/session-ui/clipboard-image.d.ts +11 -0
  40. package/dist/integrations/pi/session-ui/clipboard-image.js +29 -0
  41. package/dist/integrations/pi/session-ui/prompt-chips.js +5 -1
  42. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +9 -2
  43. package/dist/integrations/pi/session-ui/session-shell-root.js +83 -9
  44. package/dist/integrations/pi/session-ui/session-shell.d.ts +2 -0
  45. package/dist/integrations/pi/session-ui/session-shell.js +151 -15
  46. package/dist/integrations/pi/session-ui/session-viewport-controller.js +34 -2
  47. package/dist/integrations/pi/session-ui/system-clipboard.d.ts +9 -0
  48. package/dist/integrations/pi/session-ui/system-clipboard.js +34 -5
  49. package/dist/integrations/pi/tui-runtime/adapter.d.ts +4 -0
  50. package/dist/integrations/pi/tui-runtime/adapter.js +28 -0
  51. package/dist/native/darwin-arm64/manifest.json +1 -1
  52. package/dist/native/linux-x64/manifest.json +1 -1
  53. package/dist/native/win32-x64/manifest.json +2 -2
  54. package/dist/native/win32-x64/process-guardian.exe +0 -0
  55. package/dist/ui/components/spans.d.ts +2 -0
  56. package/dist/ui/components/spans.js +25 -0
  57. package/dist/ui/settings/sections.d.ts +8 -19
  58. package/dist/ui/settings/sections.js +12 -4
  59. package/dist/ui/settings/session.d.ts +8 -18
  60. package/dist/ui/settings/session.js +59 -52
  61. package/docs/architecture/project-structure.md +14 -0
  62. package/docs/ci-release-runbook.md +69 -4
  63. package/docs/repository-governance-live-acceptance.md +77 -0
  64. package/package.json +5 -2
@@ -1,10 +1,12 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { mkdir, rm, writeFile } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
4
5
  import { dirname, join, resolve } from "node:path";
5
6
  import { promisify } from "node:util";
6
7
  import { PRODUCT_IDENTITY } from "../../../product-identity.js";
7
- import { copyToClipboard, DefaultPackageManager, getAgentDir, ProjectTrustStore, SessionManager, } from "@earendil-works/pi-coding-agent";
8
+ import { configureOwnedHttpDispatcher } from "./http-dispatcher.js";
9
+ import { copyToClipboard, DefaultPackageManager, getAgentDir, ProjectTrustStore, SessionManager, VERSION, } from "@earendil-works/pi-coding-agent";
8
10
  import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, assertOwnedUiCommand, assertOwnedUiExtensionUiPort, assertOwnedUiSnapshot, } from "../../../contracts/owned-ui/index.js";
9
11
  import { PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "./workflows.js";
10
12
  import { createPiRuntimeIntegration } from "./runtime-integration.js";
@@ -33,6 +35,7 @@ export class PiEngineAdapter {
33
35
  #cwd;
34
36
  #agentDir;
35
37
  #sessionId;
38
+ #sessionPath;
36
39
  #workflowHost;
37
40
  #workflowInteraction;
38
41
  #listeners = new Set();
@@ -68,6 +71,7 @@ export class PiEngineAdapter {
68
71
  #messageBlockIds = new WeakMap();
69
72
  #messageFallbackIds = new Map();
70
73
  #toolBlockIds = new Map();
74
+ #transcriptImageAssets = new Map();
71
75
  #pendingToolUpdates = new Map();
72
76
  #toolUpdateFlush = null;
73
77
  #usageCache;
@@ -84,12 +88,17 @@ export class PiEngineAdapter {
84
88
  #extensionShutdown;
85
89
  #extensionBound = false;
86
90
  #disposed = false;
91
+ #settingsProductMode;
92
+ #projectTrustPrompt;
93
+ #settingsIntegration;
94
+ #settingsIntegrationManager;
87
95
  /** Lists the themes installed on this machine, when the caller supplies one. */
88
96
  #availableThemes;
89
97
  constructor(options = {}) {
90
98
  this.#cwd = options.cwd ?? process.cwd();
91
99
  this.#agentDir = options.agentDir ?? getAgentDir();
92
100
  this.#sessionId = options.sessionId ?? "owned-session-1";
101
+ this.#sessionPath = options.sessionPath;
93
102
  this.#runtimeFactory = options.createRuntime ?? createDefaultPiRuntime;
94
103
  this.#checkPackageUpdates = options.checkPackageUpdates
95
104
  ?? (options.createRuntime
@@ -97,6 +106,8 @@ export class PiEngineAdapter {
97
106
  : settingsManager => checkDefaultPiPackageUpdates(this.#cwd, this.#agentDir, settingsManager));
98
107
  this.#workflowHost = options.workflowHost ?? defaultWorkflowHost();
99
108
  this.#availableThemes = options.availableThemes ?? null;
109
+ this.#settingsProductMode = options.settingsProductMode ?? "bare";
110
+ this.#projectTrustPrompt = options.projectTrustPrompt;
100
111
  this.#workflowInteraction = { prompt: async () => null, notify() { } };
101
112
  }
102
113
  setWorkflowInteractionHost(interaction) {
@@ -111,6 +122,13 @@ export class PiEngineAdapter {
111
122
  get agentDir() {
112
123
  return this.#agentDir;
113
124
  }
125
+ resolveTranscriptImage(assetId) {
126
+ return this.#transcriptImageAssets.get(assetId) ?? null;
127
+ }
128
+ currentSessionFile() {
129
+ const value = this.#session?.sessionManager?.getSessionFile?.();
130
+ return typeof value === "string" && value.length > 0 ? value : null;
131
+ }
114
132
  get disposed() {
115
133
  return this.#disposed;
116
134
  }
@@ -121,6 +139,8 @@ export class PiEngineAdapter {
121
139
  cwd: this.#cwd,
122
140
  agentDir: this.#agentDir,
123
141
  sessionId: this.#sessionId,
142
+ ...(this.#sessionPath === undefined ? {} : { sessionPath: this.#sessionPath }),
143
+ ...(this.#projectTrustPrompt === undefined ? {} : { projectTrustPrompt: this.#projectTrustPrompt }),
124
144
  }).catch(error => {
125
145
  this.#lifecycle = "failed";
126
146
  this.#addDiagnostic("error", "engine-startup", error instanceof Error ? error.message : String(error), false);
@@ -140,6 +160,7 @@ export class PiEngineAdapter {
140
160
  this.#addDiagnostic(diagnostic.type === "error" ? "error" : diagnostic.type === "warning" ? "warning" : "info", "engine-startup", diagnostic.message, diagnostic.type !== "error");
141
161
  }
142
162
  this.#bindSession(runtime.session);
163
+ await this.#announceChangelog(runtime.services.settingsManager);
143
164
  this.#lifecycle = "ready";
144
165
  this.#editor = { ...this.#editor, submitEnabled: true };
145
166
  this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
@@ -147,6 +168,19 @@ export class PiEngineAdapter {
147
168
  void this.#announcePackageUpdates(runtime.services.settingsManager);
148
169
  return this.view();
149
170
  }
171
+ async #announceChangelog(settingsManager) {
172
+ if (!settingsManager || typeof settingsManager.getLastChangelogVersion !== "function"
173
+ || typeof settingsManager.setLastChangelogVersion !== "function")
174
+ return;
175
+ if (settingsManager.getLastChangelogVersion() === VERSION)
176
+ return;
177
+ const markdown = await this.#workflowHost.readChangelog().catch(() => "");
178
+ if (markdown.trim().length > 0) {
179
+ this.#addDiagnostic("info", settingsManager.getCollapseChangelog() ? "changelog-collapsed" : "changelog-expanded", markdown, true);
180
+ }
181
+ settingsManager.setLastChangelogVersion(VERSION);
182
+ await settingsManager.flush();
183
+ }
150
184
  async #announcePackageUpdates(settingsManager) {
151
185
  if (process.env.PI_OFFLINE)
152
186
  return;
@@ -432,16 +466,107 @@ export class PiEngineAdapter {
432
466
  /** Settings port for the live runtime, or null before the runtime is available. */
433
467
  settingsPort() {
434
468
  const settings = this.#runtime?.services.settingsManager;
435
- if (!settings)
469
+ if (!settings || typeof settings.getCompactionEnabled !== "function")
436
470
  return null;
437
- const session = this.#runtime?.session;
438
- return new PiSettingsIntegration(settings, {
439
- ...(this.#availableThemes === null ? {} : { themes: this.#availableThemes }),
440
- thinkingLevels: () => {
441
- const levels = session?.getAvailableThinkingLevels?.();
442
- return Array.isArray(levels) ? levels.map(level => String(level)) : [];
443
- },
444
- });
471
+ if (this.#settingsIntegration === undefined || this.#settingsIntegrationManager !== settings) {
472
+ this.#settingsIntegrationManager = settings;
473
+ configureOwnedHttpDispatcher(settings.getHttpIdleTimeoutMs());
474
+ this.#settingsIntegration = new PiSettingsIntegration(settings, {
475
+ ...(this.#availableThemes === null ? {} : { themes: this.#availableThemes }),
476
+ thinkingLevels: () => {
477
+ const levels = this.#runtime?.session?.getAvailableThinkingLevels?.();
478
+ return Array.isArray(levels) ? levels.map(level => String(level)) : [];
479
+ },
480
+ productMode: this.#settingsProductMode,
481
+ });
482
+ this.#settingsIntegration.bindOwner("shell", {
483
+ enableSkillCommands: { apply: value => {
484
+ if (typeof value !== "boolean")
485
+ throw new TypeError("Skill commands value is invalid");
486
+ settings.setEnableSkillCommands(value);
487
+ } },
488
+ doubleEscapeAction: { apply: value => {
489
+ if (value !== "tree" && value !== "fork" && value !== "none")
490
+ throw new TypeError("Double-Escape action is invalid");
491
+ settings.setDoubleEscapeAction(value);
492
+ } },
493
+ treeFilterMode: { apply: value => {
494
+ if (value !== "default" && value !== "no-tools" && value !== "user-only" && value !== "labeled-only" && value !== "all")
495
+ throw new TypeError("Tree filter mode is invalid");
496
+ settings.setTreeFilterMode(value);
497
+ } },
498
+ showCacheMissNotices: { apply: value => {
499
+ if (typeof value !== "boolean")
500
+ throw new TypeError("Cache-miss notice setting is invalid");
501
+ settings.setShowCacheMissNotices(value);
502
+ } },
503
+ });
504
+ this.#settingsIntegration.bindOwner("startup", {
505
+ // Deferred application is the owner operation: the next preflight reads
506
+ // the persisted default before constructing project-backed services.
507
+ defaultProjectTrust: { apply() { } },
508
+ collapseChangelog: { apply() { } },
509
+ });
510
+ this.#settingsIntegration.bindOwner("agent", {
511
+ autoCompact: { apply: value => {
512
+ if (typeof value !== "boolean")
513
+ throw new TypeError("Auto compact value is invalid");
514
+ this.#requireWorkflowSession().setAutoCompactionEnabled(value);
515
+ } },
516
+ autoResizeImages: { apply: value => {
517
+ if (typeof value !== "boolean")
518
+ throw new TypeError("Auto-resize images value is invalid");
519
+ settings.setImageAutoResize(value);
520
+ } },
521
+ blockImages: { apply: value => {
522
+ if (typeof value !== "boolean")
523
+ throw new TypeError("Block images value is invalid");
524
+ settings.setBlockImages(value);
525
+ } },
526
+ steeringMode: { apply: value => {
527
+ if (value !== "all" && value !== "one-at-a-time")
528
+ throw new TypeError("Steering mode is invalid");
529
+ this.#requireWorkflowSession().setSteeringMode(value);
530
+ } },
531
+ followUpMode: { apply: value => {
532
+ if (value !== "all" && value !== "one-at-a-time")
533
+ throw new TypeError("Follow-up mode is invalid");
534
+ this.#requireWorkflowSession().setFollowUpMode(value);
535
+ } },
536
+ transport: { apply: value => {
537
+ if (value !== "sse" && value !== "websocket" && value !== "websocket-cached" && value !== "auto")
538
+ throw new TypeError("Transport is invalid");
539
+ this.#requireWorkflowSession().agent.transport = value;
540
+ } },
541
+ httpIdleTimeoutMs: { apply: value => {
542
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
543
+ throw new TypeError("HTTP idle timeout is invalid");
544
+ // Provider streaming reads the manager per request; global fetch uses
545
+ // the matching owned dispatcher and zero maps to disabled semantics.
546
+ configureOwnedHttpDispatcher(value);
547
+ settings.setHttpIdleTimeoutMs(value);
548
+ } },
549
+ thinkingLevel: { apply: value => {
550
+ if (!isThinkingLevel(value))
551
+ throw new TypeError("Thinking level is invalid");
552
+ this.#requireWorkflowSession().setThinkingLevel(value);
553
+ this.#thinkingLevel = readThinkingLevel(this.#requireWorkflowSession().thinkingLevel);
554
+ this.#emitView();
555
+ } },
556
+ warnings: { apply: value => {
557
+ if (!isRecord(value) || Object.values(value).some(flag => typeof flag !== "boolean"))
558
+ throw new TypeError("Warnings setting is invalid");
559
+ settings.setWarnings(value);
560
+ } },
561
+ });
562
+ }
563
+ return this.#settingsIntegration;
564
+ }
565
+ bindSettingsOwner(owner, handlers) {
566
+ const settings = this.settingsPort();
567
+ if (settings === null)
568
+ return () => { };
569
+ return settings.bindOwner(owner, handlers);
445
570
  }
446
571
  pinnedModelSelectorContext() {
447
572
  const session = this.#requireWorkflowSession();
@@ -718,9 +843,9 @@ export class PiEngineAdapter {
718
843
  excludeFromContext,
719
844
  };
720
845
  }
721
- applyPinnedSettingValue(callback, value) {
846
+ async applyPinnedSettingValue(callback, value) {
722
847
  try {
723
- return this.#applyPinnedSetting(callback, value, true);
848
+ return await this.#applyPinnedSetting(callback, value, true);
724
849
  }
725
850
  catch (error) {
726
851
  return workflowResult("settings", "failed", error instanceof Error ? error.message : String(error));
@@ -821,6 +946,7 @@ export class PiEngineAdapter {
821
946
  this.#toolUpdateFlush = null;
822
947
  }
823
948
  this.#pendingToolUpdates.clear();
949
+ this.#transcriptImageAssets.clear();
824
950
  this.#extensionBound = false;
825
951
  this.#extensionUi = undefined;
826
952
  this.#extensionShutdown = undefined;
@@ -846,7 +972,7 @@ export class PiEngineAdapter {
846
972
  case "settings": {
847
973
  if (!selection)
848
974
  return workflowResult(request.command, "failed", "Settings requires the owned settings controller");
849
- return this.#applyPinnedSetting(selection);
975
+ return await this.#applyPinnedSetting(selection);
850
976
  }
851
977
  case "model": {
852
978
  const reference = selection ?? argument;
@@ -1236,7 +1362,7 @@ export class PiEngineAdapter {
1236
1362
  return [];
1237
1363
  return sessionInfoOptions(await SessionManager.list(cwd, typeof sessionDir === "string" ? sessionDir : undefined));
1238
1364
  }
1239
- #applyPinnedSetting(selection, selectedValue, hasSelectedValue = false) {
1365
+ async #applyPinnedSetting(selection, selectedValue, hasSelectedValue = false) {
1240
1366
  if (!PINNED_PI_SETTINGS_CALLBACKS.includes(selection)) {
1241
1367
  return workflowResult("settings", "failed", `Unknown setting callback: ${selection}`);
1242
1368
  }
@@ -1283,29 +1409,19 @@ export class PiEngineAdapter {
1283
1409
  selectedValue = currentValues[callback];
1284
1410
  hasSelectedValue = true;
1285
1411
  }
1286
- const settings = this.#runtime?.services.settingsManager;
1287
- if (!settings)
1412
+ const port = this.settingsPort();
1413
+ if (port === null)
1288
1414
  return workflowResult("settings", "failed", "Settings are unavailable");
1289
- const session = this.#requireWorkflowSession();
1290
- if (callback === "onThinkingLevelChange") {
1291
- if (!isThinkingLevel(selectedValue))
1292
- return workflowResult("settings", "failed", "Thinking level is invalid");
1293
- session.setThinkingLevel(selectedValue);
1294
- }
1295
- else {
1296
- const key = settingKeyForCallback(callback);
1297
- if (key === null)
1298
- return workflowResult("settings", "failed", `${settingLabel(callback)} is unavailable in this runtime`);
1299
- const value = agentJsonValue(selectedValue);
1300
- if (callback === "onAutoCompactChange") {
1301
- if (typeof value !== "boolean")
1302
- return workflowResult("settings", "failed", "Auto compact value is invalid");
1303
- session.setAutoCompactionEnabled?.(value);
1304
- }
1305
- new PiSettingsIntegration(settings).writeSettingNow(key, value);
1415
+ const key = settingKeyForCallback(callback);
1416
+ if (key === null)
1417
+ return workflowResult("settings", "failed", `${settingLabel(callback)} is unavailable in this runtime`);
1418
+ const result = await port.writeSetting(key, agentJsonValue(selectedValue));
1419
+ if (result.status === "failed" || result.status === "unavailable") {
1420
+ return workflowResult("settings", "failed", result.failure ?? result.limitationReason ?? `${settingLabel(callback)} is unavailable`);
1306
1421
  }
1307
1422
  this.#emitView();
1308
- return workflowResult("settings", "completed", `${settingLabel(callback)}: ${String(selectedValue)}`);
1423
+ const suffix = result.status === "deferred" ? ` (${result.application})` : "";
1424
+ return workflowResult("settings", "completed", `${settingLabel(callback)}: ${String(selectedValue)}${suffix}`);
1309
1425
  }
1310
1426
  async #perform(command) {
1311
1427
  const runtime = this.#runtime;
@@ -1384,6 +1500,7 @@ export class PiEngineAdapter {
1384
1500
  this.#activeModel = readModel(session.model);
1385
1501
  this.#reconcileActiveModelAvailability();
1386
1502
  this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
1503
+ this.#transcriptImageAssets.clear();
1387
1504
  this.#rebuildTranscript(session.messages, "finalized");
1388
1505
  this.#unsubscribe = session.subscribe(event => this.#handlePiEvent(event));
1389
1506
  if (this.#extensionUi !== undefined)
@@ -1443,6 +1560,11 @@ export class PiEngineAdapter {
1443
1560
  this.#transcriptIndex.clear();
1444
1561
  for (const [index, block] of blocks.entries())
1445
1562
  this.#transcriptIndex.set(block.id, index);
1563
+ const retainedAssets = new Set(blocks.flatMap(block => block.imageReferences?.map(reference => reference.assetId) ?? []));
1564
+ for (const assetId of this.#transcriptImageAssets.keys()) {
1565
+ if (!retainedAssets.has(assetId))
1566
+ this.#transcriptImageAssets.delete(assetId);
1567
+ }
1446
1568
  this.#transcriptSnapshot = undefined;
1447
1569
  }
1448
1570
  #transcriptBlock(id) {
@@ -1702,6 +1824,7 @@ export class PiEngineAdapter {
1702
1824
  revision: this.#nextBlockRevision(baseId),
1703
1825
  title: "User",
1704
1826
  text: textFromContent(message.content),
1827
+ imageReferences: this.#imageReferences(message.content, "user"),
1705
1828
  payload: {
1706
1829
  role: "user",
1707
1830
  imageCount: contentImageCount(message.content),
@@ -1779,6 +1902,7 @@ export class PiEngineAdapter {
1779
1902
  revision: this.#nextBlockRevision(blockId),
1780
1903
  title: stringValue(message.toolName) ?? existing?.title ?? "Tool result",
1781
1904
  text: textFromContent(message.content),
1905
+ imageReferences: this.#imageReferences(message.content, "tool-result"),
1782
1906
  payload: {
1783
1907
  ...(existingPayload ?? {}),
1784
1908
  role: "toolResult",
@@ -1834,6 +1958,24 @@ export class PiEngineAdapter {
1834
1958
  }
1835
1959
  return blocks;
1836
1960
  }
1961
+ #imageReferences(content, source) {
1962
+ if (!Array.isArray(content))
1963
+ return [];
1964
+ const references = [];
1965
+ for (const item of content) {
1966
+ if (references.length >= 16 || !isRecord(item) || item.type !== "image"
1967
+ || typeof item.data !== "string" || item.data.length === 0
1968
+ || typeof item.mimeType !== "string" || !/^image\/[a-z0-9.+-]+$/i.test(item.mimeType))
1969
+ continue;
1970
+ const byteLength = Buffer.from(item.data, "base64").byteLength;
1971
+ if (byteLength < 1 || byteLength > 20 * 1024 * 1024)
1972
+ continue;
1973
+ const assetId = `image-${createHash("sha256").update(item.mimeType).update("\0").update(item.data).digest("hex").slice(0, 24)}`;
1974
+ this.#transcriptImageAssets.set(assetId, { type: "image", data: item.data, mimeType: item.mimeType });
1975
+ references.push({ assetId, mimeType: item.mimeType, byteLength, source });
1976
+ }
1977
+ return references;
1978
+ }
1837
1979
  /**
1838
1980
  * A busy tool streams far more output chunks than a terminal frame can show, and every
1839
1981
  * chunk restates the whole accumulated output. Keeping only the newest chunk per tool
@@ -2056,7 +2198,12 @@ export async function createPiEngineAdapter(options = {}) {
2056
2198
  return adapter;
2057
2199
  }
2058
2200
  async function createDefaultPiRuntime(input) {
2059
- return createPiRuntimeIntegration({ cwd: input.cwd, agentDir: input.agentDir });
2201
+ return createPiRuntimeIntegration({
2202
+ cwd: input.cwd,
2203
+ agentDir: input.agentDir,
2204
+ ...(input.sessionPath === undefined ? {} : { sessionPath: input.sessionPath }),
2205
+ ...(input.projectTrustPrompt === undefined ? {} : { projectTrustPrompt: input.projectTrustPrompt }),
2206
+ });
2060
2207
  }
2061
2208
  async function checkDefaultPiPackageUpdates(cwd, agentDir, settingsManager) {
2062
2209
  const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
@@ -2282,7 +2429,7 @@ function settingKeyForCallback(callback) {
2282
2429
  onAutoCompactChange: "autoCompact", onShowImagesChange: "showImages", onImageWidthCellsChange: "imageWidthCells",
2283
2430
  onAutoResizeImagesChange: "autoResizeImages", onBlockImagesChange: "blockImages", onEnableSkillCommandsChange: "enableSkillCommands",
2284
2431
  onSteeringModeChange: "steeringMode", onFollowUpModeChange: "followUpMode", onTransportChange: "transport",
2285
- onHttpIdleTimeoutMsChange: "httpIdleTimeoutMs", onThemeChange: "theme", onThemePreview: "theme",
2432
+ onHttpIdleTimeoutMsChange: "httpIdleTimeoutMs", onThinkingLevelChange: "thinkingLevel", onThemeChange: "theme", onThemePreview: "theme",
2286
2433
  onHideThinkingBlockChange: "hideThinkingBlock", onMermaidRenderingModeChange: "mermaidRenderingMode",
2287
2434
  onShowCacheMissNoticesChange: "showCacheMissNotices", onCollapseChangelogChange: "collapseChangelog",
2288
2435
  onEnableInstallTelemetryChange: "enableInstallTelemetry", onQuietStartupChange: "quietStartup",
@@ -2483,7 +2630,8 @@ function sameBlockContent(left, right) {
2483
2630
  && left.status === right.status
2484
2631
  && left.title === right.title
2485
2632
  && left.text === right.text
2486
- && sameValue(left.payload, right.payload);
2633
+ && sameValue(left.payload, right.payload)
2634
+ && sameValue(left.imageReferences ?? [], right.imageReferences ?? []);
2487
2635
  }
2488
2636
  function sameValue(left, right) {
2489
2637
  if (left === right)
@@ -0,0 +1,4 @@
1
+ /** Apply the profile's HTTP timeout to npm undici and its fetch globals. */
2
+ export declare function configureOwnedHttpDispatcher(timeoutMs: number): void;
3
+ /** Test and diagnostics seam; reports configured semantics, not the max-int implementation detail. */
4
+ export declare function configuredOwnedHttpIdleTimeoutMs(): number | null;
@@ -0,0 +1,25 @@
1
+ // Mechanically adapted from Pi commit 914cf14
2
+ // packages/coding-agent/src/core/http-dispatcher.ts (MIT).
3
+ // Local modifications: expose only A1's owned timeout seam and retain no private Pi imports.
4
+ import { EnvHttpProxyAgent, install, setGlobalDispatcher } from "undici";
5
+ const MAX_DISABLED_TIMEOUT_MS = 2_147_483_647;
6
+ let configuredTimeoutMs = null;
7
+ /** Apply the profile's HTTP timeout to npm undici and its fetch globals. */
8
+ export function configureOwnedHttpDispatcher(timeoutMs) {
9
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0)
10
+ throw new TypeError("HTTP idle timeout is invalid");
11
+ const effectiveTimeoutMs = timeoutMs === 0 ? MAX_DISABLED_TIMEOUT_MS : timeoutMs;
12
+ const dispatcher = new EnvHttpProxyAgent({
13
+ allowH2: false,
14
+ bodyTimeout: effectiveTimeoutMs,
15
+ headersTimeout: effectiveTimeoutMs,
16
+ connect: { autoSelectFamilyAttemptTimeout: 2_000 },
17
+ });
18
+ setGlobalDispatcher(dispatcher);
19
+ install?.();
20
+ configuredTimeoutMs = timeoutMs;
21
+ }
22
+ /** Test and diagnostics seam; reports configured semantics, not the max-int implementation detail. */
23
+ export function configuredOwnedHttpIdleTimeoutMs() {
24
+ return configuredTimeoutMs;
25
+ }
@@ -4,6 +4,9 @@ export * from "./runtime-integration.js";
4
4
  export * from "./session-integration.js";
5
5
  export * from "./model-auth-integration.js";
6
6
  export * from "./settings-integration.js";
7
+ export * from "./settings-effects.js";
8
+ export * from "./project-trust-preflight.js";
9
+ export * from "./http-dispatcher.js";
7
10
  export * from "./package-integration.js";
8
11
  export * from "./resource-extension-integration.js";
9
12
  export * from "./workflow-controllers.js";
@@ -4,6 +4,9 @@ export * from "./runtime-integration.js";
4
4
  export * from "./session-integration.js";
5
5
  export * from "./model-auth-integration.js";
6
6
  export * from "./settings-integration.js";
7
+ export * from "./settings-effects.js";
8
+ export * from "./project-trust-preflight.js";
9
+ export * from "./http-dispatcher.js";
7
10
  export * from "./package-integration.js";
8
11
  export * from "./resource-extension-integration.js";
9
12
  export * from "./workflow-controllers.js";
@@ -0,0 +1,22 @@
1
+ export interface PiProjectTrustPreflightRequest {
2
+ readonly cwd: string;
3
+ readonly defaultDecision: "ask" | "always" | "never";
4
+ }
5
+ export type PiProjectTrustPreflightPrompt = (request: PiProjectTrustPreflightRequest) => Promise<boolean | null>;
6
+ export interface PiProjectTrustPreflightResult {
7
+ readonly trusted: boolean;
8
+ readonly source: "no-project-resources" | "saved" | "default" | "interactive" | "fail-closed";
9
+ readonly diagnostic: string | null;
10
+ }
11
+ export interface ResolvePiProjectTrustPreflightOptions {
12
+ readonly cwd: string;
13
+ readonly agentDir: string;
14
+ readonly prompt?: PiProjectTrustPreflightPrompt;
15
+ readonly hasProjectResources?: (cwd: string) => boolean;
16
+ }
17
+ /**
18
+ * Resolves trust from global state only. It never constructs a project-trusted
19
+ * settings manager or resource loader; callers may do that only after this
20
+ * result exists.
21
+ */
22
+ export declare function resolvePiProjectTrustPreflight(options: ResolvePiProjectTrustPreflightOptions): Promise<PiProjectTrustPreflightResult>;
@@ -0,0 +1,50 @@
1
+ import { hasTrustRequiringProjectResources, ProjectTrustStore, SettingsManager, } from "@earendil-works/pi-coding-agent";
2
+ /**
3
+ * Resolves trust from global state only. It never constructs a project-trusted
4
+ * settings manager or resource loader; callers may do that only after this
5
+ * result exists.
6
+ */
7
+ export async function resolvePiProjectTrustPreflight(options) {
8
+ const hasProjectResources = options.hasProjectResources ?? hasTrustRequiringProjectResources;
9
+ if (!hasProjectResources(options.cwd)) {
10
+ return { trusted: true, source: "no-project-resources", diagnostic: null };
11
+ }
12
+ const trustStore = new ProjectTrustStore(options.agentDir);
13
+ const saved = trustStore.get(options.cwd);
14
+ if (saved !== null)
15
+ return { trusted: saved, source: "saved", diagnostic: null };
16
+ // projectTrusted:false is the security boundary: this read may see global
17
+ // settings but cannot consume project settings or construct project resources.
18
+ const globalSettings = SettingsManager.create(options.cwd, options.agentDir, { projectTrusted: false });
19
+ const fallback = globalSettings.getDefaultProjectTrust();
20
+ if (fallback === "always")
21
+ return { trusted: true, source: "default", diagnostic: null };
22
+ if (fallback === "never")
23
+ return { trusted: false, source: "default", diagnostic: null };
24
+ if (options.prompt === undefined) {
25
+ return {
26
+ trusted: false,
27
+ source: "fail-closed",
28
+ diagnostic: `Project resources in ${options.cwd} were withheld because trust requires interaction`,
29
+ };
30
+ }
31
+ try {
32
+ const decision = await options.prompt({ cwd: options.cwd, defaultDecision: fallback });
33
+ if (decision === null) {
34
+ return {
35
+ trusted: false,
36
+ source: "fail-closed",
37
+ diagnostic: `Project resources in ${options.cwd} were withheld because trust selection was cancelled`,
38
+ };
39
+ }
40
+ trustStore.set(options.cwd, decision);
41
+ return { trusted: decision, source: "interactive", diagnostic: null };
42
+ }
43
+ catch (error) {
44
+ return {
45
+ trusted: false,
46
+ source: "fail-closed",
47
+ diagnostic: `Project resources in ${options.cwd} were withheld because trust resolution failed: ${error instanceof Error ? error.message : String(error)}`,
48
+ };
49
+ }
50
+ }
@@ -1,8 +1,17 @@
1
- import { type AgentSession, type AgentSessionRuntime, type AgentSessionServices, type ScopedModel } from "@earendil-works/pi-coding-agent";
1
+ import { createAgentSessionServices, SettingsManager, type AgentSession, type AgentSessionRuntime, type AgentSessionServices, type ScopedModel } from "@earendil-works/pi-coding-agent";
2
+ import { resolvePiProjectTrustPreflight, type PiProjectTrustPreflightPrompt } from "./project-trust-preflight.js";
2
3
  export interface PiRuntimeIntegrationOptions {
3
4
  readonly cwd: string;
4
5
  readonly agentDir: string;
5
6
  readonly sessionDir?: string;
7
+ readonly sessionPath?: string;
8
+ readonly projectTrustPrompt?: PiProjectTrustPreflightPrompt;
9
+ readonly preflightDependencies?: PiRuntimePreflightDependencies;
10
+ }
11
+ export interface PiRuntimePreflightDependencies {
12
+ readonly resolveTrust?: typeof resolvePiProjectTrustPreflight;
13
+ readonly createSettingsManager?: (cwd: string, agentDir: string, projectTrusted: boolean) => SettingsManager;
14
+ readonly createServices?: typeof createAgentSessionServices;
6
15
  }
7
16
  export type PiSessionReplacement = {
8
17
  readonly kind: "new";
@@ -26,6 +35,12 @@ interface ConfiguredModelScope {
26
35
  * saved default when it is in scope, otherwise the first scoped model.
27
36
  */
28
37
  export declare function resolveConfiguredModelScope(services: Pick<AgentSessionServices, "settingsManager" | "modelRuntime">): Promise<ConfiguredModelScope>;
38
+ export declare function createPiRuntimeServicesAfterTrust(options: Pick<PiRuntimeIntegrationOptions, "agentDir" | "projectTrustPrompt" | "preflightDependencies"> & {
39
+ readonly cwd: string;
40
+ }): Promise<{
41
+ readonly services: AgentSessionServices;
42
+ readonly trust: Awaited<ReturnType<typeof resolvePiProjectTrustPreflight>>;
43
+ }>;
29
44
  export declare function createPiRuntimeIntegration(options: PiRuntimeIntegrationOptions): Promise<AgentSessionRuntime>;
30
45
  export declare function bindPiRuntimeSession(runtime: AgentSessionRuntime, rebind: (session: AgentSession) => Promise<void>): () => void;
31
46
  export declare function replacePiRuntimeSession(runtime: AgentSessionRuntime, replacement: PiSessionReplacement): Promise<{
@@ -1,4 +1,5 @@
1
- import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, resolveModelScopeWithDiagnostics, SessionManager, } from "@earendil-works/pi-coding-agent";
1
+ import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, resolveModelScopeWithDiagnostics, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
2
+ import { resolvePiProjectTrustPreflight, } from "./project-trust-preflight.js";
2
3
  /**
3
4
  * Mirrors pinned Pi's CLI startup: resolve the `models` patterns from settings
4
5
  * into a scoped model list, keep the resolver's warnings (e.g. "No models match
@@ -29,10 +30,32 @@ export async function resolveConfiguredModelScope(services) {
29
30
  diagnostics: diagnostics.map(diagnostic => ({ type: diagnostic.type, message: diagnostic.message })),
30
31
  };
31
32
  }
33
+ export async function createPiRuntimeServicesAfterTrust(options) {
34
+ const resolveTrust = options.preflightDependencies?.resolveTrust ?? resolvePiProjectTrustPreflight;
35
+ const createSettingsManager = options.preflightDependencies?.createSettingsManager
36
+ ?? ((targetCwd, agentDir, projectTrusted) => SettingsManager.create(targetCwd, agentDir, { projectTrusted }));
37
+ const createServices = options.preflightDependencies?.createServices ?? createAgentSessionServices;
38
+ const trust = await resolveTrust({
39
+ cwd: options.cwd,
40
+ agentDir: options.agentDir,
41
+ ...(options.projectTrustPrompt === undefined ? {} : { prompt: options.projectTrustPrompt }),
42
+ });
43
+ const settingsManager = createSettingsManager(options.cwd, options.agentDir, trust.trusted);
44
+ const services = await createServices({ cwd: options.cwd, agentDir: options.agentDir, settingsManager });
45
+ return { services, trust };
46
+ }
32
47
  export async function createPiRuntimeIntegration(options) {
33
- const sessionManager = SessionManager.create(options.cwd, options.sessionDir ?? process.env.PI_CODING_AGENT_SESSION_DIR);
48
+ const sessionDir = options.sessionDir ?? process.env.PI_CODING_AGENT_SESSION_DIR;
49
+ const sessionManager = options.sessionPath === undefined
50
+ ? SessionManager.create(options.cwd, sessionDir)
51
+ : SessionManager.open(options.sessionPath, sessionDir, options.cwd);
34
52
  const createRuntime = async ({ cwd, sessionManager: targetSessionManager, sessionStartEvent, }) => {
35
- const services = await createAgentSessionServices({ cwd, agentDir: options.agentDir });
53
+ const { services, trust } = await createPiRuntimeServicesAfterTrust({
54
+ cwd,
55
+ agentDir: options.agentDir,
56
+ ...(options.projectTrustPrompt === undefined ? {} : { projectTrustPrompt: options.projectTrustPrompt }),
57
+ ...(options.preflightDependencies === undefined ? {} : { preflightDependencies: options.preflightDependencies }),
58
+ });
36
59
  const modelScope = await resolveConfiguredModelScope(services);
37
60
  const hasExistingSession = targetSessionManager.buildSessionContext().messages.length > 0;
38
61
  const created = await createAgentSessionFromServices({
@@ -43,7 +66,14 @@ export async function createPiRuntimeIntegration(options) {
43
66
  ...(modelScope.thinkingLevel && !hasExistingSession ? { thinkingLevel: modelScope.thinkingLevel } : {}),
44
67
  ...(modelScope.scopedModels.length > 0 ? { scopedModels: [...modelScope.scopedModels] } : {}),
45
68
  });
46
- return { ...created, services, diagnostics: [...modelScope.diagnostics] };
69
+ return {
70
+ ...created,
71
+ services,
72
+ diagnostics: [
73
+ ...(trust.diagnostic === null ? [] : [{ type: "warning", message: trust.diagnostic }]),
74
+ ...modelScope.diagnostics,
75
+ ],
76
+ };
47
77
  };
48
78
  return createAgentSessionRuntime(createRuntime, {
49
79
  cwd: options.cwd,