@timurproko/a1 0.1.1-dev.3 → 0.1.1-dev.5
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/bin/a1-ui.js +2 -2
- package/dist/src/composition/index.d.ts +0 -18
- package/dist/src/composition/index.js +0 -36
- package/dist/src/features/workspace/index.d.ts +0 -1
- package/dist/src/features/workspace/index.js +0 -1
- package/dist/src/features/workspace/router.d.ts +0 -5
- package/dist/src/features/workspace/router.js +0 -26
- package/dist/src/foundation/release/bootstrap.d.ts +2 -2
- package/dist/src/foundation/release/bootstrap.js +6 -6
- package/dist/src/foundation/release/release-store.d.ts +15 -1
- package/dist/src/foundation/release/release-store.js +53 -28
- package/dist/src/foundation/release/release.d.ts +15 -0
- package/dist/src/foundation/release/release.js +62 -35
- package/dist/src/foundation/release/update.d.ts +17 -1
- package/dist/src/foundation/release/update.js +51 -12
- package/docs/ci-release-runbook.md +63 -0
- package/package.json +10 -6
- package/dist/src/composition/structured-workspace-application.d.ts +0 -38
- package/dist/src/composition/structured-workspace-application.js +0 -261
- package/dist/src/features/workspace/structured-tabs.d.ts +0 -83
- package/dist/src/features/workspace/structured-tabs.js +0 -453
|
@@ -106,9 +106,7 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
|
|
|
106
106
|
}
|
|
107
107
|
},
|
|
108
108
|
async activateInstalled(packageRoot, targetVersion, phase) {
|
|
109
|
-
const candidate = await materializeRelease(packageRoot, paths.dataDir
|
|
110
|
-
onProgress: progress => output.stdout(`${PRODUCT_TEXT.diagnostic(`installing ${progress.fileCount} files.`)}\n`),
|
|
111
|
-
});
|
|
109
|
+
const candidate = await materializeRelease(packageRoot, paths.dataDir);
|
|
112
110
|
if (candidate.packageVersion !== targetVersion)
|
|
113
111
|
throw new Error(`installed ${PRODUCT_TEXT.displayName} version ${candidate.packageVersion} does not match target ${targetVersion}`);
|
|
114
112
|
await stateStore.recordCandidate(candidate);
|
|
@@ -129,9 +127,19 @@ export async function runSelfUpdate(options) {
|
|
|
129
127
|
const runner = options.runner ?? createNpmProcessRunner();
|
|
130
128
|
const channel = options.channel ?? "stable";
|
|
131
129
|
const distTag = UPDATE_DIST_TAGS[channel];
|
|
130
|
+
const now = options.now ?? (() => performance.now());
|
|
131
|
+
const measure = async (phase, operation) => {
|
|
132
|
+
const startedAt = now();
|
|
133
|
+
try {
|
|
134
|
+
return await operation();
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - startedAt) });
|
|
138
|
+
}
|
|
139
|
+
};
|
|
132
140
|
let runningVersion;
|
|
133
141
|
try {
|
|
134
|
-
const packageJson = JSON.parse(await fileSystem.readFile(resolve(options.packageRoot, "package.json")));
|
|
142
|
+
const packageJson = JSON.parse(await measure("package-version", async () => await fileSystem.readFile(resolve(options.packageRoot, "package.json"))));
|
|
135
143
|
const parsedVersion = typeof packageJson.version === "string" ? validSemver(packageJson.version) : null;
|
|
136
144
|
if (parsedVersion === null)
|
|
137
145
|
throw new Error("package.json does not contain a valid semantic version");
|
|
@@ -141,7 +149,7 @@ export async function runSelfUpdate(options) {
|
|
|
141
149
|
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its running package version: ${errorMessage(error)}`)}\n`);
|
|
142
150
|
return 1;
|
|
143
151
|
}
|
|
144
|
-
const targetLookup = await runNpm(runner, ["view", `${PRODUCT_PACKAGE}@${distTag}`, "version"], true, output, `query the npm ${distTag} channel`);
|
|
152
|
+
const targetLookup = await measure("target-resolution", async () => await runNpm(runner, ["view", `${PRODUCT_PACKAGE}@${distTag}`, "version"], true, output, `query the npm ${distTag} channel`));
|
|
145
153
|
if (targetLookup.result === null)
|
|
146
154
|
return targetLookup.exitCode;
|
|
147
155
|
const targetVersion = validSemver(targetLookup.result.stdout.trim());
|
|
@@ -150,7 +158,7 @@ export async function runSelfUpdate(options) {
|
|
|
150
158
|
return 1;
|
|
151
159
|
}
|
|
152
160
|
output.stdout(`${PRODUCT_TEXT.diagnostic(`update (${channel}): ${runningVersion} → ${targetVersion}.`)}\n`);
|
|
153
|
-
const rootLookup = await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root");
|
|
161
|
+
const rootLookup = await measure("global-root", async () => await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root"));
|
|
154
162
|
if (rootLookup.result === null)
|
|
155
163
|
return rootLookup.exitCode;
|
|
156
164
|
if (rootLookup.result.stdout.trim().length === 0) {
|
|
@@ -193,23 +201,39 @@ export async function runSelfUpdate(options) {
|
|
|
193
201
|
priorActiveReleaseId: cohortState.references.active,
|
|
194
202
|
});
|
|
195
203
|
if (phaseBefore(transaction.phase, "ownership-released")) {
|
|
196
|
-
await
|
|
197
|
-
|
|
204
|
+
await measure("ownership-release", async () => {
|
|
205
|
+
await lifecycle.shutdownVerifiedOwners(targetVersion);
|
|
206
|
+
await lifecycle.verifyPackageUnlocked(packageRoot);
|
|
207
|
+
});
|
|
198
208
|
transaction = await transactionStore.advance("ownership-released");
|
|
199
209
|
}
|
|
200
210
|
if (phaseBefore(transaction.phase, "package-installed")) {
|
|
201
|
-
|
|
202
|
-
const installation = await runNpm(runner, ["install", "--global", `${PRODUCT_PACKAGE}@${targetVersion}`], false, output, "start the global npm installation", false);
|
|
211
|
+
const installation = await measure("npm-install", async () => await runNpm(runner, ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${PRODUCT_PACKAGE}@${targetVersion}`], true, output, "start the global npm installation", false));
|
|
203
212
|
if (installation.result === null)
|
|
204
213
|
throw new UpdateFailure(installation.exitCode, "npm process failed");
|
|
205
|
-
if (installation.result.code !== 0)
|
|
214
|
+
if (installation.result.code !== 0) {
|
|
215
|
+
if (installation.result.stdout.trim().length > 0)
|
|
216
|
+
output.stderr(`${installation.result.stdout.trimEnd()}\n`);
|
|
206
217
|
throw new UpdateFailure(unsuccessfulCode(installation.result.code), `npm exited with status ${formatExitCode(installation.result.code)}`);
|
|
218
|
+
}
|
|
207
219
|
transaction = await transactionStore.advance("package-installed");
|
|
208
220
|
}
|
|
209
|
-
|
|
221
|
+
// Ownership can be reacquired after an interrupted installation (for
|
|
222
|
+
// example, if bare A1 is launched before the update is resumed). Recheck
|
|
223
|
+
// immediately before activation so recovery cannot start a second cohort.
|
|
224
|
+
await measure("ownership-release", async () => { await lifecycle.shutdownVerifiedOwners(targetVersion); });
|
|
225
|
+
let activationPhaseStartedAt = now();
|
|
226
|
+
await lifecycle.activateInstalled(packageRoot, targetVersion, async (phase) => {
|
|
227
|
+
options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - activationPhaseStartedAt) });
|
|
228
|
+
transaction = await transactionStore.advance(phase);
|
|
229
|
+
activationPhaseStartedAt = now();
|
|
230
|
+
});
|
|
231
|
+
options.onPhaseTiming?.({ phase: "supervisor-verified", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
|
|
232
|
+
const transactionStartedAt = now();
|
|
210
233
|
await transactionStore.advance("supervisor-verified");
|
|
211
234
|
await transactionStore.finish("completed");
|
|
212
235
|
await transactionStore.clearCompleted();
|
|
236
|
+
options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
|
|
213
237
|
output.stdout(`${PRODUCT_TEXT.diagnostic(`updated successfully: ${targetVersion} (${channel}).`)}\n`);
|
|
214
238
|
return 0;
|
|
215
239
|
}
|
|
@@ -224,6 +248,21 @@ export async function runSelfUpdate(options) {
|
|
|
224
248
|
return error instanceof UpdateFailure ? error.exitCode : 1;
|
|
225
249
|
}
|
|
226
250
|
}
|
|
251
|
+
export function assertUpdatePerformanceBudget(evidence, maximumPostNpmDurationMs = 30_000) {
|
|
252
|
+
const failures = [];
|
|
253
|
+
if (evidence.fileCount < 1)
|
|
254
|
+
failures.push("fixture contains no payload files");
|
|
255
|
+
if (evidence.sourceReads !== evidence.fileCount)
|
|
256
|
+
failures.push(`source payload read count is ${evidence.sourceReads} for ${evidence.fileCount} files`);
|
|
257
|
+
if (evidence.candidateWrites !== evidence.fileCount)
|
|
258
|
+
failures.push(`candidate payload write count is ${evidence.candidateWrites} for ${evidence.fileCount} files`);
|
|
259
|
+
if (evidence.verificationReads > 0)
|
|
260
|
+
failures.push(`fresh certification reread ${evidence.verificationReads} candidate files`);
|
|
261
|
+
if (evidence.postNpmDurationMs > maximumPostNpmDurationMs)
|
|
262
|
+
failures.push(`post-npm activation took ${Math.round(evidence.postNpmDurationMs)}ms; budget is ${maximumPostNpmDurationMs}ms`);
|
|
263
|
+
if (failures.length > 0)
|
|
264
|
+
throw new Error(`update performance budget failed: ${failures.join("; ")}`);
|
|
265
|
+
}
|
|
227
266
|
async function requestUpdateShutdown(metadata, targetVersion, timeoutMs) {
|
|
228
267
|
if (!processIsAlive(metadata.pid))
|
|
229
268
|
return { accepted: false, reason: "recorded owner is dead" };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# CI and release operations
|
|
2
|
+
|
|
3
|
+
This runbook is the operational source for scoped development validation, immutable npm candidates, stable certification, and branch enforcement. GitHub Actions is the only automation platform.
|
|
4
|
+
|
|
5
|
+
## Stable status names
|
|
6
|
+
|
|
7
|
+
Repository rules use job display names, not internal job keys. Keep these names stable:
|
|
8
|
+
|
|
9
|
+
| Protected flow | Required check | Producer |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Pull request into `develop` | `Development validation required` | `.github/workflows/ci.yml` |
|
|
12
|
+
| Accepted `develop` commit | `Development validation required` | `.github/workflows/ci.yml` push run |
|
|
13
|
+
| Promotion into `master` | `Stable candidate required` | `.github/workflows/certify-stable.yml` |
|
|
14
|
+
|
|
15
|
+
Matrix and tier job names are implementation details. Change a required name only by updating the reviewable ruleset definition, its governance tests, this runbook, and the live ruleset together.
|
|
16
|
+
|
|
17
|
+
## Advisory rollout and widening validation
|
|
18
|
+
|
|
19
|
+
New or materially changed selection rules run in advisory mode before their check becomes required. Compare at least one feature-only change and one cross-cutting change with a complete run. A selection miss is a policy defect: widen `config/validation-impact.json`, add a regression test, and rerun complete validation before enabling enforcement.
|
|
20
|
+
|
|
21
|
+
The selector can only widen validation. To override an affected plan, dispatch **Development validation** at the exact `develop` ref with `full: true`. This selects `full-release`; it cannot suppress mandatory tiers. Dispatch **Full regression** for the complete non-physical package and clean-install path. Scheduled Full regression is also the backstop for impact-map mistakes.
|
|
22
|
+
|
|
23
|
+
When selection is uncertain, deleted, renamed, unmapped, or based on an unavailable Git range, it must fail closed to full validation. Do not bypass this fallback with labels or edited workflow inputs.
|
|
24
|
+
|
|
25
|
+
## Preview candidate and `next` publication
|
|
26
|
+
|
|
27
|
+
1. Confirm `Development validation required` is green for the exact `develop` tip.
|
|
28
|
+
2. Dispatch **Build npm next candidate** on `develop` with the exact source commit, a trusted ancestor base commit, and `confirm_candidate=build-uncertified-next-candidate`. Use `full: true` whenever ordinary affected coverage is not sufficient for the release decision.
|
|
29
|
+
3. Review `candidate-evidence.json`, selected scopes, gate outcomes, package integrity, and source tree. The candidate remains explicitly stable-ineligible.
|
|
30
|
+
4. Approve the protected `npm-next` environment and dispatch **Publish npm next** with the successful candidate run id and `confirm_next=publish-certified-next`.
|
|
31
|
+
5. The publisher downloads and verifies the certified tarball, then publishes those bytes without checkout, installation, build, or tests. Verify its registry digest and `next` tag result.
|
|
32
|
+
|
|
33
|
+
Preview candidates expire after 14 days. An expired, missing, failed, or mismatched artifact is never reconstructed in the publisher. Build and certify a new candidate.
|
|
34
|
+
|
|
35
|
+
## Stable candidate, physical evidence, and `latest` publication
|
|
36
|
+
|
|
37
|
+
1. Commit the final non-prerelease version to `develop`. Confirm it is clean, registry-unpublished, and `v<package-version>` is the intended tag.
|
|
38
|
+
2. Dispatch **Build stable candidate** on that exact `develop` commit with `confirm_candidate=build-stable-candidate`. It packs once on Windows and fans the same verified digest to Windows, Linux, and macOS complete automated validation and clean installation.
|
|
39
|
+
3. Review the `Stable automated candidate` artifact. It is not stable-eligible; physical evidence is still required.
|
|
40
|
+
4. On dedicated isolated workers only, dispatch **Certify stable physical platforms** for the same source and automated-candidate run with `confirm_isolated=run-isolated-physical-certification`. Workers must carry `self-hosted`, `a1-physical`, and platform-specific labels, set `PHYSICAL_WORKER_ISOLATED=true`, and be protected by the `stable-physical` environment. Never run physical host probes on a developer workstation or ordinary hosted runner.
|
|
41
|
+
5. Dispatch **Certify stable candidate** with the successful automated and physical run ids and `confirm_certification=certify-stable-candidate`. `Stable candidate required` passes only when all three automated and all three isolated physical verdicts bind the same commit, tree, version, integrity, and shasum.
|
|
42
|
+
6. Promote that exact commit to `master` without source or package changes and create `v<version>` at the same commit. The protected `master` rule requires the existing `Stable candidate required` check on that commit.
|
|
43
|
+
7. Dispatch **Publish npm stable** on the tag with the certification run id and `confirm_stable=publish-certified-stable-latest`, then approve `npm-stable`. It requires the current `master` and tag to equal the certified source, confirms the version is unpublished, publishes the exact tarball to `latest` with provenance, and verifies registry bytes.
|
|
44
|
+
|
|
45
|
+
Stable automated and physical candidate artifacts expire after 30 days. Publication evidence is retained for 90 days. Artifact expiry requires a new pack and complete recertification; it does not permit repacking during publication.
|
|
46
|
+
|
|
47
|
+
## Failure recovery
|
|
48
|
+
|
|
49
|
+
- **Development failure:** inspect impact and outcome artifacts. Fix the source or mapping and rerun. Do not mark a failed tier optional.
|
|
50
|
+
- **Candidate validation failure:** discard the candidate. Any source change, package change, or uncertain evidence requires a new candidate run.
|
|
51
|
+
- **Physical failure:** quarantine the worker result, fix or replace the isolated worker, and rerun all evidence needed for one exact package. A hosted matrix cannot substitute for physical evidence.
|
|
52
|
+
- **Approval or artifact expiry:** create and certify a new candidate. Never upload locally rebuilt bytes.
|
|
53
|
+
- **Publisher failure before npm accepts bytes:** retain the candidate and diagnose identity, permissions, registry, or provenance. Retry only with the same candidate run if the registry still proves the version unpublished and the artifact has not expired.
|
|
54
|
+
- **Publisher uncertainty after npm accepts bytes:** do not republish or rebuild. Query the registry for version, dist-tag, integrity, and shasum; repair a dist-tag only through a separately reviewed registry operation.
|
|
55
|
+
- **Partial stable certification:** stable eligibility remains false. Missing, duplicated, failed, non-isolated, or mismatched platform evidence fails closed.
|
|
56
|
+
|
|
57
|
+
## Enforcement rollout and rollback
|
|
58
|
+
|
|
59
|
+
Ruleset mutation is a separate administrative operation. First run `node scripts/check-github-rulesets.mjs` in report mode and review the proposed diff. Apply only after workflows exist on the default branch, representative advisory runs pass, and a maintainer explicitly confirms the exact ruleset change. Capture the post-apply repository API response as evidence.
|
|
60
|
+
|
|
61
|
+
If a required check is operationally broken, prefer correcting the workflow. Emergency rollback may disable only the affected required context after restoring the previous blocking validation path and recording maintainer approval. Never weaken force-push/deletion protection to release. Never route around certification by rebuilding inside a publisher.
|
|
62
|
+
|
|
63
|
+
After rollback, publication still requires exact certified bytes. A failed or unavailable candidate workflow means release waits for a new candidate; it does not authorize an ad hoc npm upload.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timurproko/a1",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.5",
|
|
4
4
|
"description": "Standalone terminal workspace for supervised native and managed agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.13.0",
|
|
@@ -21,16 +21,20 @@
|
|
|
21
21
|
"clean": "node scripts/clean.mjs",
|
|
22
22
|
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
23
23
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
-
"check:architecture": "node scripts/check-architecture.mjs && node scripts/product-identifier-policy.mjs --check && node scripts/check-product-identity-boundaries.mjs && node scripts/check-package-identity.mjs && node scripts/check-pinned-pi-source-ledger.mjs && node scripts/check-terminal-host-provenance.mjs",
|
|
24
|
+
"check:architecture": "node scripts/check-architecture.mjs && node scripts/product-identifier-policy.mjs --check && node scripts/check-product-identity-boundaries.mjs && node scripts/check-package-identity.mjs && node scripts/check-pinned-pi-source-ledger.mjs && node scripts/check-terminal-host-provenance.mjs && node scripts/check-validation-governance.mjs",
|
|
25
25
|
"check:customization-ready": "node scripts/check-owned-ui-customization-prerequisites.mjs",
|
|
26
26
|
"check:deprecated": "node scripts/check-deprecated-dependencies.mjs",
|
|
27
27
|
"branches:prune": "node scripts/prune-merged-branches.mjs",
|
|
28
|
-
"
|
|
28
|
+
"report:validation-inventory": "node scripts/report-validation-inventory.mjs",
|
|
29
|
+
"check": "npm run test:release",
|
|
29
30
|
"validate:agent": "npm run check",
|
|
30
31
|
"publish:next": "tsx scripts/publish-next.ts",
|
|
31
|
-
"test": "
|
|
32
|
-
"test:
|
|
33
|
-
"test:
|
|
32
|
+
"test": "npm run test:fast",
|
|
33
|
+
"test:fast": "node scripts/run-validation-tier.mjs invariants fast",
|
|
34
|
+
"test:unit": "node scripts/run-validation-tier.mjs fast",
|
|
35
|
+
"test:integration": "node scripts/run-validation-tier.mjs launch-integration pi-engine-conformance release-update structured-runtime-integration",
|
|
36
|
+
"test:scope": "node scripts/run-validation-tier.mjs",
|
|
37
|
+
"test:full": "node scripts/run-validation-tier.mjs full-release",
|
|
34
38
|
"test:release": "node scripts/run-release-gates.mjs",
|
|
35
39
|
"test:terminal-host": "node scripts/run-terminal-host-probe.mjs",
|
|
36
40
|
"test:pi-terminal-parity": "npm run build --silent && node scripts/run-pi-terminal-parity.mjs",
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { OwnedUiSessionShellRoot } from "../foundation/pi-owned-ui-integration/index.js";
|
|
2
|
-
import { PiTuiRuntimeAdapter, type PiTuiComponentPort } from "../foundation/pi-tui-runtime-adapter/index.js";
|
|
3
|
-
import type { OwnedUiApplicationPort, PresentationTerminalPort } from "../foundation/presentation-contracts/index.js";
|
|
4
|
-
import { type StructuredWorkspaceTabs } from "../features/workspace/index.js";
|
|
5
|
-
export interface StructuredWorkspaceApplicationOptions {
|
|
6
|
-
readonly workspace: StructuredWorkspaceTabs;
|
|
7
|
-
readonly cwd: string;
|
|
8
|
-
readonly terminal?: PresentationTerminalPort;
|
|
9
|
-
readonly mode?: "regular" | "fullscreen";
|
|
10
|
-
}
|
|
11
|
-
export declare class StructuredWorkspaceApplication implements OwnedUiApplicationPort {
|
|
12
|
-
#private;
|
|
13
|
-
readonly workspace: StructuredWorkspaceTabs;
|
|
14
|
-
readonly root: OwnedUiSessionShellRoot;
|
|
15
|
-
readonly runtime: PiTuiRuntimeAdapter;
|
|
16
|
-
readonly component: StructuredWorkspaceRootComponent;
|
|
17
|
-
constructor(options: StructuredWorkspaceApplicationOptions);
|
|
18
|
-
get disposed(): boolean;
|
|
19
|
-
start(): void;
|
|
20
|
-
flush(): Promise<void>;
|
|
21
|
-
waitUntilStopped(): Promise<void>;
|
|
22
|
-
submit(raw: string): Promise<void>;
|
|
23
|
-
switchRelative(direction: -1 | 1): Promise<void>;
|
|
24
|
-
dispose(): Promise<void>;
|
|
25
|
-
}
|
|
26
|
-
export declare class StructuredWorkspaceRootComponent implements PiTuiComponentPort {
|
|
27
|
-
readonly workspace: StructuredWorkspaceTabs;
|
|
28
|
-
readonly sessionRoot: OwnedUiSessionShellRoot;
|
|
29
|
-
readonly onSwitch: (direction: -1 | 1) => void;
|
|
30
|
-
readonly onEditorChange: (text: string) => void;
|
|
31
|
-
readonly notice: () => string | null;
|
|
32
|
-
constructor(workspace: StructuredWorkspaceTabs, sessionRoot: OwnedUiSessionShellRoot, onSwitch: (direction: -1 | 1) => void, onEditorChange: (text: string) => void, notice: () => string | null);
|
|
33
|
-
render(width: number): readonly string[];
|
|
34
|
-
handleInput(data: string): void;
|
|
35
|
-
invalidate(): void;
|
|
36
|
-
setFocused(focused: boolean): void;
|
|
37
|
-
dispose(): void;
|
|
38
|
-
}
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import { OWNED_UI_CONTRACT_VERSION, } from "../foundation/owned-ui-contracts/index.js";
|
|
2
|
-
import { OwnedUiSessionShellRoot } from "../foundation/pi-owned-ui-integration/index.js";
|
|
3
|
-
import { createPiTerminalBridge, PiTuiRuntimeAdapter, } from "../foundation/pi-tui-runtime-adapter/index.js";
|
|
4
|
-
import {} from "../features/workspace/index.js";
|
|
5
|
-
export class StructuredWorkspaceApplication {
|
|
6
|
-
workspace;
|
|
7
|
-
root;
|
|
8
|
-
runtime;
|
|
9
|
-
component;
|
|
10
|
-
#unsubscribe;
|
|
11
|
-
#resolveStopped;
|
|
12
|
-
#stopped;
|
|
13
|
-
#notice = null;
|
|
14
|
-
#disposed = false;
|
|
15
|
-
#started = false;
|
|
16
|
-
#agentSequence;
|
|
17
|
-
constructor(options) {
|
|
18
|
-
this.workspace = options.workspace;
|
|
19
|
-
const initial = selectedPanel(options.workspace.view());
|
|
20
|
-
if (!initial)
|
|
21
|
-
throw new TypeError("structured workspace application requires an initial agent tab");
|
|
22
|
-
this.#agentSequence = options.workspace.view().panels.length;
|
|
23
|
-
this.#stopped = new Promise(resolve => { this.#resolveStopped = resolve; });
|
|
24
|
-
this.root = new OwnedUiSessionShellRoot(toOwnedView(options.workspace.view(), initial, null, 80, 24), options.cwd, {
|
|
25
|
-
getColumns: () => this.runtime?.viewport().columns ?? options.terminal?.columns ?? 80,
|
|
26
|
-
getRows: () => this.runtime?.viewport().rows ?? options.terminal?.rows ?? 24,
|
|
27
|
-
requestRender: () => this.runtime?.requestRender(),
|
|
28
|
-
onSubmit: text => { void this.submit(text); },
|
|
29
|
-
onInterrupt: () => { this.#notice = "Use /agent stop to stop the selected agent."; this.runtime?.requestRender(); },
|
|
30
|
-
onClear: () => { this.root.editor.setText(""); this.runtime?.requestRender(); },
|
|
31
|
-
onExit: () => { void this.dispose(); },
|
|
32
|
-
onModelSelect: () => { this.#notice = "Model selection remains scoped to each structured agent session."; this.runtime?.requestRender(); },
|
|
33
|
-
onThinkingCycle: () => { this.#notice = "Thinking settings remain scoped to each structured agent session."; this.runtime?.requestRender(); },
|
|
34
|
-
});
|
|
35
|
-
this.component = new StructuredWorkspaceRootComponent(this.workspace, this.root, direction => { void this.switchRelative(direction); }, text => this.#saveEditor(text), () => this.#notice);
|
|
36
|
-
this.runtime = new PiTuiRuntimeAdapter({
|
|
37
|
-
root: this.component,
|
|
38
|
-
mode: options.mode ?? "regular",
|
|
39
|
-
...(options.terminal === undefined ? {} : { terminal: createPiTerminalBridge(options.terminal) }),
|
|
40
|
-
hardwareCursor: true,
|
|
41
|
-
mouse: false,
|
|
42
|
-
});
|
|
43
|
-
this.#unsubscribe = this.workspace.subscribe(view => this.#sync(view));
|
|
44
|
-
}
|
|
45
|
-
get disposed() { return this.#disposed; }
|
|
46
|
-
start() {
|
|
47
|
-
if (this.#started || this.#disposed)
|
|
48
|
-
return;
|
|
49
|
-
this.#started = true;
|
|
50
|
-
this.runtime.start();
|
|
51
|
-
this.#sync(this.workspace.view());
|
|
52
|
-
}
|
|
53
|
-
async flush() {
|
|
54
|
-
await this.workspace.flush();
|
|
55
|
-
this.#sync(this.workspace.view());
|
|
56
|
-
}
|
|
57
|
-
waitUntilStopped() { return this.#stopped; }
|
|
58
|
-
async submit(raw) {
|
|
59
|
-
const input = raw.trim();
|
|
60
|
-
if (!input)
|
|
61
|
-
return;
|
|
62
|
-
if (input === "/agent" || input.startsWith("/agent ")) {
|
|
63
|
-
await this.#agentCommand(input.slice("/agent".length).trim());
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const selected = selectedPanel(this.workspace.view());
|
|
67
|
-
if (!selected)
|
|
68
|
-
return;
|
|
69
|
-
this.workspace.setEditorText(selected.agentId, input);
|
|
70
|
-
const result = await this.workspace.sendPrompt(selected.agentId, input);
|
|
71
|
-
this.#notice = result.kind === "rejected" ? result.diagnostic : null;
|
|
72
|
-
this.#sync(this.workspace.view());
|
|
73
|
-
}
|
|
74
|
-
async switchRelative(direction) {
|
|
75
|
-
const view = this.workspace.view();
|
|
76
|
-
if (view.panels.length < 2)
|
|
77
|
-
return;
|
|
78
|
-
this.#saveEditor(this.root.editor.getText());
|
|
79
|
-
const current = Math.max(0, view.panels.findIndex(panel => panel.selected));
|
|
80
|
-
const next = (current + direction + view.panels.length) % view.panels.length;
|
|
81
|
-
const target = view.panels[next];
|
|
82
|
-
if (target)
|
|
83
|
-
await this.workspace.selectAgent(target.agentId);
|
|
84
|
-
}
|
|
85
|
-
async dispose() {
|
|
86
|
-
if (this.#disposed)
|
|
87
|
-
return;
|
|
88
|
-
this.#disposed = true;
|
|
89
|
-
this.#unsubscribe();
|
|
90
|
-
const failures = [];
|
|
91
|
-
await this.runtime.stop().catch(error => failures.push(error));
|
|
92
|
-
await this.workspace.dispose().catch(error => failures.push(error));
|
|
93
|
-
this.#resolveStopped?.();
|
|
94
|
-
if (failures.length > 0)
|
|
95
|
-
throw new AggregateError(failures, "structured workspace application disposal failed");
|
|
96
|
-
}
|
|
97
|
-
async #agentCommand(argument) {
|
|
98
|
-
const [command = "list", ...rest] = argument.split(/\s+/).filter(Boolean);
|
|
99
|
-
const view = this.workspace.view();
|
|
100
|
-
const selected = selectedPanel(view);
|
|
101
|
-
if (command === "new") {
|
|
102
|
-
this.#agentSequence += 1;
|
|
103
|
-
const id = `agent-${this.#agentSequence}`;
|
|
104
|
-
const displayName = rest.join(" ") || `Agent ${this.#agentSequence}`;
|
|
105
|
-
const created = await this.workspace.createAgent({ id, displayName });
|
|
106
|
-
if (created.kind === "applied")
|
|
107
|
-
await this.workspace.selectAgent(id);
|
|
108
|
-
this.#notice = created.kind === "rejected" ? created.diagnostic : `Created ${created.value.agentId}.`;
|
|
109
|
-
}
|
|
110
|
-
else if (command === "next" || command === "previous" || command === "prev") {
|
|
111
|
-
await this.switchRelative(command === "next" ? 1 : -1);
|
|
112
|
-
this.#notice = null;
|
|
113
|
-
}
|
|
114
|
-
else if (command === "select") {
|
|
115
|
-
const target = rest[0];
|
|
116
|
-
const selectedResult = target ? await this.workspace.selectAgent(target) : null;
|
|
117
|
-
this.#notice = selectedResult === null ? "Usage: /agent select <id>" : selectedResult.kind === "rejected" ? selectedResult.diagnostic : null;
|
|
118
|
-
}
|
|
119
|
-
else if (command === "stop") {
|
|
120
|
-
const result = selected ? await this.workspace.stopAgent(selected.agentId) : null;
|
|
121
|
-
this.#notice = result === null ? "No agent selected." : result.kind === "rejected" ? result.diagnostic : `Stopped ${selected.agentId}.`;
|
|
122
|
-
}
|
|
123
|
-
else if (command === "restart") {
|
|
124
|
-
const result = selected ? await this.workspace.restartAgent(selected.agentId) : null;
|
|
125
|
-
this.#notice = result === null ? "No agent selected." : result.kind === "rejected" ? result.diagnostic : `Restarted ${selected.agentId}.`;
|
|
126
|
-
}
|
|
127
|
-
else if (command === "remove") {
|
|
128
|
-
if (!selected)
|
|
129
|
-
this.#notice = "No agent selected.";
|
|
130
|
-
else {
|
|
131
|
-
if (selected.lifecycle !== "stopped" && selected.lifecycle !== "failed")
|
|
132
|
-
await this.workspace.stopAgent(selected.agentId);
|
|
133
|
-
const result = await this.workspace.removeAgent(selected.agentId);
|
|
134
|
-
this.#notice = result.kind === "rejected" ? result.diagnostic : `Removed ${selected.agentId}.`;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
else if (command === "list") {
|
|
138
|
-
this.#notice = view.tabs.length === 0 ? "No managed agents." : view.tabs.map(tab => `${tab.selected ? "*" : " "}${tab.agentId}:${tab.label}`).join(" ");
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
this.#notice = "Usage: /agent [list|new [name]|next|prev|select <id>|stop|restart|remove]";
|
|
142
|
-
}
|
|
143
|
-
this.#sync(this.workspace.view());
|
|
144
|
-
}
|
|
145
|
-
#saveEditor(text) {
|
|
146
|
-
const selected = selectedPanel(this.workspace.view());
|
|
147
|
-
if (selected)
|
|
148
|
-
this.workspace.setEditorText(selected.agentId, text);
|
|
149
|
-
}
|
|
150
|
-
#sync(view) {
|
|
151
|
-
const selected = selectedPanel(view);
|
|
152
|
-
if (!selected)
|
|
153
|
-
return;
|
|
154
|
-
const viewport = this.runtime?.viewport() ?? { columns: 80, rows: 24 };
|
|
155
|
-
this.root.update(toOwnedView(view, selected, this.#notice, viewport.columns, viewport.rows));
|
|
156
|
-
if (this.root.editor.getText() !== selected.editorText)
|
|
157
|
-
this.root.editor.setText(selected.editorText);
|
|
158
|
-
this.component.invalidate();
|
|
159
|
-
this.runtime?.requestRender();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
export class StructuredWorkspaceRootComponent {
|
|
163
|
-
workspace;
|
|
164
|
-
sessionRoot;
|
|
165
|
-
onSwitch;
|
|
166
|
-
onEditorChange;
|
|
167
|
-
notice;
|
|
168
|
-
constructor(workspace, sessionRoot, onSwitch, onEditorChange, notice) {
|
|
169
|
-
this.workspace = workspace;
|
|
170
|
-
this.sessionRoot = sessionRoot;
|
|
171
|
-
this.onSwitch = onSwitch;
|
|
172
|
-
this.onEditorChange = onEditorChange;
|
|
173
|
-
this.notice = notice;
|
|
174
|
-
}
|
|
175
|
-
render(width) {
|
|
176
|
-
const view = this.workspace.view();
|
|
177
|
-
const tabLine = truncate(view.tabs.map(tab => `${tab.selected ? "[" : " "}${tab.label}${tab.selected ? "]" : " "}`).join(" "), width);
|
|
178
|
-
const notice = this.notice();
|
|
179
|
-
return [tabLine, ...(notice ? [truncate(notice, width)] : []), ...this.sessionRoot.render(width)];
|
|
180
|
-
}
|
|
181
|
-
handleInput(data) {
|
|
182
|
-
if (data === "\x1b[6;5~" || data === "\x1b[1;3C") {
|
|
183
|
-
this.onEditorChange(this.sessionRoot.editor.getText());
|
|
184
|
-
this.onSwitch(1);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (data === "\x1b[5;5~" || data === "\x1b[1;3D") {
|
|
188
|
-
this.onEditorChange(this.sessionRoot.editor.getText());
|
|
189
|
-
this.onSwitch(-1);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
this.sessionRoot.editor.handleInput?.(data);
|
|
193
|
-
}
|
|
194
|
-
invalidate() { this.sessionRoot.invalidate(); }
|
|
195
|
-
setFocused(focused) { this.sessionRoot.editor.setFocused?.(focused); }
|
|
196
|
-
dispose() { this.sessionRoot.dispose?.(); }
|
|
197
|
-
}
|
|
198
|
-
function selectedPanel(view) {
|
|
199
|
-
return view.selectedPanel ?? view.panels[0] ?? null;
|
|
200
|
-
}
|
|
201
|
-
function toOwnedView(workspace, panel, notice, columns, rows) {
|
|
202
|
-
const agent = workspace.workspace.agents.find(candidate => candidate.id === panel.agentId);
|
|
203
|
-
return {
|
|
204
|
-
contractVersion: OWNED_UI_CONTRACT_VERSION,
|
|
205
|
-
sessionId: panel.sessionId,
|
|
206
|
-
revision: workspace.workspace.revision + panel.lastSequence,
|
|
207
|
-
lifecycle: ownedLifecycle(panel.lifecycle),
|
|
208
|
-
transcript: panel.transcript.flatMap(messageBlocks),
|
|
209
|
-
editor: {
|
|
210
|
-
text: panel.editorText,
|
|
211
|
-
queuedSubmissions: [],
|
|
212
|
-
selection: null,
|
|
213
|
-
cursorOffset: panel.editorText.length,
|
|
214
|
-
historyRevision: panel.lastSequence,
|
|
215
|
-
submitEnabled: panel.lifecycle !== "stopping" && panel.lifecycle !== "stopped" && panel.lifecycle !== "failed",
|
|
216
|
-
},
|
|
217
|
-
status: {
|
|
218
|
-
title: agent?.displayName ?? panel.agentId,
|
|
219
|
-
workingMessage: panel.lifecycle === "busy" ? "Working" : null,
|
|
220
|
-
diagnostics: panel.failure ? [panel.failure] : notice ? [notice] : [],
|
|
221
|
-
badges: [
|
|
222
|
-
`${workspace.panels.length} agents`,
|
|
223
|
-
...(agent?.unreadActivity ? [`${agent.unreadActivity} unread`] : []),
|
|
224
|
-
...(agent?.attention ? ["attention"] : []),
|
|
225
|
-
],
|
|
226
|
-
footer: { branch: null, sessionName: agent?.displayName ?? null, availableProviderCount: 0, extensionStatuses: [] },
|
|
227
|
-
},
|
|
228
|
-
terminal: { columns, rows, focusedRegion: "editor", hardwareCursor: true },
|
|
229
|
-
activeModel: null,
|
|
230
|
-
thinkingLevel: "medium",
|
|
231
|
-
activeCommandIds: panel.activeCommandIds,
|
|
232
|
-
dialog: null,
|
|
233
|
-
overlay: null,
|
|
234
|
-
customizations: [],
|
|
235
|
-
diagnostics: panel.failure ? [{ sequence: panel.lastSequence, code: "structured-agent", severity: "error", message: panel.failure, recoverable: true }] : [],
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
function messageBlocks(message) {
|
|
239
|
-
return message.content.map((content, index) => {
|
|
240
|
-
const base = { id: `${message.id}.${index}`, status: message.status === "streaming" ? "live" : "finalized", revision: index + 1, title: null };
|
|
241
|
-
if (content.kind === "thinking")
|
|
242
|
-
return { ...base, kind: "thinking", text: content.text, payload: {} };
|
|
243
|
-
if (content.kind === "tool-call")
|
|
244
|
-
return { ...base, kind: "tool-call", title: content.toolName, text: JSON.stringify(content.input), payload: content };
|
|
245
|
-
if (content.kind === "tool-result")
|
|
246
|
-
return { ...base, kind: "tool-result", text: JSON.stringify(content.output), payload: content };
|
|
247
|
-
if (content.kind === "image")
|
|
248
|
-
return { ...base, kind: "custom", title: content.mediaType, text: "[image]", payload: content };
|
|
249
|
-
if (content.kind === "unknown")
|
|
250
|
-
return { ...base, kind: "custom", title: content.sourceType, text: JSON.stringify(content.payload), payload: content };
|
|
251
|
-
return { ...base, kind: message.role === "user" ? "user" : message.role === "system" ? "system" : message.role === "tool" ? "tool-result" : "assistant", text: content.text, payload: {} };
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
function ownedLifecycle(lifecycle) {
|
|
255
|
-
return lifecycle;
|
|
256
|
-
}
|
|
257
|
-
function truncate(value, width) {
|
|
258
|
-
if (width <= 0)
|
|
259
|
-
return "";
|
|
260
|
-
return value.length <= width ? value : width === 1 ? "…" : `${value.slice(0, width - 1)}…`;
|
|
261
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { type AgentCommandOutcome, type AgentEnginePort, type AgentMessage, type AgentSessionLifecycle } from "../../foundation/agent-engine-contracts/index.js";
|
|
2
|
-
import { type WorkspacePresentationModel } from "./presentation.js";
|
|
3
|
-
import { type WorkspaceView } from "./reducer.js";
|
|
4
|
-
import { WorkspaceRouter } from "./router.js";
|
|
5
|
-
export interface StructuredWorkspaceLimits {
|
|
6
|
-
readonly maxAgents: number;
|
|
7
|
-
readonly maxMessagesPerAgent: number;
|
|
8
|
-
readonly maxMessageBytes: number;
|
|
9
|
-
readonly maxEditorBytes: number;
|
|
10
|
-
}
|
|
11
|
-
export interface StructuredWorkspaceTabsOptions {
|
|
12
|
-
readonly workspaceId?: string;
|
|
13
|
-
readonly cwd: string;
|
|
14
|
-
readonly createEngine: (agentId: string) => Promise<AgentEnginePort>;
|
|
15
|
-
readonly limits?: Partial<StructuredWorkspaceLimits>;
|
|
16
|
-
readonly now?: () => string;
|
|
17
|
-
}
|
|
18
|
-
export interface StructuredAgentTabView {
|
|
19
|
-
readonly role: "tabpanel";
|
|
20
|
-
readonly agentId: string;
|
|
21
|
-
readonly sessionId: string;
|
|
22
|
-
readonly selected: boolean;
|
|
23
|
-
readonly lifecycle: AgentSessionLifecycle;
|
|
24
|
-
readonly transcript: readonly AgentMessage[];
|
|
25
|
-
readonly toolMessages: readonly AgentMessage[];
|
|
26
|
-
readonly editorText: string;
|
|
27
|
-
readonly activeCommandIds: readonly string[];
|
|
28
|
-
readonly lastSequence: number;
|
|
29
|
-
readonly failure: string | null;
|
|
30
|
-
readonly accessibleDescription: string;
|
|
31
|
-
}
|
|
32
|
-
export interface StructuredWorkspaceTabSelector {
|
|
33
|
-
readonly role: "tab";
|
|
34
|
-
readonly agentId: string;
|
|
35
|
-
readonly label: string;
|
|
36
|
-
readonly selected: boolean;
|
|
37
|
-
readonly accessibleDescription: string;
|
|
38
|
-
}
|
|
39
|
-
export interface StructuredWorkspaceTabsView {
|
|
40
|
-
readonly role: "tablist";
|
|
41
|
-
readonly workspace: WorkspaceView;
|
|
42
|
-
readonly presentation: WorkspacePresentationModel;
|
|
43
|
-
readonly tabs: readonly StructuredWorkspaceTabSelector[];
|
|
44
|
-
readonly panels: readonly StructuredAgentTabView[];
|
|
45
|
-
readonly selectedPanel: StructuredAgentTabView | null;
|
|
46
|
-
}
|
|
47
|
-
export type StructuredWorkspaceTabsResult<T> = {
|
|
48
|
-
readonly kind: "applied";
|
|
49
|
-
readonly view: StructuredWorkspaceTabsView;
|
|
50
|
-
readonly value: T;
|
|
51
|
-
} | {
|
|
52
|
-
readonly kind: "rejected";
|
|
53
|
-
readonly code: string;
|
|
54
|
-
readonly diagnostic: string;
|
|
55
|
-
};
|
|
56
|
-
export declare class StructuredWorkspaceTabs {
|
|
57
|
-
#private;
|
|
58
|
-
readonly router: WorkspaceRouter;
|
|
59
|
-
constructor(options: StructuredWorkspaceTabsOptions);
|
|
60
|
-
subscribe(listener: (view: StructuredWorkspaceTabsView) => void): () => void;
|
|
61
|
-
createAgent(input: {
|
|
62
|
-
readonly id: string;
|
|
63
|
-
readonly displayName: string;
|
|
64
|
-
readonly sessionId?: string;
|
|
65
|
-
}): Promise<StructuredWorkspaceTabsResult<StructuredAgentTabView>>;
|
|
66
|
-
selectAgent(agentId: string): Promise<StructuredWorkspaceTabsResult<StructuredAgentTabView>>;
|
|
67
|
-
setEditorText(agentId: string, text: string): StructuredWorkspaceTabsResult<StructuredAgentTabView>;
|
|
68
|
-
submitSelected(): Promise<StructuredWorkspaceTabsResult<{
|
|
69
|
-
readonly correlationId: string;
|
|
70
|
-
readonly outcome: AgentCommandOutcome;
|
|
71
|
-
}>>;
|
|
72
|
-
sendPrompt(agentId: string, text: string): Promise<StructuredWorkspaceTabsResult<{
|
|
73
|
-
readonly correlationId: string;
|
|
74
|
-
readonly outcome: AgentCommandOutcome;
|
|
75
|
-
}>>;
|
|
76
|
-
stopAgent(agentId: string): Promise<StructuredWorkspaceTabsResult<StructuredAgentTabView>>;
|
|
77
|
-
restartAgent(agentId: string): Promise<StructuredWorkspaceTabsResult<StructuredAgentTabView>>;
|
|
78
|
-
refreshAgent(agentId: string): Promise<StructuredWorkspaceTabsResult<StructuredAgentTabView>>;
|
|
79
|
-
removeAgent(agentId: string): Promise<StructuredWorkspaceTabsResult<string>>;
|
|
80
|
-
flush(): Promise<void>;
|
|
81
|
-
view(): StructuredWorkspaceTabsView;
|
|
82
|
-
dispose(): Promise<void>;
|
|
83
|
-
}
|