@timurproko/a1 0.2.7-dev.1276 → 0.2.7-dev.1286
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.
- package/README.md +5 -1
- package/dist/app/session-shell/session-shell-root.d.ts +4 -68
- package/dist/app/session-shell/session-shell.js +5 -11
- package/dist/app/session-shell/shell-host.js +2 -2
- package/dist/app/session-shell/shell-options.d.ts +65 -0
- package/dist/app/session-shell/shell-options.js +1 -0
- package/dist/app/session-shell/workflow-router.d.ts +3 -1
- package/dist/app/session-shell/workflow-router.js +12 -0
- package/dist/cli/dispatch.js +27 -1
- package/dist/cli/packages.js +18 -2
- package/dist/cli/session-context.js +6 -2
- package/dist/composition/owned-ui.js +20 -8
- package/dist/composition/session-naming.js +1 -1
- package/dist/composition/session-runtime-registry.d.ts +2 -0
- package/dist/composition/session-runtime-registry.js +49 -9
- package/dist/contracts/agent-engine/package-ports.d.ts +10 -14
- package/dist/contracts/agent-engine/package-ports.js +5 -9
- package/dist/contracts/owned-ui/session-workflows.d.ts +6 -0
- package/dist/contracts/presentation/index.d.ts +1 -0
- package/dist/contracts/presentation/progress-palette.generated.d.ts +1 -1
- package/dist/contracts/presentation/progress-palette.generated.js +1 -1
- package/dist/contracts/presentation/progress-spinner.d.ts +7 -0
- package/dist/contracts/presentation/progress-spinner.js +1 -0
- package/dist/features/launch/session-context.d.ts +8 -0
- package/dist/features/launch/session-context.js +19 -10
- package/dist/features/owned-ui/reference-screen-app.js +9 -5
- package/dist/features/owned-ui/session-fork-prompt.d.ts +2 -1
- package/dist/features/owned-ui/session-fork-prompt.js +11 -5
- package/dist/features/owned-ui/settings-app.d.ts +1 -1
- package/dist/features/owned-ui/settings-app.js +14 -7
- package/dist/features/owned-ui/settings-spinner-preview.d.ts +9 -0
- package/dist/features/owned-ui/settings-spinner-preview.js +29 -0
- package/dist/features/session-naming/context.d.ts +17 -0
- package/dist/features/session-naming/context.js +39 -0
- package/dist/features/session-naming/controller.d.ts +1 -5
- package/dist/features/session-naming/controller.js +7 -4
- package/dist/features/session-naming/generate.d.ts +2 -1
- package/dist/features/session-naming/generate.js +14 -10
- package/dist/features/session-naming/index.d.ts +1 -1
- package/dist/foundation/lifecycle/index.d.ts +3 -1
- package/dist/foundation/lifecycle/index.js +3 -1
- package/dist/foundation/lifecycle/session-claim-lock.d.ts +2 -2
- package/dist/foundation/lifecycle/session-claim-lock.js +36 -10
- package/dist/foundation/lifecycle/session-context-retry.d.ts +2 -0
- package/dist/foundation/lifecycle/session-context-retry.js +17 -0
- package/dist/foundation/lifecycle/session-repository-context.d.ts +7 -1
- package/dist/foundation/lifecycle/session-repository-context.js +105 -39
- package/dist/foundation/lifecycle/session-worktree-entry.d.ts +3 -0
- package/dist/foundation/lifecycle/session-worktree-entry.js +87 -0
- package/dist/foundation/release/automatic-update.js +2 -2
- package/dist/foundation/release/bootstrap.js +19 -3
- package/dist/foundation/release/index.d.ts +1 -1
- package/dist/foundation/release/index.js +1 -1
- package/dist/foundation/release/release-gc.js +41 -5
- package/dist/foundation/release/release-state.d.ts +48 -4
- package/dist/foundation/release/release-state.js +168 -31
- package/dist/foundation/release/release-use.d.ts +10 -0
- package/dist/foundation/release/release-use.js +86 -0
- package/dist/foundation/release/update-activation.d.ts +3 -1
- package/dist/foundation/release/update-activation.js +23 -6
- package/dist/foundation/release/update-recovery.js +2 -2
- package/dist/foundation/release/update.d.ts +3 -1
- package/dist/foundation/release/update.js +61 -22
- package/dist/foundation/release/warmup.js +8 -9
- package/dist/foundation/session-writer-lock/index.d.ts +2 -2
- package/dist/foundation/session-writer-lock/index.js +82 -12
- package/dist/foundation/startup/startup-descriptor.js +2 -2
- package/dist/foundation/terminal-cleanup/fatal-diagnostic.d.ts +2 -0
- package/dist/foundation/terminal-cleanup/fatal-diagnostic.js +59 -0
- package/dist/foundation/terminal-cleanup/fatal-exit.js +1 -29
- package/dist/integrations/pi/components/resources/builtin-themes.d.ts +2 -2
- package/dist/integrations/pi/components/resources/builtin-themes.js +2 -2
- package/dist/integrations/pi/components/share-operation-dialog.d.ts +2 -1
- package/dist/integrations/pi/components/share-operation-dialog.js +13 -5
- package/dist/integrations/pi/components/shell-footer-status.d.ts +2 -1
- package/dist/integrations/pi/components/shell-footer-status.js +15 -8
- package/dist/integrations/pi/components/shell-session-info-reference.js +11 -8
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +1 -1
- package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
- package/dist/integrations/pi/components/upstream/components/auth-url.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/auth-url.js +1 -1
- package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/login-dialog.js +1 -1
- package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
- package/dist/integrations/pi/components/upstream/components/oauth-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/oauth-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/session-footer.js +14 -8
- package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
- package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
- package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/status-indicator.js +1 -1
- package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/tool-execution.js +1 -1
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
- package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
- package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
- package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
- package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
- package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
- package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/theme/theme.js +1 -1
- package/dist/integrations/pi/engine/adapter.d.ts +3 -1
- package/dist/integrations/pi/engine/adapter.js +24 -6
- package/dist/integrations/pi/engine/changelog.js +5 -2
- package/dist/integrations/pi/engine/delivery-retention.d.ts +7 -0
- package/dist/integrations/pi/engine/delivery-retention.js +66 -0
- package/dist/integrations/pi/engine/event-delivery.d.ts +17 -1
- package/dist/integrations/pi/engine/event-delivery.js +25 -3
- package/dist/integrations/pi/engine/extension-ui-binding.d.ts +2 -1
- package/dist/integrations/pi/engine/extension-ui-binding.js +45 -0
- package/dist/integrations/pi/engine/host-ports.d.ts +2 -0
- package/dist/integrations/pi/engine/host.d.ts +3 -0
- package/dist/integrations/pi/engine/host.js +2 -0
- package/dist/integrations/pi/engine/package-integration.js +19 -4
- package/dist/integrations/pi/engine/pending-delivery.d.ts +4 -1
- package/dist/integrations/pi/engine/pending-delivery.js +51 -18
- package/dist/integrations/pi/engine/resources/changelog.json +4 -4
- package/dist/integrations/pi/engine/runtime-integration.d.ts +8 -0
- package/dist/integrations/pi/engine/runtime-integration.js +58 -9
- package/dist/integrations/pi/engine/session-cwd-recovery.d.ts +11 -0
- package/dist/integrations/pi/engine/session-cwd-recovery.js +18 -0
- package/dist/integrations/pi/engine/session-runtime.d.ts +3 -0
- package/dist/integrations/pi/engine/session-runtime.js +2 -0
- package/dist/integrations/pi/engine/session-selection.d.ts +3 -2
- package/dist/integrations/pi/engine/session-selection.js +25 -5
- package/dist/integrations/pi/engine/transcript-image-assets.d.ts +3 -0
- package/dist/integrations/pi/engine/transcript-image-assets.js +65 -14
- package/dist/integrations/pi/engine/view-state.d.ts +14 -0
- package/dist/integrations/pi/engine/view-state.js +17 -0
- package/dist/integrations/pi/engine/workflow-runner.d.ts +2 -0
- package/dist/integrations/pi/engine/workflow-runner.js +47 -26
- package/dist/integrations/pi/engine/worktree-tools.d.ts +4 -0
- package/dist/integrations/pi/engine/worktree-tools.js +26 -0
- package/dist/integrations/pi/session-presenters/components.d.ts +2 -1
- package/dist/integrations/pi/session-presenters/components.js +5 -3
- package/dist/integrations/pi/session-presenters/presenters.d.ts +3 -1
- package/dist/integrations/pi/session-presenters/presenters.js +1 -1
- package/dist/integrations/pi/startup-public.d.ts +1 -1
- package/dist/integrations/pi/startup-public.js +16 -20
- package/dist/integrations/pi/startup-public.manifest.json +29 -29
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
- package/dist/product-identity.json +1 -1
- package/dist/runtime-payload-inventory.json +5027 -5027
- package/dist/ui/components/index.d.ts +1 -1
- package/dist/ui/components/index.js +1 -1
- package/dist/ui/components/progress-status.d.ts +7 -0
- package/dist/ui/components/progress-status.js +21 -0
- package/dist/ui/components/value-menu.d.ts +1 -1
- package/dist/ui/components/value-menu.js +4 -2
- package/dist/ui/settings/declarations.d.ts +16 -4
- package/dist/ui/settings/declarations.js +13 -4
- package/docs/architecture/internal-naming.md +1 -1
- package/docs/architecture/project-structure.md +7 -11
- package/docs/features/session-exports.md +1 -1
- package/docs/features/session-naming.md +8 -6
- package/docs/features/worktrees.md +38 -0
- package/docs/local-worktree-cleanup.md +27 -4
- package/docs/openspec-archive-automation.md +30 -4
- package/docs/worktree-link-recovery.md +2 -2
- package/package.json +5 -5
|
@@ -136,10 +136,12 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
|
|
|
136
136
|
// its own to know. A tree that serves the contract activates itself with its own code;
|
|
137
137
|
// a tree from before the contract is activated here, with the layout it had then.
|
|
138
138
|
const contracts = await readActivationContracts(packageRoot, fileSystem.readFile);
|
|
139
|
-
if (contracts.includes(UPDATE_ACTIVATION_CONTRACT))
|
|
139
|
+
if (contracts.includes(UPDATE_ACTIVATION_CONTRACT)) {
|
|
140
140
|
await delegateActivation(request, callbacks);
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
return "delegated";
|
|
142
|
+
}
|
|
143
|
+
await activateInstalledRelease(request, callbacks);
|
|
144
|
+
return "in-process";
|
|
143
145
|
},
|
|
144
146
|
};
|
|
145
147
|
}
|
|
@@ -164,7 +166,7 @@ export function renderUpdateProgressBar(percent, accent = SHARED_PROGRESS_ACCENT
|
|
|
164
166
|
// percentage retain the update meter's neutral treatment.
|
|
165
167
|
const gray = "\u001b[38;2;128;128;128m";
|
|
166
168
|
const track = "\u001b[38;2;102;102;102m";
|
|
167
|
-
return `${accent}${"━".repeat(filled)}${track}${"
|
|
169
|
+
return `${accent}${"━".repeat(filled)}${track}${"━".repeat(PROGRESS_BAR_WIDTH - filled)}${gray} ${bounded}%\u001b[39m`;
|
|
168
170
|
}
|
|
169
171
|
function updateProgressAccent(environment) {
|
|
170
172
|
return earlyAccentPalette(resolveEarlyAccentColor(environment), earlyTerminalAppearance(environment), earlyTerminalColorMode(environment)).accent;
|
|
@@ -416,13 +418,22 @@ async function withUpdateExecutionLease(input, options, output, installer, opera
|
|
|
416
418
|
// back to the release that ran before it, so this request proceeds as an ordinary update of its
|
|
417
419
|
// own target instead of reporting a dead attempt as in progress.
|
|
418
420
|
const interrupted = lease.owner.targetVersion;
|
|
419
|
-
const rollback = await installer.rollback(lease.transaction.priorActiveReleaseId)
|
|
420
|
-
.catch(error => `rollback failed: ${errorMessage(error)}`);
|
|
421
421
|
const store = new UpdateTransactionStore(input.dataDir);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
422
|
+
if (lease.transaction.phase === "active-reference-committed") {
|
|
423
|
+
// Invariant: the interrupted update already committed its target; agents may run it, so it stays.
|
|
424
|
+
await store.finish("completed", `interrupted ${interrupted} update was already active; settled by ${input.targetVersion}`);
|
|
425
|
+
await store.clearCompleted().catch(() => undefined);
|
|
426
|
+
await reclaimDeadLease(input.dataDir, lease.owner);
|
|
427
|
+
output.stdout(`${PRODUCT_TEXT.commandName} settled the interrupted ${interrupted} update, which was already active\n`);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
const rollback = await installer.rollback(lease.transaction.priorActiveReleaseId)
|
|
431
|
+
.catch(error => `rollback failed: ${errorMessage(error)}`);
|
|
432
|
+
await store.finish("rolled-back", `interrupted ${interrupted} update settled by ${input.targetVersion}; ${rollback}`);
|
|
433
|
+
await store.clearCompleted().catch(() => undefined);
|
|
434
|
+
await reclaimDeadLease(input.dataDir, lease.owner);
|
|
435
|
+
output.stdout(`${PRODUCT_TEXT.commandName} rolled back the interrupted ${interrupted} update: ${rollback}\n`);
|
|
436
|
+
}
|
|
426
437
|
lease = await acquireUpdateExecutionLease(input);
|
|
427
438
|
}
|
|
428
439
|
if (lease.status !== "acquired") {
|
|
@@ -533,7 +544,9 @@ async function runUpdateTransaction(plan) {
|
|
|
533
544
|
const { packageRoot, npmCliRoot } = plan.installation;
|
|
534
545
|
const run = { transaction: await transactionStore.read() };
|
|
535
546
|
try {
|
|
536
|
-
|
|
547
|
+
// Rationale: a settled transaction, even one reported as failed after its commit, leaves nothing
|
|
548
|
+
// to install when its target is already active; reinstalling the same release would only churn.
|
|
549
|
+
if ((!run.transaction || run.transaction.status !== "active") && await lifecycle.targetIsActive(targetVersion)) {
|
|
537
550
|
await installer.maintenance();
|
|
538
551
|
await transactionStore.clearCompleted();
|
|
539
552
|
output.stdout(`${PRODUCT_TEXT.commandName} is up to date — no update needed.\n`);
|
|
@@ -610,7 +623,7 @@ async function replaceUpdatePackage(plan, run, protection, npmCli) {
|
|
|
610
623
|
}
|
|
611
624
|
/** Activate the installed package, commit maintenance, and only then report success. */
|
|
612
625
|
async function activateUpdate(plan, run) {
|
|
613
|
-
const {
|
|
626
|
+
const { progress, transactionStore, now, onPhaseTiming } = plan;
|
|
614
627
|
progress.set(70, MATERIALIZE_PROGRESS.from);
|
|
615
628
|
let activationPhaseStartedAt = now();
|
|
616
629
|
const recordActivationPhase = async (phase) => {
|
|
@@ -619,7 +632,7 @@ async function activateUpdate(plan, run) {
|
|
|
619
632
|
progress.set(ACTIVATION_PROGRESS[phase].at, ACTIVATION_PROGRESS[phase].creepTo);
|
|
620
633
|
activationPhaseStartedAt = now();
|
|
621
634
|
};
|
|
622
|
-
await plan.lifecycle.activateInstalled(plan.installation.packageRoot, plan.targetVersion, recordActivationPhase, ({ completed, total }) => progress.set(MATERIALIZE_PROGRESS.from
|
|
635
|
+
const activation = await plan.lifecycle.activateInstalled(plan.installation.packageRoot, plan.targetVersion, recordActivationPhase, ({ completed, total }) => progress.set(MATERIALIZE_PROGRESS.from
|
|
623
636
|
+ (MATERIALIZE_PROGRESS.to - MATERIALIZE_PROGRESS.from) * (total > 0 ? Math.min(1, completed / total) : 0)), state => {
|
|
624
637
|
if (state === "started") {
|
|
625
638
|
activationPhaseStartedAt = now();
|
|
@@ -632,19 +645,39 @@ async function activateUpdate(plan, run) {
|
|
|
632
645
|
}
|
|
633
646
|
});
|
|
634
647
|
const transactionStartedAt = now();
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
|
|
648
|
+
const warnings = [];
|
|
649
|
+
// Invariant: a delegated tree owns the control state it just wrote and has already scheduled its
|
|
650
|
+
// own maintenance; this updater may be older and must not interpret that state. An in-process
|
|
651
|
+
// activation ran this updater's own code, so the updater remains the owner of its maintenance.
|
|
652
|
+
if (activation !== "delegated") {
|
|
653
|
+
// Rationale: the activation is committed, so cleanup trouble is retried later, never a failure.
|
|
654
|
+
await plan.installer.maintenance().catch(error => { warnings.push(`release cleanup will retry: ${errorMessage(error)}`); });
|
|
655
|
+
}
|
|
656
|
+
await completeCommittedUpdate(plan, run.transaction, warnings);
|
|
657
|
+
onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* The active reference names the target, so the update has happened. Everything here is
|
|
662
|
+
* best-effort bookkeeping: it may warn, but it never fails the update and never rolls back.
|
|
663
|
+
*/
|
|
664
|
+
async function completeCommittedUpdate(plan, transaction, warnings) {
|
|
665
|
+
const { output, progress, transactionStore, paths } = plan;
|
|
666
|
+
if (transaction?.recovery?.capsulePath) {
|
|
667
|
+
await removeUpdateRecoveryCapsule(paths.dataDir, transaction.transactionId)
|
|
668
|
+
.catch(error => { warnings.push(`recovery evidence will be removed later: ${errorMessage(error)}`); });
|
|
669
|
+
}
|
|
641
670
|
// Concurrency: this is the launch cutover. No failure-prone update work follows success.
|
|
642
|
-
await transactionStore.finish("completed")
|
|
671
|
+
await transactionStore.finish("completed", warnings.length === 0 ? null : warnings.join("; ").slice(0, 2_000))
|
|
672
|
+
.catch(error => { warnings.push(`update journal could not be settled: ${errorMessage(error)}`); });
|
|
643
673
|
await Promise.all([transactionStore.clearCompleted(), invalidateReleaseCheckCache(paths.configDir)]).catch(() => undefined);
|
|
644
|
-
onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
|
|
645
674
|
progress.finish();
|
|
646
675
|
output.stdout(`${PRODUCT_TEXT.commandName} updated successfully to ${plan.targetVersion}\n`);
|
|
647
|
-
|
|
676
|
+
for (const warning of warnings)
|
|
677
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`warning: ${singleLine(warning)}`)}\n`);
|
|
678
|
+
}
|
|
679
|
+
function singleLine(text) {
|
|
680
|
+
return text.replace(/[\r\n]+/gu, " ").slice(0, 400);
|
|
648
681
|
}
|
|
649
682
|
async function settleUpdateFailure(plan, transaction, error) {
|
|
650
683
|
const { output, transactionStore } = plan;
|
|
@@ -654,6 +687,12 @@ async function settleUpdateFailure(plan, transaction, error) {
|
|
|
654
687
|
output.stderr(`${PRODUCT_TEXT.diagnostic(`update could not start safely: ${message}. Nothing was changed. Retry with npm x -y -- ${PRODUCT_PACKAGE}-install`)}\n`);
|
|
655
688
|
return 1;
|
|
656
689
|
}
|
|
690
|
+
if (transaction?.status === "active" && transaction.phase === "active-reference-committed") {
|
|
691
|
+
// Invariant: once the active reference names the target, running agents may already use it.
|
|
692
|
+
// Rolling back would move the reference under them, so a later failure only warns.
|
|
693
|
+
await completeCommittedUpdate(plan, transaction, [`post-activation step failed: ${message}`]);
|
|
694
|
+
return 0;
|
|
695
|
+
}
|
|
657
696
|
const rollback = await plan.installer.rollback(transaction?.priorActiveReleaseId ?? null)
|
|
658
697
|
.catch(rollbackError => `rollback failed: ${errorMessage(rollbackError)}`);
|
|
659
698
|
if (transaction)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnReleaseEntry } from "./release-use.js";
|
|
2
2
|
import { resolveReleaseEntryPoint } from "./release-store.js";
|
|
3
3
|
import { releaseEnvironment } from "./bootstrap.js";
|
|
4
4
|
import { launchContractTarget, launchEnvironmentKeys } from "../launch-context/index.js";
|
|
@@ -11,15 +11,13 @@ function warmupDiagnostics(text) {
|
|
|
11
11
|
/** Import the exact immutable startup graph in a terminal-free bounded child process. */
|
|
12
12
|
export async function warmMaterializedRelease(release, environment, timeoutMs = 30_000) {
|
|
13
13
|
const entry = await resolveReleaseEntryPoint(release, "bin/warmup.js");
|
|
14
|
+
const child = await spawnReleaseEntry(entry, [], {
|
|
15
|
+
env: { ...releaseEnvironment(environment, release), [launchEnvironmentKeys(launchContractTarget(release)).immutableWarmup]: "1" },
|
|
16
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
});
|
|
14
19
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
15
|
-
// Rationale:
|
|
16
|
-
// update reporting only an exit status. Its own diagnostics are read and bounded so the
|
|
17
|
-
// transaction journal records why the graph could not be imported.
|
|
18
|
-
const child = spawn(process.execPath, [entry], {
|
|
19
|
-
env: { ...releaseEnvironment(environment, release), [launchEnvironmentKeys(launchContractTarget(release)).immutableWarmup]: "1" },
|
|
20
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
21
|
-
windowsHide: true,
|
|
22
|
-
});
|
|
20
|
+
// Rationale: bounded diagnostics explain graph-import failures without attaching a terminal.
|
|
23
21
|
let diagnostics = "";
|
|
24
22
|
child.stderr?.setEncoding("utf8");
|
|
25
23
|
child.stderr?.on("data", chunk => { diagnostics = `${diagnostics}${chunk}`.slice(-WARMUP_DIAGNOSTIC_LIMIT); });
|
|
@@ -44,4 +42,5 @@ export async function warmMaterializedRelease(release, environment, timeoutMs =
|
|
|
44
42
|
}, timeoutMs);
|
|
45
43
|
timer.unref?.();
|
|
46
44
|
});
|
|
45
|
+
await child.releaseSettled;
|
|
47
46
|
}
|
|
@@ -20,8 +20,8 @@ export interface SessionWriterHandle {
|
|
|
20
20
|
release(): void;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* Heartbeats diagnose ownership loss, but never authorize takeover of a live writer.
|
|
24
|
+
* Dead owners are recovered by process absence; legacy or uncertain owners stay protected.
|
|
25
25
|
*/
|
|
26
26
|
export declare const SESSION_WRITER_STALE_MS = 10000;
|
|
27
27
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, realpathSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, realpathSync, statSync, readFileSync, writeFileSync, unlinkSync, rmdirSync, utimesSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import lockfile from "proper-lockfile";
|
|
6
6
|
/** Reports that a canonical session identity already has a live writer. */
|
|
7
7
|
export class SessionWriterConflict extends Error {
|
|
@@ -15,8 +15,8 @@ export class SessionWriterConflict extends Error {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Heartbeats diagnose ownership loss, but never authorize takeover of a live writer.
|
|
19
|
+
* Dead owners are recovered by process absence; legacy or uncertain owners stay protected.
|
|
20
20
|
*/
|
|
21
21
|
export const SESSION_WRITER_STALE_MS = 10_000;
|
|
22
22
|
let backendOverride;
|
|
@@ -35,15 +35,37 @@ export function lockfileBackend(root = join(tmpdir(), "a1-session-writer-locks")
|
|
|
35
35
|
};
|
|
36
36
|
const acquireAuthority = (path) => {
|
|
37
37
|
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
stale: staleMs,
|
|
43
|
-
|
|
44
|
-
// stalled one must not crash for it.
|
|
45
|
-
onCompromised: () => { },
|
|
38
|
+
// Compatibility: the nonempty owner record also prevents legacy heartbeat-only clients from reclaiming
|
|
39
|
+
// a stalled live writer: proper-lockfile removes directories non-recursively.
|
|
40
|
+
reclaimDeadWriter(path);
|
|
41
|
+
const release = lockfile.lockSync(path, {
|
|
42
|
+
realpath: false, lockfilePath: path, stale: staleMs,
|
|
43
|
+
onCompromised: () => { throw new Error("Session writer ownership was lost; execution has been stopped."); },
|
|
46
44
|
});
|
|
45
|
+
const owner = { pid: process.pid, token: randomUUID() };
|
|
46
|
+
const stamp = statSync(path).mtime;
|
|
47
|
+
try {
|
|
48
|
+
writeFileSync(join(path, "owner.json"), JSON.stringify(owner), { flag: "wx", mode: 0o600 });
|
|
49
|
+
// Concurrency: creating the owner record must not masquerade as a foreign heartbeat.
|
|
50
|
+
utimesSync(path, stamp, stamp);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
try {
|
|
54
|
+
unlinkSync(join(path, "owner.json"));
|
|
55
|
+
release();
|
|
56
|
+
}
|
|
57
|
+
catch { /* Security: retain uncertain ownership. */ }
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
return () => {
|
|
61
|
+
if (readWriterOwner(path)?.token !== owner.token)
|
|
62
|
+
throw new Error("Session writer ownership was lost");
|
|
63
|
+
// Concurrency: a legacy contender must not reclaim the short empty-directory release interval.
|
|
64
|
+
const now = new Date();
|
|
65
|
+
utimesSync(path, now, now);
|
|
66
|
+
unlinkSync(join(path, "owner.json"));
|
|
67
|
+
release();
|
|
68
|
+
};
|
|
47
69
|
};
|
|
48
70
|
return {
|
|
49
71
|
acquire(requested) {
|
|
@@ -92,6 +114,54 @@ export function lockfileBackend(root = join(tmpdir(), "a1-session-writer-locks")
|
|
|
92
114
|
},
|
|
93
115
|
};
|
|
94
116
|
}
|
|
117
|
+
function readWriterOwner(path) {
|
|
118
|
+
try {
|
|
119
|
+
const source = readFileSync(join(path, "owner.json"), "utf8");
|
|
120
|
+
if (source.length > 1024)
|
|
121
|
+
return null;
|
|
122
|
+
const value = JSON.parse(source);
|
|
123
|
+
return Number.isSafeInteger(value.pid) && value.pid > 0 && typeof value.token === "string"
|
|
124
|
+
&& /^[a-f0-9-]{36}$/.test(value.token) ? value : null;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function writerAbsent(pid) {
|
|
131
|
+
try {
|
|
132
|
+
process.kill(pid, 0);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return error instanceof Error && "code" in error && error.code === "ESRCH";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function reclaimDeadWriter(path) {
|
|
140
|
+
if (!existsSync(path))
|
|
141
|
+
return;
|
|
142
|
+
const owner = readWriterOwner(path);
|
|
143
|
+
// Security: age and malformed/legacy records cannot prove that a writer is absent.
|
|
144
|
+
if (owner === null || !writerAbsent(owner.pid))
|
|
145
|
+
throw new Error("Session writer is live or unverifiable");
|
|
146
|
+
const gate = join(path, ".reclaim");
|
|
147
|
+
mkdirSync(gate);
|
|
148
|
+
try {
|
|
149
|
+
if (readWriterOwner(path)?.token !== owner.token || !writerAbsent(owner.pid))
|
|
150
|
+
throw new Error("Session writer changed during recovery");
|
|
151
|
+
const now = new Date();
|
|
152
|
+
utimesSync(path, now, now);
|
|
153
|
+
unlinkSync(join(path, "owner.json"));
|
|
154
|
+
rmdirSync(gate);
|
|
155
|
+
rmdirSync(path);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
try {
|
|
159
|
+
rmdirSync(gate);
|
|
160
|
+
}
|
|
161
|
+
catch { /* Security: a partial recovery remains conservatively blocked. */ }
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
95
165
|
function backend() {
|
|
96
166
|
return backendOverride ?? (cachedBackend ??= lockfileBackend());
|
|
97
167
|
}
|
|
@@ -10,7 +10,7 @@ export const STARTUP_DESCRIPTOR = Object.freeze({
|
|
|
10
10
|
],
|
|
11
11
|
"generatedArtifact": {
|
|
12
12
|
"path": "dist/integrations/pi/startup-public.js",
|
|
13
|
-
"sha256": "
|
|
13
|
+
"sha256": "c1fca0ab0fe9d078a80faac1dad7d9f719cfc553645e38c2244a9c9ad381c205"
|
|
14
14
|
},
|
|
15
15
|
"dependencyLayerIdentity": {
|
|
16
16
|
"source": "A1_RELEASE_LAYERS",
|
|
@@ -19,7 +19,7 @@ export const STARTUP_DESCRIPTOR = Object.freeze({
|
|
|
19
19
|
"compileCacheNamespace": {
|
|
20
20
|
"strategy": "node-abi-version-and-ordered-layer-sha256-v1"
|
|
21
21
|
},
|
|
22
|
-
"identity": "
|
|
22
|
+
"identity": "3a54e70a22a20fa71dc2916cff394d4b06a3359677b3423002ff5b2dfbdccd70"
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
export function loadDeclaredStartupGraph() {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/** Loaded only on a failure; persistence must never impede terminal restoration. */
|
|
5
|
+
export async function writeFatalDiagnostic(directory, origin, error, releaseId) {
|
|
6
|
+
try {
|
|
7
|
+
// Security: only known classifications and code locations are retained, never private messages or objects.
|
|
8
|
+
const cleanupFailed = error instanceof AggregateError && error.cause instanceof Error;
|
|
9
|
+
const primary = cleanupFailed ? error.cause : error;
|
|
10
|
+
const classification = primary instanceof TypeError ? "TypeError" : primary instanceof RangeError ? "RangeError" : "Error";
|
|
11
|
+
const stack = primary instanceof Error && typeof primary.stack === "string" ? primary.stack.slice(0, 32768) : "";
|
|
12
|
+
const locations = stack.split("\n").slice(1).flatMap(line => {
|
|
13
|
+
const match = /\/(?:dist|src)\/([a-zA-Z0-9_./-]+\.[cm]?[jt]s):(\d+):(\d+)\)?$/.exec(line.replaceAll("\\", "/"));
|
|
14
|
+
return match ? [{ file: match[1].slice(0, 160), line: Number(match[2]), column: Number(match[3]) }] : [];
|
|
15
|
+
}).slice(0, 16);
|
|
16
|
+
const delivery = deliveryFailure(primary);
|
|
17
|
+
const record = {
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
origin: ["uncaughtException", "unhandledRejection", "entry", "delivery"].includes(origin) ? origin : "entry",
|
|
20
|
+
...(delivery === null ? {} : { deliveryFailure: delivery }),
|
|
21
|
+
classification, cleanupFailed, locations, node: process.version, platform: process.platform,
|
|
22
|
+
releaseId: releaseId && /^[a-zA-Z0-9._-]{1,128}$/.test(releaseId) ? releaseId : "unknown",
|
|
23
|
+
};
|
|
24
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
25
|
+
const name = `${origin === "delivery" ? "delivery" : "fatal"}-${Date.now()}-${randomUUID()}.json`;
|
|
26
|
+
const path = join(directory, name);
|
|
27
|
+
const text = JSON.stringify(record, null, 2);
|
|
28
|
+
if (Buffer.byteLength(text) > 16 * 1024)
|
|
29
|
+
return null;
|
|
30
|
+
await writeFile(path, text, { flag: "wx", mode: 0o600 });
|
|
31
|
+
const names = (await readdir(directory)).filter(file => /^(?:fatal|delivery)-\d+-[a-f0-9-]+\.json$/.test(file))
|
|
32
|
+
.sort((a, b) => Number(b.split("-")[1]) - Number(a.split("-")[1]));
|
|
33
|
+
await Promise.all(names.slice(10).map(file => unlink(join(directory, file)).catch(() => undefined)));
|
|
34
|
+
return path;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function deliveryFailure(error) {
|
|
41
|
+
if (!(error instanceof Error) || !("deliveryFailure" in error))
|
|
42
|
+
return null;
|
|
43
|
+
const value = error.deliveryFailure;
|
|
44
|
+
if (!value || typeof value !== "object")
|
|
45
|
+
return null;
|
|
46
|
+
const record = value;
|
|
47
|
+
if (!["event-count", "retained-bytes", "listener"].includes(String(record.reason))
|
|
48
|
+
|| !["snapshot", "session-view", "transcript-block", "session-lifecycle", "status", "editor-state", "terminal-surface",
|
|
49
|
+
"agent-run-started", "agent-run-settled", "assistant-message-completed", "command-outcome", "diagnostic"].includes(String(record.event)))
|
|
50
|
+
return null;
|
|
51
|
+
const result = { reason: String(record.reason), event: String(record.event) };
|
|
52
|
+
for (const key of ["generation", "pending", "bytes"]) {
|
|
53
|
+
const number = record[key];
|
|
54
|
+
if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0)
|
|
55
|
+
return null;
|
|
56
|
+
result[key] = number;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
@@ -1,36 +1,8 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
|
|
3
1
|
import { writeSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
2
|
import { boundedCleanup, restoreProcessTerminal } from "./terminal-reset.js";
|
|
6
3
|
export async function writeFatalDiagnostic(directory, origin, error, releaseId) {
|
|
7
4
|
try {
|
|
8
|
-
|
|
9
|
-
// function names, arbitrary paths, and request objects are never persisted.
|
|
10
|
-
const cleanupFailed = error instanceof AggregateError && error.cause instanceof Error;
|
|
11
|
-
const primary = cleanupFailed ? error.cause : error;
|
|
12
|
-
const classification = primary instanceof TypeError ? "TypeError" : primary instanceof RangeError ? "RangeError" : "Error";
|
|
13
|
-
const stack = primary instanceof Error && typeof primary.stack === "string" ? primary.stack.slice(0, 32768) : "";
|
|
14
|
-
const locations = stack.split("\n").slice(1).flatMap(line => {
|
|
15
|
-
const match = /\/(?:dist|src)\/([a-zA-Z0-9_./-]+\.[cm]?[jt]s):(\d+):(\d+)\)?$/.exec(line.replaceAll("\\", "/"));
|
|
16
|
-
return match ? [{ file: match[1].slice(0, 160), line: Number(match[2]), column: Number(match[3]) }] : [];
|
|
17
|
-
}).slice(0, 16);
|
|
18
|
-
const record = {
|
|
19
|
-
timestamp: new Date().toISOString(),
|
|
20
|
-
origin: ["uncaughtException", "unhandledRejection", "entry"].includes(origin) ? origin : "entry",
|
|
21
|
-
classification, cleanupFailed, locations, node: process.version, platform: process.platform,
|
|
22
|
-
releaseId: releaseId && /^[a-zA-Z0-9._-]{1,128}$/.test(releaseId) ? releaseId : "unknown",
|
|
23
|
-
};
|
|
24
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
25
|
-
const name = `fatal-${Date.now()}-${randomUUID()}.json`;
|
|
26
|
-
const path = join(directory, name);
|
|
27
|
-
const text = JSON.stringify(record, null, 2);
|
|
28
|
-
if (Buffer.byteLength(text) > 16 * 1024)
|
|
29
|
-
return null;
|
|
30
|
-
await writeFile(path, text, { flag: "wx", mode: 0o600 });
|
|
31
|
-
const names = (await readdir(directory)).filter(file => /^fatal-\d+-[a-f0-9-]+\.json$/.test(file)).sort().reverse();
|
|
32
|
-
await Promise.all(names.slice(10).map(file => unlink(join(directory, file)).catch(() => undefined)));
|
|
33
|
-
return path;
|
|
5
|
+
return await (await import("./fatal-diagnostic.js")).writeFatalDiagnostic(directory, origin, error, releaseId);
|
|
34
6
|
}
|
|
35
7
|
catch {
|
|
36
8
|
return null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Attributed owned theme resources adapted from @earendil-works/pi-coding-agent 1.5.
|
|
3
|
-
* commit
|
|
2
|
+
* Attributed owned theme resources adapted from @earendil-works/pi-coding-agent 1.5.2 (MIT),
|
|
3
|
+
* commit b4afdddf44e059c67ccf5097e5317d913521b470.
|
|
4
4
|
* Upstream dark.json SHA-256: e1f96982f8cb7d2a48eca7db03c9cb282cb1e14a243680cd855638ecd6a251b6
|
|
5
5
|
* Upstream light.json SHA-256: 2dee6a4be7f765e7fd7728166b7ebf8c29e4ce313f8ec6b38ae50699e3abc2f3
|
|
6
6
|
* Modifications: removed upstream schema/export metadata; runtime color data is unchanged.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Attributed owned theme resources adapted from @earendil-works/pi-coding-agent 1.5.
|
|
3
|
-
* commit
|
|
2
|
+
* Attributed owned theme resources adapted from @earendil-works/pi-coding-agent 1.5.2 (MIT),
|
|
3
|
+
* commit b4afdddf44e059c67ccf5097e5317d913521b470.
|
|
4
4
|
* Upstream dark.json SHA-256: e1f96982f8cb7d2a48eca7db03c9cb282cb1e14a243680cd855638ecd6a251b6
|
|
5
5
|
* Upstream light.json SHA-256: 2dee6a4be7f765e7fd7728166b7ebf8c29e4ce313f8ec6b38ae50699e3abc2f3
|
|
6
6
|
* Modifications: removed upstream schema/export metadata; runtime color data is unchanged.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { ProgressSpinnerSource } from "../../../contracts/presentation/index.js";
|
|
1
2
|
import { type PiShellComponentPort, type PiShellEditorOptions } from "./shell-shared-facade.js";
|
|
2
3
|
export interface PiShellOperationLoaderPort extends PiShellComponentPort {
|
|
3
4
|
readonly signal: AbortSignal;
|
|
4
5
|
/** Relabels the running operation; the pinned comparison loader keeps its first label. */
|
|
5
6
|
readonly setMessage?: (message: string) => void;
|
|
6
7
|
}
|
|
7
|
-
export declare function createPiShellShareOperationDialog(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">, message: string): PiShellOperationLoaderPort;
|
|
8
|
+
export declare function createPiShellShareOperationDialog(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">, message: string, spinner?: ProgressSpinnerSource): PiShellOperationLoaderPort;
|
|
8
9
|
export declare function createPiShellOperationLoader(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">, message: string): PiShellOperationLoaderPort;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CancellableLoader, Spacer, Text, } from "@earendil-works/pi-tui";
|
|
2
2
|
import { BorderedLoader, DynamicBorder } from "../startup-public.js";
|
|
3
|
+
import { progressSpinnerFrame } from "../../../ui/components/index.js";
|
|
3
4
|
import { adoptPiModalFrame, PiModalFrame } from "./modal-frame.js";
|
|
4
5
|
import { DIALOG_CLOSE_SHORTCUT_HINT, paintPiBorder, piTheme, renderPiModalShortcutHints } from "./theme.js";
|
|
5
6
|
import { componentPort, createTuiFacade, ensureTheme, } from "./shell-shared-facade.js";
|
|
@@ -7,26 +8,33 @@ import { componentPort, createTuiFacade, ensureTheme, } from "./shell-shared-fac
|
|
|
7
8
|
class ShareOperationDialog {
|
|
8
9
|
#loader;
|
|
9
10
|
#frame;
|
|
10
|
-
|
|
11
|
+
#releaseSpinner;
|
|
12
|
+
constructor(runtime, message, spinner) {
|
|
11
13
|
const theme = piTheme();
|
|
12
|
-
|
|
14
|
+
let phase = -1;
|
|
15
|
+
this.#loader = new CancellableLoader(createTuiFacade(runtime), currentFrame => {
|
|
16
|
+
phase += 1;
|
|
17
|
+
const glyph = progressSpinnerFrame(spinner?.snapshot() ?? "current", phase, currentFrame);
|
|
18
|
+
return glyph === "" ? "" : piTheme().fg("accent", glyph);
|
|
19
|
+
}, text => piTheme().fg("muted", text), message);
|
|
13
20
|
this.#frame = new PiModalFrame(new DynamicBorder(paintPiBorder), [
|
|
14
21
|
new Text(theme.fg("accent", theme.bold("Share")), 0, 0),
|
|
15
22
|
this.#loader,
|
|
16
23
|
new Spacer(1),
|
|
17
24
|
new Text(renderPiModalShortcutHints([DIALOG_CLOSE_SHORTCUT_HINT]), 0, 0),
|
|
18
25
|
], new DynamicBorder(paintPiBorder));
|
|
26
|
+
this.#releaseSpinner = spinner?.onChange(() => this.#loader.invalidate());
|
|
19
27
|
}
|
|
20
28
|
get signal() { return this.#loader.signal; }
|
|
21
29
|
setMessage(message) { this.#loader.setMessage(message); }
|
|
22
30
|
invalidate() { this.#frame.invalidate(); }
|
|
23
31
|
render(width) { return this.#frame.render(width); }
|
|
24
32
|
handleInput(data) { this.#loader.handleInput(data); }
|
|
25
|
-
dispose() { this.#loader.dispose(); }
|
|
33
|
+
dispose() { this.#releaseSpinner?.(); this.#loader.dispose(); }
|
|
26
34
|
}
|
|
27
|
-
export function createPiShellShareOperationDialog(runtime, message) {
|
|
35
|
+
export function createPiShellShareOperationDialog(runtime, message, spinner) {
|
|
28
36
|
ensureTheme();
|
|
29
|
-
const dialog = new ShareOperationDialog(runtime, message);
|
|
37
|
+
const dialog = new ShareOperationDialog(runtime, message, spinner);
|
|
30
38
|
return { ...componentPort(dialog), signal: dialog.signal, setMessage: message => dialog.setMessage(message) };
|
|
31
39
|
}
|
|
32
40
|
export function createPiShellOperationLoader(runtime, message) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { OwnedUiShellKeybindings } from "../../../contracts/owned-ui/index.js";
|
|
2
|
+
import type { ProgressSpinnerSource } from "../../../contracts/presentation/index.js";
|
|
2
3
|
import type { OwnedUiSessionViewModel } from "../../../contracts/owned-ui/index.js";
|
|
3
4
|
import { type PiShellViewComponentPort, type PiShellStatusPort, type PiShellProgressStatusPresentation, type PiShellQueuedInputPort, type PiShellHeaderPort, type PiShellResourceEntry, type PiShellLoadedResourcesPort, type PiShellHeaderOptions, type PiShellEditorOptions } from "./shell-shared-facade.js";
|
|
4
5
|
export declare function createPiShellHeader(options?: PiShellHeaderOptions): PiShellHeaderPort;
|
|
5
6
|
export declare function createPiShellLoadedResources(resources: readonly PiShellResourceEntry[], initialExpanded?: boolean): PiShellLoadedResourcesPort;
|
|
6
|
-
export declare function createPiShellStatus(view: OwnedUiSessionViewModel, progressStatus: PiShellProgressStatusPresentation, runtime?: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender"
|
|
7
|
+
export declare function createPiShellStatus(view: OwnedUiSessionViewModel, progressStatus: PiShellProgressStatusPresentation, runtime?: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">, spinner?: ProgressSpinnerSource): PiShellStatusPort;
|
|
7
8
|
export declare function createPiShellFooter(view: OwnedUiSessionViewModel, cwd: string, profile?: "pi" | "a1", showLevel?: () => boolean): PiShellViewComponentPort;
|
|
8
9
|
export declare function createPiQueuedInputStatus(submissions: readonly string[], presentation?: "pinned" | "custom-viewport", getKeybindings?: () => OwnedUiShellKeybindings): PiShellQueuedInputPort;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { progressSpinnerFrame } from "../../../ui/components/progress-status.js";
|
|
1
2
|
import { piKeybindings } from "./shell-shared-facade.js";
|
|
2
3
|
import { rawKeyHint, VERSION } from "../startup-public.js";
|
|
3
4
|
import { Spacer, Text, } from "@earendil-works/pi-tui";
|
|
@@ -74,7 +75,7 @@ export function createPiShellLoadedResources(resources, initialExpanded = false)
|
|
|
74
75
|
invalidate() { },
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
|
-
export function createPiShellStatus(view, progressStatus, runtime) {
|
|
78
|
+
export function createPiShellStatus(view, progressStatus, runtime, spinner) {
|
|
78
79
|
ensureTheme();
|
|
79
80
|
const statusUi = createTuiFacade(runtime ?? { getColumns: () => 80, getRows: () => 24, requestRender() { } });
|
|
80
81
|
let workingOverride;
|
|
@@ -83,7 +84,7 @@ export function createPiShellStatus(view, progressStatus, runtime) {
|
|
|
83
84
|
let progressPresentation = "pinned";
|
|
84
85
|
let placement = statusPlacement(view, workingOverride, workingOverrideActive);
|
|
85
86
|
const liveStatusText = () => progressStatus.text(liveWorkingText(view, workingOverride, progressPresentation), progressPresentation);
|
|
86
|
-
let component = statusComponent(view, statusUi, outputPad, liveStatusText, placement, progressPresentation, progressStatus);
|
|
87
|
+
let component = statusComponent(view, statusUi, outputPad, liveStatusText, placement, progressPresentation, progressStatus, spinner);
|
|
87
88
|
let signature = statusSignature(view, workingOverride, outputPad, placement, progressPresentation);
|
|
88
89
|
const rebuild = () => {
|
|
89
90
|
const nextPlacement = statusPlacement(view, workingOverride, workingOverrideActive);
|
|
@@ -105,8 +106,12 @@ export function createPiShellStatus(view, progressStatus, runtime) {
|
|
|
105
106
|
component.dispose();
|
|
106
107
|
placement = nextPlacement;
|
|
107
108
|
signature = nextSignature;
|
|
108
|
-
component = statusComponent(view, statusUi, outputPad, liveStatusText, placement, progressPresentation, progressStatus);
|
|
109
|
+
component = statusComponent(view, statusUi, outputPad, liveStatusText, placement, progressPresentation, progressStatus, spinner);
|
|
109
110
|
};
|
|
111
|
+
const releaseSpinner = spinner?.onChange(() => {
|
|
112
|
+
if (progressPresentation === "custom-viewport")
|
|
113
|
+
component?.invalidate();
|
|
114
|
+
});
|
|
110
115
|
return {
|
|
111
116
|
render: width => appendInstalledUpdateStatus(component?.render(width) ?? [], width, view, progressPresentation),
|
|
112
117
|
renderDock: width => placement === "dock" ? component?.render(width) ?? [] : [],
|
|
@@ -131,6 +136,7 @@ export function createPiShellStatus(view, progressStatus, runtime) {
|
|
|
131
136
|
rebuild();
|
|
132
137
|
},
|
|
133
138
|
dispose() {
|
|
139
|
+
releaseSpinner?.();
|
|
134
140
|
if (component !== undefined && "dispose" in component && typeof component.dispose === "function")
|
|
135
141
|
component.dispose();
|
|
136
142
|
component = undefined;
|
|
@@ -201,21 +207,22 @@ function liveWorkingText(view, workingOverride, progressPresentation) {
|
|
|
201
207
|
: message;
|
|
202
208
|
}
|
|
203
209
|
class OwnedWorkingStatusIndicator extends StatusIndicator {
|
|
204
|
-
constructor(ui, message, frame) {
|
|
210
|
+
constructor(ui, message, frame, spinner) {
|
|
205
211
|
let phase = -1;
|
|
206
|
-
super("working", ui,
|
|
212
|
+
super("working", ui, defaultFrame => {
|
|
207
213
|
phase += 1;
|
|
208
|
-
|
|
214
|
+
const glyph = progressSpinnerFrame(spinner?.snapshot() ?? "current", phase, defaultFrame);
|
|
215
|
+
return glyph === "" ? "" : piTheme().fg("accent", glyph);
|
|
209
216
|
}, text => frame(text, phase, {
|
|
210
217
|
muted: value => piTheme().fg("muted", value),
|
|
211
218
|
highlight: value => piTheme().fg("text", value),
|
|
212
219
|
}), message);
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
|
-
function statusComponent(view, ui, outputPad, liveStatusText, placement, progressPresentation, progressStatus) {
|
|
222
|
+
function statusComponent(view, ui, outputPad, liveStatusText, placement, progressPresentation, progressStatus, spinner) {
|
|
216
223
|
if (placement === "live") {
|
|
217
224
|
return progressPresentation === "custom-viewport"
|
|
218
|
-
? new OwnedWorkingStatusIndicator(ui, liveStatusText(), progressStatus.frame)
|
|
225
|
+
? new OwnedWorkingStatusIndicator(ui, liveStatusText(), progressStatus.frame, spinner)
|
|
219
226
|
: new WorkingStatusIndicator(ui, liveStatusText());
|
|
220
227
|
}
|
|
221
228
|
if (placement === "dock") {
|