@piagent/platform 1.37.1 → 1.38.2
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/CHANGELOG.md +60 -0
- package/README.md +5 -3
- package/README.vi.md +2 -2
- package/catalog/capabilities.json +2 -2
- package/docs/command-reference-vietnamese.md +4 -3
- package/docs/distribution-standard.md +6 -6
- package/docs/en/maintainer-guide.md +8 -0
- package/docs/en/workspace-editor.md +222 -0
- package/docs/languages.json +13 -4
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +3 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/research/ide-m0-spike.md +101 -0
- package/docs/team-onboarding.md +2 -2
- package/docs/vi/maintainer-guide.md +8 -0
- package/docs/vi/workspace-editor.md +217 -0
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/index.html +4 -4
- package/docs-site/content/en/whats-new.html +23 -0
- package/docs-site/content/index.html +4 -4
- package/docs-site/content/whats-new.html +23 -0
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +5 -5
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +24 -1
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +5 -5
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +24 -1
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/piagent-webui/checkpoint-v1.invalid.json +21 -0
- package/evals/fixtures/piagent-webui/checkpoint-v1.valid.json +20 -0
- package/evals/fixtures/piagent-webui/terminal-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/terminal-v1.valid.json +16 -0
- package/evals/fixtures/piagent-webui/workspace-file-v1.invalid.json +24 -0
- package/evals/fixtures/piagent-webui/workspace-file-v1.valid.json +23 -0
- package/evals/fixtures/piagent-webui/workspace-paths-v1.invalid.json +24 -0
- package/evals/fixtures/piagent-webui/workspace-paths-v1.valid.json +24 -0
- package/evals/fixtures/piagent-webui/workspace-search-v1.invalid.json +26 -0
- package/evals/fixtures/piagent-webui/workspace-search-v1.valid.json +26 -0
- package/evals/fixtures/piagent-webui/workspace-tree-v1.invalid.json +28 -0
- package/evals/fixtures/piagent-webui/workspace-tree-v1.valid.json +27 -0
- package/evals/fixtures/piagent-webui/workspace-write-v1.invalid.json +19 -0
- package/evals/fixtures/piagent-webui/workspace-write-v1.valid.json +19 -0
- package/evals/long-task-soak-v1/README.md +17 -0
- package/evals/long-task-soak-v1/lib/session-loop.mjs +19 -3
- package/evals/long-task-soak-v1/scenarios/ide-editor-terminal.json +19 -0
- package/evals/long-task-soak-v1/surfaces/webui-gateway.mjs +17 -2
- package/evals/long-task-soak-v1/surfaces/workspace-actions.mjs +95 -0
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +7 -3
- package/packages/piagent-core/extensions/fallback-base-policy.ts +1 -1
- package/packages/piagent-core/extensions/guard-types.ts +3 -1
- package/packages/piagent-core/extensions/path-suggestions.ts +137 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +85 -7
- package/packages/piagent-core/extensions/workspace-inspection.ts +145 -0
- package/packages/piagent-core/extensions/workspace-policy.d.ts +7 -0
- package/packages/piagent-core/extensions/workspace-policy.js +3 -0
- package/packages/piagent-core/extensions/workspace-search-index.mjs +8 -0
- package/packages/piagent-core/extensions/workspace-search.ts +74 -0
- package/packages/piagent-core/extensions/workspace-write.ts +116 -0
- package/packages/piagent-core/managed/background-process.mjs +9 -4
- package/packages/piagent-core/managed/boundary-package-proxy.mjs +21 -0
- package/packages/piagent-core/managed/boundary-queue.mjs +24 -0
- package/packages/piagent-core/managed/checkpoint-git.mjs +16 -0
- package/packages/piagent-core/managed/checkpoints.mjs +17 -64
- package/packages/piagent-core/managed/command-tools.mjs +6 -6
- package/packages/piagent-core/managed/continuation.mjs +3 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/backend-api/profile.json +88 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/be-readonly-fe/profile.json +130 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/data/profile.json +88 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/devops/profile.json +90 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/docs/profile.json +74 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/fullstack/profile.json +92 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/generic/profile.json +95 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/mobile/profile.json +86 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/node-typescript/profile.json +74 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/python/profile.json +75 -0
- package/packages/piagent-core/managed/dist/workspace/assets/adapters/web-frontend/profile.json +99 -0
- package/packages/piagent-core/managed/dist/workspace/assets/local-state-path-BrmghxEZ.js +284 -0
- package/packages/piagent-core/managed/dist/workspace/assets/packages/piagent-core/policies/base-policy.json +351 -0
- package/packages/piagent-core/managed/dist/workspace/manifest.json +65 -0
- package/packages/piagent-core/managed/dist/workspace/worker.mjs +7374 -0
- package/packages/piagent-core/managed/dist/workspace/workspace-search-index.mjs +1007 -0
- package/packages/piagent-core/managed/patch-snapshot.mjs +6 -8
- package/packages/piagent-core/managed/permission.mjs +29 -6
- package/packages/piagent-core/managed/repository-operation.mjs +2 -1
- package/packages/piagent-core/managed/review-snapshot-request.mjs +21 -0
- package/packages/piagent-core/managed/sandbox-diagnostic.mjs +2 -0
- package/packages/piagent-core/managed/session-mutation-observations.mjs +37 -0
- package/packages/piagent-core/managed/session.mjs +2 -2
- package/packages/piagent-core/managed/source-revert-requests.mjs +21 -0
- package/packages/piagent-core/managed/terminal-boundary.mjs +88 -0
- package/packages/piagent-core/managed/terminal-channel.mjs +66 -0
- package/packages/piagent-core/managed/terminal-runtime.mjs +64 -0
- package/packages/piagent-core/managed/terminal-seatbelt.mjs +48 -0
- package/packages/piagent-core/managed/terminal-worker.mjs +70 -0
- package/packages/piagent-core/managed/tool-boundary.mjs +45 -35
- package/packages/piagent-core/managed/tool-freshness.mjs +47 -0
- package/packages/piagent-core/managed/tool-worker.mjs +18 -3
- package/packages/piagent-core/managed/workspace-worker-pins.mjs +26 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +2 -1
- package/packages/piagent-core/runtime/checkpoints/checkpoint-authority.ts +43 -0
- package/packages/piagent-core/runtime/checkpoints/checkpoint-refs.mjs +51 -0
- package/packages/piagent-core/runtime/checkpoints/checkpoint-store.mjs +179 -0
- package/packages/piagent-core/runtime/checkpoints/checkpoint-worker.ts +28 -0
- package/packages/piagent-core/runtime/checkpoints/git-adapter.mjs +36 -0
- package/packages/piagent-core/runtime/checkpoints/persistent-history.mjs +96 -0
- package/packages/piagent-core/runtime/checkpoints/personal-git.ts +272 -0
- package/packages/piagent-core/runtime/checkpoints/personal-runtime.ts +158 -0
- package/packages/piagent-core/runtime/checkpoints/prune.mjs +29 -0
- package/packages/piagent-core/runtime/checkpoints/restore-preview.mjs +46 -0
- package/packages/piagent-core/runtime/checkpoints/turn-checkpoints.mjs +167 -0
- package/packages/piagent-core/runtime/hooks/session-hooks.ts +19 -16
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +3 -0
- package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +2 -2
- package/packages/piagent-core/runtime/inspection/diff-projection.ts +2 -1
- package/packages/piagent-core/runtime/inspection/managed-source-revert.ts +90 -0
- package/packages/piagent-core/runtime/inspection/review-state-projection.ts +19 -8
- package/packages/piagent-core/runtime/inspection/session-diff-projection.ts +47 -0
- package/packages/piagent-core/runtime/inspection/session-hunk-provenance.ts +56 -0
- package/packages/piagent-core/runtime/inspection/session-mutation-observer.ts +125 -0
- package/packages/piagent-core/runtime/inspection/source-action-contract.ts +123 -0
- package/packages/piagent-core/runtime/inspection/source-change-projection.ts +3 -9
- package/packages/piagent-core/runtime/inspection/source-mutation-projection.ts +3 -41
- package/packages/piagent-core/runtime/inspection/source-mutation-store.ts +6 -36
- package/packages/piagent-core/runtime/inspection/source-revert-projection.ts +36 -30
- package/packages/piagent-core/runtime/inspection/text-diff.ts +5 -4
- package/packages/piagent-core/runtime/inspection/workspace-worker.ts +79 -0
- package/packages/piagent-core/runtime/policy/source-index-transaction.ts +3 -10
- package/packages/piagent-core/runtime/policy/source-mutation-guard-binding.ts +27 -2
- package/packages/piagent-core/runtime/policy/source-mutation-guard.ts +42 -12
- package/packages/piagent-core/runtime/policy/source-worktree-transaction.ts +10 -5
- package/packages/piagent-core/runtime/policy/workspace-write-authority.ts +51 -0
- package/packages/piagent-core/runtime/process-owner.mjs +20 -0
- package/packages/piagent-core/runtime/process-tree-watch.mjs +46 -0
- package/packages/piagent-core/runtime/quality/model-mutation-proof.ts +8 -5
- package/packages/piagent-core/runtime/registration/permission-commands.ts +10 -2
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +1 -1
- package/packages/piagent-core/runtime/session/workspace-member-edits.d.mts +7 -0
- package/packages/piagent-core/runtime/session/workspace-member-edits.mjs +41 -0
- package/packages/piagent-core/security/workspace-file-reader.ts +2 -1
- package/packages/piagent-webui/dist/client/assets/Alert-DGWhP2MM.js +1 -0
- package/packages/piagent-webui/dist/client/assets/Button-QItfZUNj.js +1 -0
- package/packages/piagent-webui/dist/client/assets/DialogTitle-CyaFPaiW.js +3 -0
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-DtEwqme0.js +29 -0
- package/packages/piagent-webui/dist/client/assets/TerminalPanel-C3vkzjQ6.js +2 -0
- package/packages/piagent-webui/dist/client/assets/TerminalView-TNjhBGfa.js +9 -0
- package/packages/piagent-webui/dist/client/assets/TerminalView-kHJ-D0s7.css +1 -0
- package/packages/piagent-webui/dist/client/assets/Typography-B9om7Evq.js +1 -0
- package/packages/piagent-webui/dist/client/assets/WorkspaceCodeEditor-BAhYwQNM.js +2 -0
- package/packages/piagent-webui/dist/client/assets/WorkspaceExplorer-Coh_GWCO.js +5 -0
- package/packages/piagent-webui/dist/client/assets/WorkspaceQuickOpen-Bn4miZq4.js +1 -0
- package/packages/piagent-webui/dist/client/assets/bootstrap-CedkNtRG.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-B5MCHZv5.js +9 -0
- package/packages/piagent-webui/dist/client/assets/dist-BM_LL1o9.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-C2TMLUTo.js +23 -0
- package/packages/piagent-webui/dist/client/assets/dist-CqL3udNu.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-D4df0jLQ.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-D6i5SpQu.js +10 -0
- package/packages/piagent-webui/dist/client/assets/dist-DfjUS8LS.js +6 -0
- package/packages/piagent-webui/dist/client/assets/dist-IRUjhd1P.js +1 -0
- package/packages/piagent-webui/dist/client/assets/dist-sNhoSseW.js +1 -0
- package/packages/piagent-webui/dist/client/assets/index-Brs_HjIo.css +1 -0
- package/packages/piagent-webui/dist/client/assets/index-a-perpOX.js +102 -0
- package/packages/piagent-webui/dist/client/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/packages/piagent-webui/dist/client/assets/tab-close-nAxrK7Lq.js +1 -0
- package/packages/piagent-webui/dist/client/assets/ui-preferences-BO3AV7QE.js +10 -0
- package/packages/piagent-webui/dist/client/assets/useId-DEefKUAT.js +124 -0
- package/packages/piagent-webui/dist/client/assets/workspace-api-O7auv_td.js +1 -0
- package/packages/piagent-webui/dist/client/index.html +10 -2
- package/packages/piagent-webui/extension/same-session-bridge.ts +2 -3
- package/packages/piagent-webui/extension/source-revert-controller.ts +14 -208
- package/packages/piagent-webui/gateway/checkpoint-sessions.ts +99 -0
- package/packages/piagent-webui/gateway/checkpoint-store.ts +6 -0
- package/packages/piagent-webui/gateway/company-relay.ts +5 -0
- package/packages/piagent-webui/gateway/company-terminal-cache.ts +54 -0
- package/packages/piagent-webui/gateway/company-terminal-channel.ts +117 -0
- package/packages/piagent-webui/gateway/company-terminal-relay.ts +75 -0
- package/packages/piagent-webui/gateway/gateway-protocol-service.ts +12 -0
- package/packages/piagent-webui/gateway/gateway-service.ts +48 -4
- package/packages/piagent-webui/gateway/managed-gateway.mjs +30 -3
- package/packages/piagent-webui/gateway/managed-projection.ts +5 -3
- package/packages/piagent-webui/gateway/managed-source-revert-controller.ts +45 -0
- package/packages/piagent-webui/gateway/managed-source-revert-sessions.ts +74 -0
- package/packages/piagent-webui/gateway/managed-terminal-binding.ts +30 -0
- package/packages/piagent-webui/gateway/managed-terminal-processes.ts +74 -0
- package/packages/piagent-webui/gateway/managed-terminal-sessions.ts +9 -0
- package/packages/piagent-webui/gateway/path-suggestions.ts +1 -124
- package/packages/piagent-webui/gateway/personal-terminal-binding.ts +36 -0
- package/packages/piagent-webui/gateway/personal-terminal-diagnostic.mjs +16 -0
- package/packages/piagent-webui/gateway/personal-terminal-processes.ts +119 -0
- package/packages/piagent-webui/gateway/personal-terminal-sessions.ts +22 -0
- package/packages/piagent-webui/gateway/runtime-session-controls.ts +2 -1
- package/packages/piagent-webui/gateway/session-catalog.ts +3 -2
- package/packages/piagent-webui/gateway/session-command-controller.ts +4 -4
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +29 -5
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -8
- package/packages/piagent-webui/gateway/signed-session-journal.ts +77 -0
- package/packages/piagent-webui/gateway/source-revert-sessions.ts +74 -0
- package/packages/piagent-webui/gateway/source-revert-store.ts +47 -0
- package/packages/piagent-webui/gateway/terminal-command-ledger.ts +33 -0
- package/packages/piagent-webui/gateway/terminal-mentions.ts +29 -0
- package/packages/piagent-webui/gateway/terminal-output-buffer.ts +58 -0
- package/packages/piagent-webui/gateway/terminal-router.ts +12 -0
- package/packages/piagent-webui/gateway/terminal-sessions.ts +247 -0
- package/packages/piagent-webui/gateway/workspace-write-sessions.ts +69 -0
- package/packages/piagent-webui/gateway/workspace-write-store.ts +6 -0
- package/packages/piagent-webui/package.json +15 -2
- package/packages/piagent-webui/server/checkpoint-command.ts +30 -0
- package/packages/piagent-webui/server/checkpoint-controller.ts +87 -0
- package/packages/piagent-webui/server/checkpoint-routes.ts +38 -0
- package/packages/piagent-webui/server/control-command-digest.ts +5 -0
- package/packages/piagent-webui/server/core-inspection-provider.ts +16 -22
- package/packages/piagent-webui/server/gateway-websocket.ts +36 -8
- package/packages/piagent-webui/server/loopback-server.ts +9 -0
- package/packages/piagent-webui/server/read-model-provider.ts +3 -0
- package/packages/piagent-webui/server/read-only-router.ts +3 -0
- package/packages/piagent-webui/server/session-auth.ts +4 -1
- package/packages/piagent-webui/server/session-source-revert-routes.ts +54 -0
- package/packages/piagent-webui/server/source-projection-binding.ts +18 -0
- package/packages/piagent-webui/server/source-revert-controller.ts +225 -0
- package/packages/piagent-webui/server/terminal-command.ts +35 -0
- package/packages/piagent-webui/server/terminal-websocket-stream.ts +56 -0
- package/packages/piagent-webui/server/transcript-projection.ts +2 -1
- package/packages/piagent-webui/server/workspace-read-routes.ts +32 -0
- package/packages/piagent-webui/server/workspace-reader.ts +23 -0
- package/packages/piagent-webui/server/workspace-write-controller.ts +118 -0
- package/packages/piagent-webui/server/workspace-write-routes.ts +38 -0
- package/packages/piagent-webui/shared/terminal-message.ts +23 -0
- package/schemas/piagent-webui/catalog-v1.json +63 -0
- package/schemas/piagent-webui/checkpoint-v1.schema.json +466 -0
- package/schemas/piagent-webui/control-command-v1.schema.json +68 -3
- package/schemas/piagent-webui/diff-v1.schema.json +4 -0
- package/schemas/piagent-webui/gateway-protocol-v1.schema.json +123 -3
- package/schemas/piagent-webui/session-catalog-v1.schema.json +1 -1
- package/schemas/piagent-webui/session-command-v1.schema.json +1 -1
- package/schemas/piagent-webui/snapshot-v1.schema.json +2 -1
- package/schemas/piagent-webui/source-revert-v1.schema.json +22 -3
- package/schemas/piagent-webui/terminal-v1.schema.json +495 -0
- package/schemas/piagent-webui/workspace-file-v1.schema.json +79 -0
- package/schemas/piagent-webui/workspace-paths-v1.schema.json +71 -0
- package/schemas/piagent-webui/workspace-search-v1.schema.json +80 -0
- package/schemas/piagent-webui/workspace-tree-v1.schema.json +87 -0
- package/schemas/piagent-webui/workspace-write-v1.schema.json +276 -0
- package/schemas/project-profile.schema.json +8 -3
- package/scripts/company-terminal.mjs +16 -11
- package/scripts/pi-auto.sh +5 -1
- package/scripts/team-doctor.sh +8 -0
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-CfgyQQbW.js +0 -29
- package/packages/piagent-webui/dist/client/assets/index-HyzWKvDu.js +0 -234
- package/packages/piagent-webui/dist/client/assets/index-_0nGP2n3.css +0 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { TerminalStreamPort } from "../server/terminal-websocket-stream.ts";
|
|
2
|
+
import { parseTerminalCommand } from "../server/terminal-command.ts";
|
|
1
3
|
import type { PiagentGatewayCapabilityHandshakeV1 } from "../contracts/generated/gateway-capabilities-v1.ts";
|
|
2
4
|
import type { Catalog, SessionDetail } from "../contracts/generated/session-catalog-v1.ts";
|
|
3
5
|
import type { GatewayProtocolHandler, GatewayRequest } from "../server/gateway-websocket.ts";
|
|
@@ -13,19 +15,24 @@ export class GatewayProtocolFailure extends Error {
|
|
|
13
15
|
|
|
14
16
|
export class GatewayProtocolService implements GatewayProtocolHandler {
|
|
15
17
|
readonly events: GatewayEventStore;
|
|
18
|
+
readonly terminalEvents?: TerminalStreamPort;
|
|
16
19
|
readonly #capabilities: () => PiagentGatewayCapabilityHandshakeV1;
|
|
17
20
|
readonly #catalog: () => Promise<Catalog>;
|
|
21
|
+
readonly #terminals?: { state(sessionRef: string): Promise<unknown>; execute(command: unknown): Promise<unknown> };
|
|
18
22
|
readonly #command?: { execute(command: unknown): Promise<unknown> };
|
|
19
23
|
|
|
20
24
|
constructor(options: {
|
|
21
25
|
capabilities(): PiagentGatewayCapabilityHandshakeV1;
|
|
22
26
|
catalog(): Promise<Catalog>;
|
|
23
27
|
events?: GatewayEventStore;
|
|
28
|
+
terminalEvents?: TerminalStreamPort;
|
|
24
29
|
command?: { execute(command: unknown): Promise<unknown> };
|
|
30
|
+
terminals?: { state(sessionRef: string): Promise<unknown>; execute(command: unknown): Promise<unknown> };
|
|
25
31
|
}) {
|
|
26
32
|
this.#capabilities = options.capabilities;
|
|
27
33
|
this.#catalog = options.catalog;
|
|
28
34
|
this.#command = options.command;
|
|
35
|
+
this.#terminals = options.terminals; this.terminalEvents = options.terminalEvents;
|
|
29
36
|
this.events = options.events ?? new GatewayEventStore();
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -38,6 +45,11 @@ export class GatewayProtocolService implements GatewayProtocolHandler {
|
|
|
38
45
|
try { return await this.#command.execute(request.params.command); }
|
|
39
46
|
catch (error) { throw new GatewayProtocolFailure(error instanceof Error ? error.message : "invalid-session-command"); }
|
|
40
47
|
}
|
|
48
|
+
if (request.method === "terminals.state" || request.method === "terminals.command") {
|
|
49
|
+
if (!this.#terminals) throw new GatewayProtocolFailure("terminals-unavailable");
|
|
50
|
+
return request.method === "terminals.state" ? this.#terminals.state(String(request.params.sessionRef))
|
|
51
|
+
: this.#terminals.execute(parseTerminalCommand(request.params.command));
|
|
52
|
+
}
|
|
41
53
|
const catalog = await this.#catalog();
|
|
42
54
|
if (catalog.state !== "ready") throw new GatewayProtocolFailure(catalog.reasonCode ?? "catalog-unavailable", true);
|
|
43
55
|
if (request.method === "sessions.get") {
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { attachTerminalMentions } from "./terminal-mentions.ts";
|
|
2
|
+
import { PersonalTerminalSessions } from "./personal-terminal-sessions.ts";
|
|
3
|
+
import { ManagedTerminalSessions } from "./managed-terminal-sessions.ts";
|
|
4
|
+
import type { ManagedTerminalRuntime } from "./managed-terminal-binding.ts";
|
|
5
|
+
import { terminalRouter } from "./terminal-router.ts";
|
|
6
|
+
import { ManagedSourceRevertSessions, type ManagedSourceRuntime } from "./managed-source-revert-sessions.ts";
|
|
7
|
+
import { SourceRevertSessions } from "./source-revert-sessions.ts";
|
|
8
|
+
import { WorkspaceWriteSessions, type ManagedWorkspaceWriter } from './workspace-write-sessions.ts';
|
|
9
|
+
import { CheckpointSessions, type ManagedCheckpointRuntime } from './checkpoint-sessions.ts';
|
|
1
10
|
import { managedProjection } from './managed-projection.ts';
|
|
2
11
|
import { randomBytes } from "node:crypto";
|
|
3
12
|
import fs from "node:fs";
|
|
@@ -100,7 +109,7 @@ export async function startPiagentGateway(options: {
|
|
|
100
109
|
agentDir?: string;
|
|
101
110
|
staticRoot?: string;
|
|
102
111
|
scopedBrokerRouter?: ScopedBrokerRouter;
|
|
103
|
-
managed?: { host: any; models: any; runtimeFactory: RuntimeFactory; project: string | null };
|
|
112
|
+
managed?: { host: any; models: any; runtimeFactory: RuntimeFactory; project: string | null; managedCheckpoint?: (id: string) => ManagedCheckpointRuntime; managedTerminal?: (id: string) => ManagedTerminalRuntime; managedSource?: (id: string) => ManagedSourceRuntime; workspaceWrite?: ManagedWorkspaceWriter; workspaceRead?: (id: string, request: { kind: string; path: string }) => Promise<unknown> };
|
|
104
113
|
// Company sessions relayed into this dashboard (the dashboard entry passes
|
|
105
114
|
// the Agent Watch binding; omitted: none).
|
|
106
115
|
company?: CompanyConnector;
|
|
@@ -118,6 +127,7 @@ export async function startPiagentGateway(options: {
|
|
|
118
127
|
let loopback: Awaited<ReturnType<typeof startLoopbackServer>> | null = null;
|
|
119
128
|
let control: Awaited<ReturnType<typeof startGatewayControlSocket>> | null = null;
|
|
120
129
|
let runtimes: SessionRuntimeSupervisor | null = null;
|
|
130
|
+
let terminals: PersonalTerminalSessions | ManagedTerminalSessions | null = null;
|
|
121
131
|
let mcpAuth: McpAuthBroker | null = null;
|
|
122
132
|
let attachments: SessionAttachmentRegistry | null = null;
|
|
123
133
|
let relay: CompanyRelay | null = null;
|
|
@@ -138,6 +148,7 @@ export async function startPiagentGateway(options: {
|
|
|
138
148
|
// run once this process is gone.
|
|
139
149
|
try { attachments?.close(); } catch { /* shutdown never fails on cleanup */ }
|
|
140
150
|
await mcpAuth?.close().catch(() => undefined);
|
|
151
|
+
await terminals?.dispose().catch(() => undefined);
|
|
141
152
|
await runtimes?.close().catch(() => undefined);
|
|
142
153
|
await control?.close().catch(() => undefined);
|
|
143
154
|
settleWait?.();
|
|
@@ -187,10 +198,13 @@ export async function startPiagentGateway(options: {
|
|
|
187
198
|
gatewayInstanceRef, key, leases, listSessions: cachedSessionLister((dir) => host.SessionManager.listAll(dir), path.join(state.agentDir, 'sessions'), Boolean(options.managed)),
|
|
188
199
|
host, agentDir: state.agentDir, packageRoot: options.packageRoot, modelRuntime: inspectionModels, events,
|
|
189
200
|
runtimeFactory: options.managed?.runtimeFactory,
|
|
201
|
+
closeSessionResources: async sessionRef => { await terminals?.closeSession(sessionRef); },
|
|
190
202
|
sessionDirectory: options.managed ? path.join(state.agentDir, 'sessions') : undefined,
|
|
191
203
|
scopedBrokerRouter: options.scopedBrokerRouter,
|
|
192
204
|
resolveProject: (projectRef) => projects.resolve(projectRef)
|
|
193
205
|
});
|
|
206
|
+
terminals = options.managed ? new ManagedTerminalSessions({ key, gatewayInstanceRef, managed: true, runtimes,
|
|
207
|
+
managedTerminal: options.managed.managedTerminal }) : new PersonalTerminalSessions({ key, gatewayInstanceRef, managed: false, runtimes });
|
|
194
208
|
const sessionFacts = (manager: { buildSessionContext(): any; getBranch(): any[] }) => {
|
|
195
209
|
const context = manager.buildSessionContext();
|
|
196
210
|
return { model: context.model ? { provider: String(context.model.provider), modelId: String(context.model.modelId) } : null,
|
|
@@ -247,14 +261,18 @@ export async function startPiagentGateway(options: {
|
|
|
247
261
|
// controller, because it reads sessions through the inspection registry.
|
|
248
262
|
prepareAttachments: (sessionRef, refs, messageRequestId, text) => {
|
|
249
263
|
if (!attachments) throw new Error("session-attachment-unavailable");
|
|
250
|
-
return attachments.reserveForPrompt(sessionRef, refs, messageRequestId, text);
|
|
264
|
+
return attachments.reserveForPrompt(sessionRef, refs, messageRequestId, attachTerminalMentions(sessionRef, text, terminals));
|
|
251
265
|
} });
|
|
252
266
|
const runtimeCommands = new RuntimeCommandController({ catalog: readCatalog, runtimes, events });
|
|
267
|
+
const routedTerminals = terminalRouter(terminals, relay);
|
|
253
268
|
const protocol = new GatewayProtocolService({ capabilities: () => capabilities(gatewayInstanceRef, true, !!options.managed), catalog: hubCatalog,
|
|
254
|
-
events, command: relay ? { execute: (command) => relay!.handles(command)
|
|
269
|
+
events, terminals: routedTerminals, terminalEvents: routedTerminals, command: relay ? { execute: (command) => relay!.handles(command)
|
|
255
270
|
? relay!.command(command as Record<string, unknown>, async () => (await hubCatalog()).catalogRevision) : commands.execute(command) } : commands });
|
|
256
271
|
const inspections = new SessionInspectionRegistry({ gatewayInstanceRef, host, key, packageRoot: options.packageRoot, agentDir: state.agentDir,
|
|
257
|
-
models: inspectionModels,
|
|
272
|
+
models: inspectionModels, managed: !!options.managed,
|
|
273
|
+
workspaceRead: options.managed?.workspaceRead ? async (sessionRef, id, kind, path) => {
|
|
274
|
+
await runtimes!.acquire(sessionRef); return options.managed!.workspaceRead!(id, { kind, path });
|
|
275
|
+
} : undefined, projects, mcpAuth, listSessions: () => runtimes!.listSessions(),
|
|
258
276
|
openLiveSession: (sessionRef) => runtimes!.liveSessionManager(sessionRef),
|
|
259
277
|
operationLiveness: (sessionRef) => runtimes!.currentOperation(sessionRef) ? "running" : "idle" });
|
|
260
278
|
// Staged bytes live beside the inspection projection they were checked
|
|
@@ -266,7 +284,33 @@ export async function startPiagentGateway(options: {
|
|
|
266
284
|
updates = new UpdateCenter({ packageRoot: options.packageRoot, piVersion: options.expectedPiVersion, managed: Boolean(options.managed),
|
|
267
285
|
busy: async () => runtimes!.currentOperations().length + (relay ? (await relay.operations()).length : 0) });
|
|
268
286
|
updates.start();
|
|
287
|
+
const workspaceWrites = new WorkspaceWriteSessions({ root: state.root, key, gatewayInstanceRef, runtimes, managed: !!options.managed,
|
|
288
|
+
managedWrite: options.managed?.workspaceWrite, changed: sessionRef => events.publish("session.changed", { sessionRef }) });
|
|
289
|
+
const checkpoints = new CheckpointSessions({ root: state.root, key, gatewayInstanceRef, runtimes, managed: !!options.managed,
|
|
290
|
+
managedCheckpoint: options.managed?.managedCheckpoint,
|
|
291
|
+
changed: sessionRef => events.publish("session.changed", { sessionRef }) });
|
|
292
|
+
const sourceReverts = options.managed ? new ManagedSourceRevertSessions({ root: state.root, key, gatewayInstanceRef, runtimes,
|
|
293
|
+
managedSource: options.managed.managedSource, changed: sessionRef => events.publish("session.changed", { sessionRef }) }) : new SourceRevertSessions({ root: state.root, key, runtimes, managed: !!options.managed,
|
|
294
|
+
provider: sessionRef => inspections.provider(sessionRef), changed: sessionRef => events.publish("session.changed", { sessionRef }) });
|
|
269
295
|
loopback = await startLoopbackServer({
|
|
296
|
+
checkpoints: {
|
|
297
|
+
state: sessionRef => company(sessionRef) ? relay!.json("GET", `/api/v1/sessions/${encodeURIComponent(sessionRef)}/checkpoints`) : checkpoints.state(sessionRef),
|
|
298
|
+
preview: (sessionRef, target) => company(sessionRef) ? relay!.json("GET", `/api/v1/sessions/${encodeURIComponent(sessionRef)}/checkpoints/${encodeURIComponent(target)}`) : checkpoints.preview(sessionRef, target),
|
|
299
|
+
execute: (sessionRef, command) => company(sessionRef) ? relayPost(`/api/v1/sessions/${encodeURIComponent(sessionRef)}/checkpoints`, command) : checkpoints.execute(sessionRef, command)
|
|
300
|
+
},
|
|
301
|
+
sessionSourceRevert: {
|
|
302
|
+
sources: sessionRef => company(sessionRef) ? relay!.json("GET", `/api/v1/sessions/${encodeURIComponent(sessionRef)}/source-review`) : sourceReverts.sources(sessionRef),
|
|
303
|
+
diff: (sessionRef, fileRef) => company(sessionRef) ? relay!.json("GET", `/api/v1/sessions/${encodeURIComponent(sessionRef)}/source-review/${encodeURIComponent(fileRef)}`) : sourceReverts.diff(sessionRef, fileRef),
|
|
304
|
+
preview: (sessionRef, fileRef, hunkRef) => company(sessionRef)
|
|
305
|
+
? relay!.json("GET", `/api/v1/sessions/${encodeURIComponent(sessionRef)}/source-reverts/${encodeURIComponent(fileRef)}${hunkRef ? `?hunkRef=${encodeURIComponent(hunkRef)}` : ""}`)
|
|
306
|
+
: sourceReverts.preview(sessionRef, fileRef, hunkRef),
|
|
307
|
+
execute: (sessionRef, command) => company(sessionRef)
|
|
308
|
+
? relayPost(`/api/v1/sessions/${encodeURIComponent(sessionRef)}/source-reverts`, command) : sourceReverts.execute(sessionRef, command)
|
|
309
|
+
},
|
|
310
|
+
workspaceWrite: {
|
|
311
|
+
state: sessionRef => company(sessionRef) ? relay!.json("GET", `/api/v1/sessions/${encodeURIComponent(sessionRef)}/workspace/write`) : workspaceWrites.state(sessionRef),
|
|
312
|
+
execute: (sessionRef, command) => company(sessionRef) ? relayPost(`/api/v1/sessions/${encodeURIComponent(sessionRef)}/workspace/write`, command) : workspaceWrites.execute(sessionRef, command)
|
|
313
|
+
},
|
|
270
314
|
staticRoot,
|
|
271
315
|
mode: "gateway",
|
|
272
316
|
readCapabilities: () => capabilities(gatewayInstanceRef, true, !!options.managed),
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { managedThinkingLevels, nearestLevel } from '../../piagent-core/managed/capabilities.mjs';
|
|
2
2
|
import { ManagedBrokerClient } from '../../piagent-core/managed/broker-client.mjs';
|
|
3
3
|
import { ManagedSession } from '../../piagent-core/managed/session.mjs';
|
|
4
|
+
import { checkpointsOf } from '../../piagent-core/managed/checkpoints.mjs';
|
|
4
5
|
import { fastAllowed } from '../../piagent-core/managed/fast-mode.mjs';
|
|
6
|
+
import { bypassAllowed } from '../../piagent-core/managed/permission.mjs';
|
|
5
7
|
import { PI_HOST_VERSION } from '../../piagent-core/managed/pi-host.mjs';
|
|
6
8
|
import fs from 'node:fs';
|
|
7
9
|
|
|
8
10
|
// A company model as the new chat page sees it: the thinking levels its
|
|
9
11
|
// Harness allows, and whether the Harness allows Fast mode (the page shows
|
|
10
12
|
// the Fast switch only then).
|
|
11
|
-
export const managedModelOption=(manifest,models,model)=>({...model,managedThinkingLevels:managedThinkingLevels(manifest,models,model),managedFast:fastAllowed(manifest)});
|
|
13
|
+
export const managedModelOption=(manifest,models,model)=>({...model,managedThinkingLevels:managedThinkingLevels(manifest,models,model),managedFast:fastAllowed(manifest),managedBypass:bypassAllowed(manifest)});
|
|
12
14
|
|
|
13
15
|
// Only called by the pinned launcher with Watch's non-secret import manifest.
|
|
14
16
|
// Each web session owns its own broker; no personal auth/model/resource loader.
|
|
@@ -30,6 +32,7 @@ export async function startManagedGateway({ config, configPath, cwd, agentDir, p
|
|
|
30
32
|
const host=probe.api,models=probe.modelRuntime;
|
|
31
33
|
const snapshot=models.getAvailableSnapshot.bind(models);
|
|
32
34
|
models.getAvailableSnapshot=()=>snapshot().map(model=>managedModelOption(probe.manifest,models,model));
|
|
35
|
+
const workspaceRuntimes = new Map();
|
|
33
36
|
const factory=async(info,_instance,manager,initial)=>{
|
|
34
37
|
if(initial?.modelRef && initial.modelRef!==webUiModelRef('agent_watch_managed','agent-watch-auto')) throw Error('managed-personal-switch-requires-new-session');
|
|
35
38
|
const broker=newBroker();
|
|
@@ -41,10 +44,34 @@ export async function startManagedGateway({ config, configPath, cwd, agentDir, p
|
|
|
41
44
|
// changed it: the conversation takes the nearest level the Harness allows now.
|
|
42
45
|
if(initial) runtime.session.setThinkingLevel(nearestLevel(initial.thinkingLevel,managedThinkingLevels(runtime.manifest,runtime.modelRuntime,runtime.session.model)));
|
|
43
46
|
await runtime.session.bindExtensions({mode:'rpc',uiContext:rpcUiContext()});
|
|
44
|
-
|
|
47
|
+
workspaceRuntimes.set(info.id, runtime);
|
|
48
|
+
return {session:runtime.session,dispose:async()=>{ workspaceRuntimes.delete(info.id); await runtime.dispose(); }};
|
|
45
49
|
} catch(error) { if(runtime) await runtime.dispose(); else await broker.dispose(); throw error; }
|
|
46
50
|
};
|
|
47
|
-
const gateway=await startPiagentGateway({packageRoot,agentDir,expectedPiVersion:PI_HOST_VERSION,managed:{host,models,runtimeFactory:factory,project:registerProject?cwd:null
|
|
51
|
+
const gateway=await startPiagentGateway({packageRoot,agentDir,expectedPiVersion:PI_HOST_VERSION,managed:{host,models,runtimeFactory:factory,project:registerProject?cwd:null,
|
|
52
|
+
managedCheckpoint: id => {
|
|
53
|
+
const runtime = workspaceRuntimes.get(id); if (!runtime) throw Error('checkpoint-runtime-unavailable');
|
|
54
|
+
const boundary = runtime.boundary, checkpoints = checkpointsOf(runtime);
|
|
55
|
+
const current = () => workspaceRuntimes.get(id) === runtime && runtime.boundary === boundary;
|
|
56
|
+
const check = () => { if (!current()) throw Error('checkpoint-runtime-replaced'); };
|
|
57
|
+
return { current, writable: async () => { check(); const state = await boundary.invoke('workspace', { kind: 'write-state' }); check(); return state.writable === true; },
|
|
58
|
+
port: { list: () => { check(); return checkpoints.list(); }, preview: target => { check(); return checkpoints.preview(target); },
|
|
59
|
+
restoreTo: (target, expected, guard) => checkpoints.restoreTo(target, expected, () => { check(); guard?.(); }) } };
|
|
60
|
+
},
|
|
61
|
+
managedTerminal: id => {
|
|
62
|
+
const runtime = workspaceRuntimes.get(id); if (!runtime) throw Error('terminal-runtime-unavailable');
|
|
63
|
+
const boundary = runtime.boundary;
|
|
64
|
+
return { terminals: boundary.terminals, current: () => workspaceRuntimes.get(id) === runtime && runtime.boundary === boundary };
|
|
65
|
+
},
|
|
66
|
+
managedSource: id => {
|
|
67
|
+
const runtime = workspaceRuntimes.get(id); if (!runtime) throw Error('source-runtime-unavailable');
|
|
68
|
+
const boundary = runtime.boundary;
|
|
69
|
+
return { controlRevision: boundary.sourceControlRevision,
|
|
70
|
+
current: () => workspaceRuntimes.get(id) === runtime && runtime.boundary === boundary,
|
|
71
|
+
source: (request, binding) => boundary.source(request, binding) };
|
|
72
|
+
},
|
|
73
|
+
workspaceWrite: (id, request) => { const runtime = workspaceRuntimes.get(id); if (!runtime) throw Error('workspace-runtime-unavailable'); return runtime.boundary.invoke('workspace', request); },
|
|
74
|
+
workspaceRead: (id, request) => { const runtime = workspaceRuntimes.get(id); if (!runtime) throw Error('workspace-runtime-unavailable'); return runtime.boundary.invoke('workspace', request); }}});
|
|
48
75
|
return gateway;
|
|
49
76
|
} finally { if(probe) await probe.dispose(); else await probeBroker.dispose(); }
|
|
50
77
|
}
|
|
@@ -30,11 +30,13 @@ export function managedProjection(context: any, entries: any[]) {
|
|
|
30
30
|
// Helpers the Harness enables (up to four); a conversation recorded before
|
|
31
31
|
// that was known says two.
|
|
32
32
|
const maximum = Number.isInteger(counter?.maximum) && counter.maximum >= 0 && counter.maximum <= 4 ? counter.maximum : 2;
|
|
33
|
-
// The member's access choice
|
|
34
|
-
|
|
33
|
+
// The member's access choice; Bypass only while the Harness allows it, else
|
|
34
|
+
// it applies as Full access (permission.mjs).
|
|
35
|
+
const chosen = last('agent-watch-permission')?.mode, bypassAllowed = last('agent-watch-bypass')?.allowed === true;
|
|
36
|
+
const permission = chosen === 'bypass' ? (bypassAllowed ? 'bypass' : 'trusted-full-access') : chosen === 'trusted-full-access' ? 'trusted-full-access' : 'workspace-write';
|
|
35
37
|
// Fast mode, only where the conversation's Harness allows it (fast-mode.mjs).
|
|
36
38
|
const fast = last('agent-watch-fast');
|
|
37
|
-
return {modelLabel:'agent-watch-auto', managedPermission: permission, ...(fast?.allowed === true ? {managedFast: fast.enabled === true ? 'on' as const : 'off' as const} : {}), managedHelpers:{active:Number.isInteger(counter?.active) && counter.active>=0 && counter.active<=maximum ? counter.active : 0, maximum},
|
|
39
|
+
return {modelLabel:'agent-watch-auto', managedPermission: permission, ...(bypassAllowed ? {managedBypass: true as const} : {}), ...(fast?.allowed === true ? {managedFast: fast.enabled === true ? 'on' as const : 'off' as const} : {}), managedHelpers:{active:Number.isInteger(counter?.active) && counter.active>=0 && counter.active<=maximum ? counter.active : 0, maximum},
|
|
38
40
|
...(Array.isArray(capabilities)?{managedThinkingLevels:levels.filter(level=>capabilities.includes(level))}:{}),
|
|
39
41
|
...(steps ? {managedPlan:{steps}} : {}), ...(process ? {managedProcess:process} : {}), ...(turnEnd ? {managedTurnEnd:turnEnd} : {}), ...(review ? {managedReview:review} : {})};
|
|
40
42
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { BridgeSnapshot, BridgeRevisions } from "../../piagent-core/runtime/inspection/session-identity.ts";
|
|
2
|
+
import type { SourceRevertProjection, SourceRevertTarget, SourceIndexTransactionResult } from "../../piagent-core/runtime/inspection/source-action-contract.ts";
|
|
3
|
+
import { SourceRevertController, type SourceRevertEvidenceStore } from "../server/source-revert-controller.ts";
|
|
4
|
+
|
|
5
|
+
export type ManagedSourceRequest = { kind: "preview"; fileRef: string; hunkRefs: string[] }
|
|
6
|
+
| { kind: "execute"; target: SourceRevertTarget } | { kind: "sources" } | { kind: "diff"; fileRef: string };
|
|
7
|
+
export type ManagedSourceBinding = {
|
|
8
|
+
identity: NonNullable<BridgeSnapshot["identity"]>; rawSessionId: string; controlRevision: string;
|
|
9
|
+
current(): boolean; idle(): boolean;
|
|
10
|
+
};
|
|
11
|
+
export type ManagedSourcePort = (request: ManagedSourceRequest, binding: ManagedSourceBinding) => Promise<unknown>;
|
|
12
|
+
|
|
13
|
+
// The company Gateway owns receipts and live session checks. Only the worker
|
|
14
|
+
// resolves a preview into filesystem authority; no patch bytes cross this port.
|
|
15
|
+
export function managedSourceRevertController(options: {
|
|
16
|
+
projectRoot: string; binding: ManagedSourceBinding; snapshot(): BridgeSnapshot;
|
|
17
|
+
revisions(): Promise<BridgeRevisions>; evidence: SourceRevertEvidenceStore; source: ManagedSourcePort;
|
|
18
|
+
}) {
|
|
19
|
+
const { binding, source } = options;
|
|
20
|
+
const preview = async (fileRef: string, hunkRefs: string[]) => {
|
|
21
|
+
if (!binding.current() || !binding.idle()) throw new Error("source-session-unavailable");
|
|
22
|
+
const value = await source({ kind: "preview", fileRef, hunkRefs }, binding) as SourceRevertProjection;
|
|
23
|
+
if (!value || value.version !== "piagent-webui-source-revert-v1"
|
|
24
|
+
|| ["projectRef", "sessionRef", "runtimeInstanceId", "taskId", "taskRunId"].some(key =>
|
|
25
|
+
value.identity?.[key as keyof typeof value.identity] !== binding.identity[key as keyof typeof binding.identity]))
|
|
26
|
+
throw new Error("source-preview-binding-mismatch");
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
const controller = new SourceRevertController<SourceRevertTarget>({ projectRoot: options.projectRoot,
|
|
30
|
+
allowSessionReverts: true, bridge: { snapshot: options.snapshot }, evidence: options.evidence, revisions: options.revisions,
|
|
31
|
+
resolve: async (fileRef, hunks) => {
|
|
32
|
+
const projection = await preview(fileRef, hunks);
|
|
33
|
+
return { projection, authority: projection.state === "ready" ? projection.target : null };
|
|
34
|
+
},
|
|
35
|
+
mutate: async request => {
|
|
36
|
+
const result = (state: "rejected" | "uncertain", reasonCode: string): SourceIndexTransactionResult => ({ state, reasonCode,
|
|
37
|
+
beforeIndexPreimage: request.expectedIndexPreimage, beforeWorkspacePreimage: request.expectedWorkspacePreimage,
|
|
38
|
+
afterIndexPreimage: null, afterWorkspacePreimage: null, executor: "pi-guard", directExecution: false });
|
|
39
|
+
if (!binding.current() || !binding.idle() || request.expectedControlRevision !== binding.controlRevision)
|
|
40
|
+
return result("rejected", "source-session-unavailable");
|
|
41
|
+
try { return await source({ kind: "execute", target: request.authority }, binding) as SourceIndexTransactionResult; }
|
|
42
|
+
catch { return result("uncertain", "source-boundary-effect-unknown"); }
|
|
43
|
+
} });
|
|
44
|
+
return { preview, execute: (command: unknown) => controller.execute(command) };
|
|
45
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { webUiProjectRef, webUiSessionRef } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
|
|
3
|
+
import type { BridgeSnapshot, BridgeRevisions } from "../../piagent-core/runtime/inspection/session-identity.ts";
|
|
4
|
+
import type { SourceChangeViews } from "../../piagent-core/runtime/inspection/source-change-projection.ts";
|
|
5
|
+
import { managedSourceRevertController, type ManagedSourcePort, type ManagedSourceRequest } from "./managed-source-revert-controller.ts";
|
|
6
|
+
import { SourceRevertStore } from "./source-revert-store.ts";
|
|
7
|
+
import { sessionRefForPath } from "./session-catalog.ts";
|
|
8
|
+
import { inspectionRuntimeRef } from "./session-inspection-registry.ts";
|
|
9
|
+
import type { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
|
|
10
|
+
|
|
11
|
+
export type ManagedSourceRuntime = { controlRevision: string; current(): boolean; source: ManagedSourcePort };
|
|
12
|
+
type Options = { root: string; key: Buffer; gatewayInstanceRef: string;
|
|
13
|
+
runtimes: Pick<SessionRuntimeSupervisor, "listSessions" | "acquire" | "liveSessionManager" | "currentOperation">;
|
|
14
|
+
managedSource?(id: string): ManagedSourceRuntime; changed(sessionRef: string): void };
|
|
15
|
+
|
|
16
|
+
export class ManagedSourceRevertSessions {
|
|
17
|
+
readonly #options: Options; readonly #store: SourceRevertStore;
|
|
18
|
+
readonly #pending = new Map<string, Promise<unknown>>();
|
|
19
|
+
constructor(options: Options) { this.#options = options; this.#store = new SourceRevertStore(options.root, options.key); }
|
|
20
|
+
#serial<T>(sessionRef: string, action: () => Promise<T>): Promise<T> {
|
|
21
|
+
const run = (this.#pending.get(sessionRef) ?? Promise.resolve()).then(action, action);
|
|
22
|
+
this.#pending.set(sessionRef, run);
|
|
23
|
+
void run.finally(() => { if (this.#pending.get(sessionRef) === run) this.#pending.delete(sessionRef); }).catch(() => {});
|
|
24
|
+
return run;
|
|
25
|
+
}
|
|
26
|
+
async #controller(sessionRef: string) {
|
|
27
|
+
const { runtimes, key, managedSource, gatewayInstanceRef } = this.#options;
|
|
28
|
+
if (!managedSource) throw new Error("source-company-boundary-required");
|
|
29
|
+
const matches = (await runtimes.listSessions()).filter(info => sessionRefForPath(key, info.path) === sessionRef);
|
|
30
|
+
if (matches.length !== 1) throw new Error("source-session-unavailable");
|
|
31
|
+
const info = matches[0]; await runtimes.acquire(sessionRef);
|
|
32
|
+
const manager = runtimes.liveSessionManager(sessionRef), runtime = managedSource(info.id);
|
|
33
|
+
if (!manager || manager.getSessionId() !== info.id || !runtime.current()) throw new Error("source-session-unavailable");
|
|
34
|
+
const identity = { projectRef: webUiProjectRef(info.cwd), sessionRef: webUiSessionRef(info.id),
|
|
35
|
+
runtimeInstanceId: inspectionRuntimeRef(gatewayInstanceRef, sessionRef), taskId: null, taskRunId: null, agentOperationId: null, toolCallId: null };
|
|
36
|
+
const binding = { identity, rawSessionId: info.id, controlRevision: runtime.controlRevision,
|
|
37
|
+
current: () => runtimes.liveSessionManager(sessionRef) === manager && manager.getSessionId() === info.id && runtime.current(),
|
|
38
|
+
idle: () => !runtimes.currentOperation(sessionRef) };
|
|
39
|
+
const commandBinding = { controlRevision: binding.controlRevision,
|
|
40
|
+
runtimeRevision: `source-runtime.${createHash("sha256").update(JSON.stringify([identity, binding.controlRevision])).digest("hex")}` };
|
|
41
|
+
const revisions: BridgeRevisions = { ...commandBinding, taskRevision: null, workspaceRevision: null, indexRevision: null,
|
|
42
|
+
approvalRevision: null, sessionOptionRevision: null, queueRevision: null };
|
|
43
|
+
const snapshot = (): BridgeSnapshot => ({ state: binding.current() ? "ready" : "replacement-pending", identity, revisions,
|
|
44
|
+
taskState: "none", liveness: binding.idle() ? "idle" : "running", eventSequence: 0 });
|
|
45
|
+
const controller = managedSourceRevertController({ projectRoot: info.cwd, binding, snapshot, source: runtime.source,
|
|
46
|
+
evidence: this.#store.forSession(info.cwd, identity.projectRef, identity.sessionRef), revisions: async () => {
|
|
47
|
+
const views = await runtime.source({ kind: "sources" }, binding) as SourceChangeViews;
|
|
48
|
+
return { ...revisions, workspaceRevision: views.workingTree.viewRevision, indexRevision: views.staged.viewRevision };
|
|
49
|
+
} });
|
|
50
|
+
return { controller, commandBinding, read: (request: ManagedSourceRequest) => runtime.source(request, binding) };
|
|
51
|
+
}
|
|
52
|
+
sources(sessionRef: string) {
|
|
53
|
+
return this.#serial(sessionRef, async () => {
|
|
54
|
+
const { read } = await this.#controller(sessionRef);
|
|
55
|
+
return (await read({ kind: "sources" }) as SourceChangeViews).workingTree;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
diff(sessionRef: string, fileRef: string) {
|
|
59
|
+
return this.#serial(sessionRef, async () => (await this.#controller(sessionRef)).read({ kind: "diff", fileRef }));
|
|
60
|
+
}
|
|
61
|
+
preview(sessionRef: string, fileRef: string, hunkRef: string | null) {
|
|
62
|
+
return this.#serial(sessionRef, async () => {
|
|
63
|
+
const { controller, commandBinding } = await this.#controller(sessionRef);
|
|
64
|
+
return { ...await controller.preview(fileRef, hunkRef ? [hunkRef] : []), commandBinding };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
execute(sessionRef: string, command: unknown) {
|
|
68
|
+
return this.#serial(sessionRef, async () => {
|
|
69
|
+
const { controller } = await this.#controller(sessionRef), receipt = await controller.execute(command);
|
|
70
|
+
if (receipt.phase !== "rejected") this.#options.changed(sessionRef);
|
|
71
|
+
return receipt;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { sessionRefForPath } from './session-catalog.ts';
|
|
2
|
+
import type { SessionRuntimeSupervisor } from './session-runtime-supervisor.ts';
|
|
3
|
+
|
|
4
|
+
export type ManagedTerminalEvent = { kind: 'output'; data: string } | { kind: 'exit'; exitCode: number | null; signal: number | null };
|
|
5
|
+
export type ManagedTerminalHandle = { id: string; input(data: string): Promise<void>;
|
|
6
|
+
resize(size: { cols: number; rows: number }): Promise<void>; close(): Promise<void> };
|
|
7
|
+
export type ManagedTerminalRuntime = { current(): boolean; terminals: { reasonCode: string | null;
|
|
8
|
+
open(size: { cols: number; rows: number }, emit: (event: ManagedTerminalEvent) => void): Promise<ManagedTerminalHandle> } };
|
|
9
|
+
export type ManagedTerminalOptions = { key: Buffer; gatewayInstanceRef: string; managed: boolean;
|
|
10
|
+
runtimes: Pick<SessionRuntimeSupervisor, 'listSessions' | 'acquire' | 'liveSessionManager' | 'ownership'>;
|
|
11
|
+
managedTerminal?(rawSessionId: string): ManagedTerminalRuntime };
|
|
12
|
+
|
|
13
|
+
export async function managedTerminalBinding(options: ManagedTerminalOptions, sessionRef: string) {
|
|
14
|
+
if (!options.managed || !options.managedTerminal) throw Error('terminal-company-boundary-required');
|
|
15
|
+
const { runtimes } = options;
|
|
16
|
+
const matches = (await runtimes.listSessions()).filter(info => sessionRefForPath(options.key, info.path) === sessionRef);
|
|
17
|
+
if (matches.length !== 1) throw Error('terminal-session-unavailable');
|
|
18
|
+
const info = matches[0]!, lease = await runtimes.acquire(sessionRef), manager = runtimes.liveSessionManager(sessionRef);
|
|
19
|
+
const runtimeInstanceRef = lease.runtimeInstanceRef, runtime = options.managedTerminal(info.id);
|
|
20
|
+
if (!runtimeInstanceRef || !manager || manager.getSessionId() !== info.id || !runtime.current()) throw Error('terminal-session-unavailable');
|
|
21
|
+
const assertCurrent = () => {
|
|
22
|
+
const ownership = runtimes.ownership(sessionRef);
|
|
23
|
+
if (runtimes.liveSessionManager(sessionRef) !== manager || manager.getSessionId() !== info.id || !runtime.current()
|
|
24
|
+
|| ownership.state !== 'gateway-owned' || ownership.owner.continuity !== 'exact'
|
|
25
|
+
|| ownership.owner.gatewayInstanceRef !== options.gatewayInstanceRef || ownership.owner.runtimeInstanceRef !== runtimeInstanceRef
|
|
26
|
+
|| ownership.owner.ownerEpoch !== lease.ownerEpoch) throw Error('terminal-runtime-replaced');
|
|
27
|
+
};
|
|
28
|
+
assertCurrent();
|
|
29
|
+
return { identity: { gatewayInstanceRef: options.gatewayInstanceRef, sessionRef, runtimeInstanceRef }, runtime, assertCurrent };
|
|
30
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { managedTerminalBinding, ManagedTerminalHandle, ManagedTerminalEvent } from './managed-terminal-binding.ts';
|
|
3
|
+
import type { TerminalProcessEvent, TerminalProcessOwner } from './terminal-sessions.ts';
|
|
4
|
+
|
|
5
|
+
type Binding = Awaited<ReturnType<typeof managedTerminalBinding>>;
|
|
6
|
+
type Entry = { binding: Binding; terminalRef: string; handle: ManagedTerminalHandle | null; active: boolean;
|
|
7
|
+
pending: ManagedTerminalEvent[]; bytes: number; failed: boolean };
|
|
8
|
+
|
|
9
|
+
// This adapter has no shell/native import. Effects only reach the captured
|
|
10
|
+
// managed runtime's boundary, even if that boundary reports unavailable.
|
|
11
|
+
export class ManagedTerminalProcesses implements TerminalProcessOwner<Binding> {
|
|
12
|
+
readonly #entries = new Map<string, Entry>();
|
|
13
|
+
readonly #emit: (event: TerminalProcessEvent) => void;
|
|
14
|
+
#closed = false;
|
|
15
|
+
constructor(emit: (event: TerminalProcessEvent) => void) { this.#emit = emit; }
|
|
16
|
+
reason(binding: Binding) { return binding.runtime.terminals.reasonCode; }
|
|
17
|
+
async open(binding: Binding, size: { cols: number; rows: number }) {
|
|
18
|
+
binding.assertCurrent();
|
|
19
|
+
if (this.#closed) throw Error('terminal-session-closed');
|
|
20
|
+
if (this.#entries.size >= 256 || [...this.#entries.values()].filter(entry => entry.binding.identity.sessionRef === binding.identity.sessionRef).length >= 4)
|
|
21
|
+
throw Error('terminal-limit');
|
|
22
|
+
const terminalRef = `terminal.${randomUUID()}`;
|
|
23
|
+
const entry: Entry = { binding, terminalRef, handle: null, active: false, pending: [], bytes: 0, failed: false };
|
|
24
|
+
this.#entries.set(terminalRef, entry);
|
|
25
|
+
try {
|
|
26
|
+
entry.handle = await binding.runtime.terminals.open(size, event => {
|
|
27
|
+
if (entry.failed) return;
|
|
28
|
+
if (entry.active) { this.#publish(entry, event); return; }
|
|
29
|
+
entry.bytes += event.kind === 'output' ? Buffer.byteLength(event.data) : 64;
|
|
30
|
+
if (entry.bytes > 512 * 1024 || entry.pending.length >= 256) { entry.failed = true; entry.pending = []; return; }
|
|
31
|
+
entry.pending.push(event);
|
|
32
|
+
});
|
|
33
|
+
binding.assertCurrent();
|
|
34
|
+
if (entry.failed || this.#closed) throw Error('terminal-start-unavailable');
|
|
35
|
+
return { terminalRef, activate: () => {
|
|
36
|
+
entry.active = true;
|
|
37
|
+
for (const event of entry.pending) this.#publish(entry, event);
|
|
38
|
+
entry.pending = []; entry.bytes = 0;
|
|
39
|
+
} };
|
|
40
|
+
} catch (error) {
|
|
41
|
+
try { await entry.handle?.close(); } finally { this.#entries.delete(terminalRef); }
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
#publish(entry: Entry, event: ManagedTerminalEvent) {
|
|
46
|
+
try { entry.binding.assertCurrent(); } catch { return; }
|
|
47
|
+
this.#emit({ ...event, sessionRef: entry.binding.identity.sessionRef, terminalRef: entry.terminalRef });
|
|
48
|
+
}
|
|
49
|
+
#get(ref: string, terminalRef: string) {
|
|
50
|
+
const entry = this.#entries.get(terminalRef);
|
|
51
|
+
if (!entry?.handle || entry.binding.identity.sessionRef !== ref) throw Error('terminal-not-found');
|
|
52
|
+
return entry;
|
|
53
|
+
}
|
|
54
|
+
async input(ref: string, terminalRef: string, data: string) {
|
|
55
|
+
const entry = this.#get(ref, terminalRef); entry.binding.assertCurrent();
|
|
56
|
+
await entry.handle!.input(data);
|
|
57
|
+
}
|
|
58
|
+
async resize(ref: string, terminalRef: string, size: { cols: number; rows: number }) {
|
|
59
|
+
const entry = this.#get(ref, terminalRef); entry.binding.assertCurrent();
|
|
60
|
+
await entry.handle!.resize(size);
|
|
61
|
+
}
|
|
62
|
+
async close(ref: string, terminalRef: string) {
|
|
63
|
+
const entry = this.#get(ref, terminalRef);
|
|
64
|
+
await entry.handle!.close(); this.#entries.delete(terminalRef);
|
|
65
|
+
}
|
|
66
|
+
async closeSession(ref: string) {
|
|
67
|
+
await Promise.all([...this.#entries.values()].filter(entry => entry.binding.identity.sessionRef === ref)
|
|
68
|
+
.map(entry => this.close(ref, entry.terminalRef)));
|
|
69
|
+
}
|
|
70
|
+
async dispose() {
|
|
71
|
+
this.#closed = true;
|
|
72
|
+
await Promise.all([...this.#entries.values()].map(entry => this.close(entry.binding.identity.sessionRef, entry.terminalRef)));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { managedTerminalBinding, type ManagedTerminalOptions } from './managed-terminal-binding.ts';
|
|
2
|
+
import { ManagedTerminalProcesses } from './managed-terminal-processes.ts';
|
|
3
|
+
import { TerminalSessions } from './terminal-sessions.ts';
|
|
4
|
+
|
|
5
|
+
export class ManagedTerminalSessions extends TerminalSessions<Awaited<ReturnType<typeof managedTerminalBinding>>> {
|
|
6
|
+
constructor(options: ManagedTerminalOptions) {
|
|
7
|
+
super(ref => managedTerminalBinding(options, ref), emit => new ManagedTerminalProcesses(emit));
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -1,124 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
|
|
6
|
-
// What an @ in the composer offers, as Pi's terminal does: files and folders
|
|
7
|
-
// of the project matched loosely by name (fd, which honours .gitignore), and,
|
|
8
|
-
// after a folder and a slash (src/, ~/Documents/, /Volumes/), the names under
|
|
9
|
-
// that folder anywhere on this Mac. Only names reach the browser; no file is
|
|
10
|
-
// opened.
|
|
11
|
-
export type PathSuggestion = { value: string; label: string; detail: string; kind: "file" | "directory" };
|
|
12
|
-
type Entry = { path: string; directory: boolean };
|
|
13
|
-
|
|
14
|
-
const LIMIT = 20;
|
|
15
|
-
const SEARCH_RESULTS = 100;
|
|
16
|
-
const LISTED_ENTRIES = 5_000;
|
|
17
|
-
// Outside the project a search stays shallow and skips app data and
|
|
18
|
-
// dependency folders: a home folder is far larger than a repository.
|
|
19
|
-
const OUTSIDE_DEPTH = 4;
|
|
20
|
-
const fold = (value: string) => value.normalize("NFC").toLowerCase();
|
|
21
|
-
const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22
|
-
// A name typed composed (Dự án) may be stored decomposed on disk: either form matches.
|
|
23
|
-
const either = (value: string) => value.normalize("NFC") === value.normalize("NFD") ? escape(value)
|
|
24
|
-
: `(?:${escape(value.normalize("NFC"))}|${escape(value.normalize("NFD"))})`;
|
|
25
|
-
const inside = (file: string, root: string) => file === root || file.startsWith(root + path.sep);
|
|
26
|
-
|
|
27
|
-
// fd where it is commonly installed, then on PATH (Debian and Ubuntu name it
|
|
28
|
-
// fdfind; Windows adds .exe).
|
|
29
|
-
export function findFd(home = os.homedir()): string | null {
|
|
30
|
-
const names = process.platform === "win32" ? ["fd.exe"] : ["fd", "fdfind"];
|
|
31
|
-
const onPath = String(process.env.PATH ?? "").split(path.delimiter).filter(Boolean).flatMap((dir) => names.map((name) => path.join(dir, name)));
|
|
32
|
-
for (const candidate of ["/opt/homebrew/bin/fd", "/usr/local/bin/fd", path.join(home, ".pi/agent/bin", names[0]), ...onPath]) {
|
|
33
|
-
try { fs.accessSync(candidate, fs.constants.X_OK); return candidate; } catch { /* next */ }
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
// macOS: the system's git, always there; elsewhere git from PATH.
|
|
38
|
-
const GIT = process.platform === "darwin" ? "/usr/bin/git" : "git";
|
|
39
|
-
|
|
40
|
-
// Higher is better: the name itself, then its start, then anywhere in it,
|
|
41
|
-
// then anywhere in the path; a folder before a file with the same score.
|
|
42
|
-
function score(entry: string, query: string, directory: boolean): number {
|
|
43
|
-
const name = fold(path.basename(entry)), wanted = fold(query);
|
|
44
|
-
const value = name === wanted ? 100 : name.startsWith(wanted) ? 80 : name.includes(wanted) ? 50 : fold(entry).includes(wanted) ? 30 : 0;
|
|
45
|
-
return value && directory ? value + 10 : value;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function run(file: string, args: string[], signal?: AbortSignal, input?: string): Promise<string> {
|
|
49
|
-
return new Promise((resolve) => {
|
|
50
|
-
const child = execFile(file, args, { encoding: "utf8", timeout: 4_000, maxBuffer: 8 << 20, signal }, (error, stdout) => resolve(error ? "" : stdout));
|
|
51
|
-
child.stdin?.on("error", () => {}); child.stdin?.end(input ?? "");
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// The folder's own names, without what its repository ignores (dist/,
|
|
56
|
-
// node_modules/), as fd lists them for Pi.
|
|
57
|
-
async function listed(folder: string, signal?: AbortSignal): Promise<Entry[]> {
|
|
58
|
-
const entries: Entry[] = [];
|
|
59
|
-
try {
|
|
60
|
-
for await (const entry of await fs.promises.opendir(folder)) {
|
|
61
|
-
let directory = entry.isDirectory();
|
|
62
|
-
if (entry.isSymbolicLink()) try { directory = fs.statSync(path.join(folder, entry.name)).isDirectory(); } catch { /* broken link */ }
|
|
63
|
-
entries.push({ path: entry.name, directory });
|
|
64
|
-
if (entries.length >= LISTED_ENTRIES) break;
|
|
65
|
-
}
|
|
66
|
-
} catch { /* unreadable */ }
|
|
67
|
-
if (!entries.length) return entries;
|
|
68
|
-
const ignored = new Set((await run(GIT, ["-C", folder, "check-ignore", "-z", "--stdin"], signal, entries.map((entry) => `${entry.path}\0`).join("")))
|
|
69
|
-
.split("\0").filter(Boolean));
|
|
70
|
-
return ignored.size ? entries.filter((entry) => !ignored.has(entry.path)) : entries;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Names deeper down that match: fd as Pi runs it in the project; shallow and
|
|
74
|
-
// without hidden folders elsewhere; without fd, the files git knows about.
|
|
75
|
-
async function searched(folder: string, query: string, options: { fd: string | null; project: boolean; atRoot: boolean; signal?: AbortSignal }): Promise<Entry[]> {
|
|
76
|
-
if (options.fd) {
|
|
77
|
-
const args = ["--base-directory", folder, "--type", "f", "--type", "d", "--max-results", String(SEARCH_RESULTS), "--ignore-case", "--exclude", ".git",
|
|
78
|
-
...(options.project ? ["--follow", "--hidden"] : ["--max-depth", String(OUTSIDE_DEPTH), "--exclude", "Library", "--exclude", "node_modules", ...(query.startsWith(".") ? ["--hidden"] : [])]),
|
|
79
|
-
...(query.includes("/") ? ["--full-path", query.split("/").filter(Boolean).map(either).join("/")] : [either(query)])];
|
|
80
|
-
return (await run(options.fd, args, options.signal)).split("\n").filter(Boolean)
|
|
81
|
-
.map((line) => ({ path: line.replace(/^\.\//, "").replace(/\/$/, ""), directory: line.endsWith("/") }));
|
|
82
|
-
}
|
|
83
|
-
if (!options.atRoot) return [];
|
|
84
|
-
const files = (await run(GIT, ["-C", folder, "ls-files", "--cached", "--others", "--exclude-standard", "-z"], options.signal))
|
|
85
|
-
.split("\0").filter(Boolean).slice(0, 50_000);
|
|
86
|
-
const folders = new Set<string>();
|
|
87
|
-
for (const file of files) for (let dir = path.posix.dirname(file); dir !== "."; dir = path.posix.dirname(dir)) folders.add(dir);
|
|
88
|
-
const wanted = fold(query);
|
|
89
|
-
return [...[...folders].map((dir) => ({ path: dir, directory: true })), ...files.map((file) => ({ path: file, directory: false }))]
|
|
90
|
-
.filter((entry) => fold(entry.path).includes(wanted));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function suggestPaths(options: { root: string; query: string; home?: string; fd?: string | null; signal?: AbortSignal }): Promise<{ suggestions: PathSuggestion[] }> {
|
|
94
|
-
const home = options.home ?? os.homedir(), fd = options.fd === undefined ? findFd(home) : options.fd;
|
|
95
|
-
const root = fs.realpathSync(options.root);
|
|
96
|
-
const typed = options.query.trim() === "~" ? "~/" : options.query.trim();
|
|
97
|
-
// Text up to the last slash names a folder: search under it (Pi's scoped
|
|
98
|
-
// search). When it is not a folder, the whole text is matched in the project.
|
|
99
|
-
const cut = typed.lastIndexOf("/") + 1;
|
|
100
|
-
let base = typed.slice(0, cut), name = typed.slice(cut), folder = root;
|
|
101
|
-
if (base) {
|
|
102
|
-
try {
|
|
103
|
-
const candidate = fs.realpathSync(base.startsWith("~/") ? path.join(home, base.slice(2)) : path.resolve(root, base));
|
|
104
|
-
if (fs.statSync(candidate).isDirectory()) folder = candidate; else { base = ""; name = typed; }
|
|
105
|
-
} catch { base = ""; name = typed; }
|
|
106
|
-
}
|
|
107
|
-
const project = inside(folder, root);
|
|
108
|
-
const near = await listed(folder, options.signal);
|
|
109
|
-
const deep = name ? await searched(folder, name, { fd, project, atRoot: folder === root, signal: options.signal }) : [];
|
|
110
|
-
const seen = new Set<string>(), entries: Array<Entry & { score: number }> = [];
|
|
111
|
-
for (const entry of [...near, ...deep]) {
|
|
112
|
-
if (seen.has(entry.path)) continue; seen.add(entry.path);
|
|
113
|
-
// Hidden entries (.github, .ssh, .zshrc) for a name typed with a dot, and
|
|
114
|
-
// in the project for any name typed, as Pi's search finds them there.
|
|
115
|
-
if (entry.path.split("/").some((part) => part.startsWith(".")) && !name.startsWith(".") && !(project && name)) continue;
|
|
116
|
-
const value = name ? score(entry.path, name.split("/").filter(Boolean).at(-1) ?? name, entry.directory) : 1;
|
|
117
|
-
if (value > 0) entries.push({ ...entry, score: value });
|
|
118
|
-
}
|
|
119
|
-
const depth = (entry: Entry) => entry.path.split("/").length;
|
|
120
|
-
entries.sort((a, b) => b.score - a.score || depth(a) - depth(b)
|
|
121
|
-
|| (name ? a.path.length - b.path.length : Number(b.directory) - Number(a.directory)) || a.path.localeCompare(b.path));
|
|
122
|
-
return { suggestions: entries.slice(0, LIMIT).map((entry) => ({ value: `${base}${entry.path}${entry.directory ? "/" : ""}`,
|
|
123
|
-
label: path.basename(entry.path) + (entry.directory ? "/" : ""), detail: `${base}${entry.path}`, kind: entry.directory ? "directory" : "file" })) };
|
|
124
|
-
}
|
|
1
|
+
export { findFd, suggestPaths, type PathSuggestion } from "../../piagent-core/extensions/path-suggestions.ts";
|