@huseynsnmz/pi-suite 0.0.0-stage → 0.2.0
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 +28 -0
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/SECURITY.md +5 -0
- package/package.json +112 -4
- package/packages/board/CHANGELOG.md +340 -0
- package/packages/board/CONTRIBUTING.md +7 -0
- package/packages/board/LICENSE +21 -0
- package/packages/board/README.md +88 -0
- package/packages/board/SECURITY.md +19 -0
- package/packages/board/docs/how-it-works.md +31 -0
- package/packages/board/docs/reference.md +278 -0
- package/packages/board/package.json +81 -0
- package/packages/board/runner/job-runner-legacy.mjs +68 -0
- package/packages/board/runner/job-runner.mjs +834 -0
- package/packages/board/runner/pty-runner-legacy.mjs +50 -0
- package/packages/board/runner/pty-runner.mjs +2175 -0
- package/packages/board/runner/state-coordinator.mjs +673 -0
- package/packages/board/runner/state-runner.mjs +203 -0
- package/packages/board/runner/title-runner.mjs +80 -0
- package/packages/board/src/commands/agent-board.ts +394 -0
- package/packages/board/src/commands/attach-decision.mjs +66 -0
- package/packages/board/src/commands/attach-flow.ts +251 -0
- package/packages/board/src/commands/bg.ts +103 -0
- package/packages/board/src/commands/board-model.ts +21 -0
- package/packages/board/src/core/atomic.mjs +184 -0
- package/packages/board/src/core/attach-delete.mjs +24 -0
- package/packages/board/src/core/attach-frame-proof.mjs +870 -0
- package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
- package/packages/board/src/core/attach-frame-stream.mjs +266 -0
- package/packages/board/src/core/attach-view-proof.mjs +709 -0
- package/packages/board/src/core/auto-state.mjs +377 -0
- package/packages/board/src/core/background-runs.mjs +273 -0
- package/packages/board/src/core/code-refs-store.mjs +315 -0
- package/packages/board/src/core/code-refs.mjs +913 -0
- package/packages/board/src/core/config.mjs +77 -0
- package/packages/board/src/core/control-protocol.mjs +482 -0
- package/packages/board/src/core/coordinator-client.mjs +633 -0
- package/packages/board/src/core/coordinator-journal.mjs +282 -0
- package/packages/board/src/core/coordinator-protocol.mjs +14 -0
- package/packages/board/src/core/cwd-stats.mjs +114 -0
- package/packages/board/src/core/dashboard-render.mjs +10 -0
- package/packages/board/src/core/derive.mjs +117 -0
- package/packages/board/src/core/diagnostics.mjs +133 -0
- package/packages/board/src/core/double-press.mjs +26 -0
- package/packages/board/src/core/editor-state-reporter.mjs +112 -0
- package/packages/board/src/core/events.mjs +286 -0
- package/packages/board/src/core/evidence.mjs +245 -0
- package/packages/board/src/core/follow-up-queue.mjs +211 -0
- package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
- package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
- package/packages/board/src/core/frame-witness-update.mjs +35 -0
- package/packages/board/src/core/heuristics.mjs +313 -0
- package/packages/board/src/core/hold-input.mjs +27 -0
- package/packages/board/src/core/host-coordination.mjs +300 -0
- package/packages/board/src/core/host-crash.mjs +79 -0
- package/packages/board/src/core/host-probe.mjs +207 -0
- package/packages/board/src/core/host-protocol.mjs +73 -0
- package/packages/board/src/core/ids.mjs +35 -0
- package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
- package/packages/board/src/core/invocation.mjs +43 -0
- package/packages/board/src/core/launch-options.mjs +418 -0
- package/packages/board/src/core/launch.mjs +157 -0
- package/packages/board/src/core/load-config.mjs +4 -0
- package/packages/board/src/core/locks.mjs +530 -0
- package/packages/board/src/core/paths.mjs +183 -0
- package/packages/board/src/core/pid.mjs +75 -0
- package/packages/board/src/core/prewarm-schedule.mjs +41 -0
- package/packages/board/src/core/process-identity-probe.mjs +228 -0
- package/packages/board/src/core/prompt-transport.mjs +13 -0
- package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
- package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
- package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
- package/packages/board/src/core/pty-attach-render.mjs +229 -0
- package/packages/board/src/core/pty-links.mjs +81 -0
- package/packages/board/src/core/pty-scroll.mjs +522 -0
- package/packages/board/src/core/pty-support.mjs +327 -0
- package/packages/board/src/core/repo.mjs +103 -0
- package/packages/board/src/core/rows.mjs +414 -0
- package/packages/board/src/core/screen-log-gc.mjs +198 -0
- package/packages/board/src/core/screen-log.mjs +160 -0
- package/packages/board/src/core/session-view.mjs +174 -0
- package/packages/board/src/core/slash-commands.mjs +52 -0
- package/packages/board/src/core/state-commands.mjs +757 -0
- package/packages/board/src/core/status-consistency.mjs +98 -0
- package/packages/board/src/core/steering-prompts.mjs +34 -0
- package/packages/board/src/core/steering.mjs +133 -0
- package/packages/board/src/core/stop-latch.mjs +41 -0
- package/packages/board/src/core/store.mjs +523 -0
- package/packages/board/src/core/suspend.mjs +36 -0
- package/packages/board/src/core/terminal-attach-client.mjs +981 -0
- package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
- package/packages/board/src/core/terminal-model.mjs +222 -0
- package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
- package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
- package/packages/board/src/core/terminal-snapshot.mjs +440 -0
- package/packages/board/src/core/test-knobs.mjs +28 -0
- package/packages/board/src/core/title.mjs +43 -0
- package/packages/board/src/core/types.mjs +414 -0
- package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
- package/packages/board/src/core/worktree.mjs +65 -0
- package/packages/board/src/index.ts +303 -0
- package/packages/board/src/runtime/service.mjs +3222 -0
- package/packages/board/src/ui/chrome.ts +20 -0
- package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
- package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
- package/packages/board/src/ui/dashboard.ts +2237 -0
- package/packages/board/src/ui/double-left.ts +30 -0
- package/packages/board/src/ui/external-editor.ts +25 -0
- package/packages/board/src/ui/focus.ts +32 -0
- package/packages/board/src/ui/pty-attach.ts +3184 -0
- package/packages/board/src/ui/waiting.ts +30 -0
- package/packages/commands/LICENSE +21 -0
- package/packages/commands/README.md +19 -0
- package/packages/commands/package.json +22 -0
- package/packages/commands/src/index.ts +43 -0
- package/packages/commands/src/security-review.ts +109 -0
- package/packages/frame/CHANGELOG.md +113 -0
- package/packages/frame/CONTRIBUTING.md +7 -0
- package/packages/frame/LICENSE +22 -0
- package/packages/frame/README.md +105 -0
- package/packages/frame/SECURITY.md +16 -0
- package/packages/frame/docs/how-it-works.md +53 -0
- package/packages/frame/docs/reference.md +78 -0
- package/packages/frame/package.json +72 -0
- package/packages/frame/src/data/git.ts +90 -0
- package/packages/frame/src/data/github.ts +140 -0
- package/packages/frame/src/data/gitlab.ts +63 -0
- package/packages/frame/src/data/mode.ts +49 -0
- package/packages/frame/src/data/past-prompts.ts +57 -0
- package/packages/frame/src/data/runner.ts +32 -0
- package/packages/frame/src/data/snapshot.ts +223 -0
- package/packages/frame/src/data/start-notes.ts +104 -0
- package/packages/frame/src/data/telemetry.ts +337 -0
- package/packages/frame/src/data/todos.ts +162 -0
- package/packages/frame/src/data/totals.ts +94 -0
- package/packages/frame/src/data/visits.ts +45 -0
- package/packages/frame/src/index.ts +696 -0
- package/packages/frame/src/kit/LICENSE +21 -0
- package/packages/frame/src/kit/app/attribution.ts +30 -0
- package/packages/frame/src/kit/app/builder.ts +186 -0
- package/packages/frame/src/kit/app/claim.ts +27 -0
- package/packages/frame/src/kit/app/feature.ts +91 -0
- package/packages/frame/src/kit/app/host.ts +214 -0
- package/packages/frame/src/kit/app/renderers.ts +62 -0
- package/packages/frame/src/kit/app/scope.ts +125 -0
- package/packages/frame/src/kit/board/model.ts +337 -0
- package/packages/frame/src/kit/contract.ts +79 -0
- package/packages/frame/src/kit/contracts/accounts.ts +33 -0
- package/packages/frame/src/kit/contracts/ask.ts +120 -0
- package/packages/frame/src/kit/contracts/board.ts +6 -0
- package/packages/frame/src/kit/contracts/by.ts +22 -0
- package/packages/frame/src/kit/contracts/calls.ts +46 -0
- package/packages/frame/src/kit/contracts/events.ts +89 -0
- package/packages/frame/src/kit/contracts/mode.ts +26 -0
- package/packages/frame/src/kit/contracts/names.ts +12 -0
- package/packages/frame/src/kit/contracts/peers.ts +77 -0
- package/packages/frame/src/kit/contracts/prompts.ts +8 -0
- package/packages/frame/src/kit/contracts/screen.ts +75 -0
- package/packages/frame/src/kit/contracts/skills.ts +81 -0
- package/packages/frame/src/kit/contracts/sources.ts +81 -0
- package/packages/frame/src/kit/contracts/views.ts +42 -0
- package/packages/frame/src/kit/control.ts +59 -0
- package/packages/frame/src/kit/dark-theme.ts +41 -0
- package/packages/frame/src/kit/decode.ts +210 -0
- package/packages/frame/src/kit/events/log.ts +313 -0
- package/packages/frame/src/kit/format.ts +25 -0
- package/packages/frame/src/kit/host.ts +4 -0
- package/packages/frame/src/kit/index.ts +25 -0
- package/packages/frame/src/kit/legacy-tasks.ts +39 -0
- package/packages/frame/src/kit/overlay-images.ts +36 -0
- package/packages/frame/src/kit/paths.ts +16 -0
- package/packages/frame/src/kit/screen/client.ts +74 -0
- package/packages/frame/src/kit/screen/feature.ts +64 -0
- package/packages/frame/src/kit/screen/model.ts +182 -0
- package/packages/frame/src/kit/screen/view.ts +838 -0
- package/packages/frame/src/kit/settings/files.ts +90 -0
- package/packages/frame/src/kit/settings/setting.ts +57 -0
- package/packages/frame/src/kit/settings/store.ts +192 -0
- package/packages/frame/src/kit/sources/file.ts +129 -0
- package/packages/frame/src/kit/sources/follow.ts +155 -0
- package/packages/frame/src/kit/testing.ts +260 -0
- package/packages/frame/src/kit/totals.ts +72 -0
- package/packages/frame/src/kit/ui/entry.ts +75 -0
- package/packages/frame/src/kit/ui/frame.ts +212 -0
- package/packages/frame/src/kit/ui/glyphs.ts +132 -0
- package/packages/frame/src/kit/ui/scroll.ts +14 -0
- package/packages/frame/src/render/diff.ts +157 -0
- package/packages/frame/src/render/fit.ts +86 -0
- package/packages/frame/src/render/format.ts +108 -0
- package/packages/frame/src/render/frame.ts +106 -0
- package/packages/frame/src/render/glyphs.ts +189 -0
- package/packages/frame/src/render/keys.ts +43 -0
- package/packages/frame/src/render/lead.ts +37 -0
- package/packages/frame/src/render/paint.ts +135 -0
- package/packages/frame/src/render/safe.ts +19 -0
- package/packages/frame/src/render/segments.ts +735 -0
- package/packages/frame/src/render/streaming.ts +223 -0
- package/packages/frame/src/render/timing.ts +106 -0
- package/packages/frame/src/render/tools.ts +167 -0
- package/packages/frame/src/settings.ts +105 -0
- package/packages/frame/src/ui/editor.ts +272 -0
- package/packages/frame/src/ui/effort.ts +36 -0
- package/packages/frame/src/ui/footer.ts +165 -0
- package/packages/frame/src/ui/header.ts +362 -0
- package/packages/frame/src/ui/history-search.ts +23 -0
- package/packages/frame/src/ui/interrupt.ts +21 -0
- package/packages/frame/src/ui/outputs.ts +170 -0
- package/packages/frame/src/ui/pi-entries.ts +298 -0
- package/packages/frame/src/ui/previews.ts +72 -0
- package/packages/frame/src/ui/queue.ts +75 -0
- package/packages/frame/src/ui/screen.ts +59 -0
- package/packages/frame/src/ui/timings.ts +48 -0
- package/packages/frame/src/ui/todos.ts +205 -0
- package/packages/frame/src/ui/tool-frame.ts +595 -0
- package/packages/frame/src/ui/working.ts +25 -0
- package/packages/frame/src/view/state.ts +97 -0
- package/packages/frame/src/view/transcript.ts +175 -0
- package/packages/frame/src/view/view.ts +162 -0
- package/packages/memory/CONTRIBUTING.md +7 -0
- package/packages/memory/LICENSE +21 -0
- package/packages/memory/README.md +44 -0
- package/packages/memory/SECURITY.md +20 -0
- package/packages/memory/package.json +55 -0
- package/packages/memory/skills/review-memory/SKILL.md +48 -0
- package/packages/memory/src/index.ts +163 -0
- package/packages/memory/src/prompt.ts +25 -0
- package/packages/memory/src/review.ts +150 -0
- package/packages/memory/src/secrets.ts +22 -0
- package/packages/memory/src/store.ts +173 -0
- package/packages/modes/CHANGELOG.md +104 -0
- package/packages/modes/CONTRIBUTING.md +7 -0
- package/packages/modes/LICENSE +21 -0
- package/packages/modes/README.md +174 -0
- package/packages/modes/SECURITY.md +17 -0
- package/packages/modes/package.json +55 -0
- package/packages/modes/skills/review-permissions/SKILL.md +54 -0
- package/packages/modes/src/bash.ts +218 -0
- package/packages/modes/src/deny-off.ts +62 -0
- package/packages/modes/src/git-safety.ts +72 -0
- package/packages/modes/src/index.ts +338 -0
- package/packages/modes/src/interview.ts +206 -0
- package/packages/modes/src/judge-gate.ts +113 -0
- package/packages/modes/src/modes.ts +121 -0
- package/packages/modes/src/review/apply.mjs +156 -0
- package/packages/modes/src/review/collect.mjs +208 -0
- package/packages/modes/src/review/tools.ts +71 -0
- package/packages/modes/src/sandbox.ts +37 -0
- package/packages/router/CHANGELOG.md +8 -0
- package/packages/router/LICENSE +21 -0
- package/packages/router/README.md +90 -0
- package/packages/router/package.json +51 -0
- package/packages/router/src/config.ts +65 -0
- package/packages/router/src/index.ts +172 -0
- package/packages/router/src/judge.ts +51 -0
- package/packages/router/src/route.ts +67 -0
- package/packages/web/CHANGELOG.md +36 -0
- package/packages/web/LICENSE +21 -0
- package/packages/web/README.md +87 -0
- package/packages/web/SECURITY.md +13 -0
- package/packages/web/package.json +59 -0
- package/packages/web/src/config.ts +109 -0
- package/packages/web/src/dnsfilter.ts +97 -0
- package/packages/web/src/extract.ts +65 -0
- package/packages/web/src/index.ts +217 -0
- package/packages/web/src/markdown.ts +122 -0
- package/packages/web/src/net.ts +109 -0
- package/packages/web/src/reader.ts +73 -0
- package/packages/web/src/sanitize.ts +50 -0
- package/packages/web/src/search.ts +76 -0
- package/packages/web/src/secrets.ts +22 -0
- package/packages/web/src/validate.ts +186 -0
- package/src/index.ts +156 -0
- package/src/settings.ts +79 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash-time finalization for PTY hosts (used by pty-runner's uncaughtException
|
|
3
|
+
* handler). Pure enough to unit-test: writes a failed host.json + a diagnostic.
|
|
4
|
+
* Never throws — this runs on the crash path.
|
|
5
|
+
*/
|
|
6
|
+
import { appendDiagnostic } from "./diagnostics.mjs";
|
|
7
|
+
import { updateOwnedHost, writeHost } from "./store.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} root
|
|
11
|
+
* @param {string} viewId
|
|
12
|
+
* @param {object|null} host
|
|
13
|
+
* @param {unknown} error
|
|
14
|
+
* @param {{ expectedInstanceId?: string|null }} [opts] When `expectedInstanceId`
|
|
15
|
+
* is set (new ownership protocol, issue #70), the failed state is written via
|
|
16
|
+
* the owner-fenced `updateOwnedHost` — a superseded instance can never
|
|
17
|
+
* clobber the replacement's live record; the skip is recorded as a
|
|
18
|
+
* `host_crash_owner_changed` diagnostic. Without it the legacy wholesale
|
|
19
|
+
* `writeHost` behavior is preserved exactly.
|
|
20
|
+
* @returns {object}
|
|
21
|
+
*/
|
|
22
|
+
export function finalizeHostCrash(root, viewId, host, error, opts = {}) {
|
|
23
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
+
const failed = {
|
|
25
|
+
...(host ?? { version: 1, viewId, mode: "pty", socketPath: null, startedAt: Date.now() }),
|
|
26
|
+
state: "failed",
|
|
27
|
+
endedAt: Date.now(),
|
|
28
|
+
lastSeenAt: Date.now(),
|
|
29
|
+
exitCode: 1,
|
|
30
|
+
error: message,
|
|
31
|
+
};
|
|
32
|
+
if (opts.expectedInstanceId != null) {
|
|
33
|
+
try {
|
|
34
|
+
// Build the terminal state from the LIVE record inside the host-meta
|
|
35
|
+
// lock — never from the possibly-stale `host` snapshot.
|
|
36
|
+
const result = updateOwnedHost(root, viewId, opts.expectedInstanceId, (cur) => ({
|
|
37
|
+
...cur,
|
|
38
|
+
state: "failed",
|
|
39
|
+
endedAt: Date.now(),
|
|
40
|
+
lastSeenAt: Date.now(),
|
|
41
|
+
exitCode: 1,
|
|
42
|
+
error: message,
|
|
43
|
+
}));
|
|
44
|
+
if (result.updated && result.host) {
|
|
45
|
+
appendCrashDiagnostic(root, viewId, message);
|
|
46
|
+
return result.host;
|
|
47
|
+
}
|
|
48
|
+
// ownerChanged, or the meta lock was briefly busy: never write unfenced.
|
|
49
|
+
try {
|
|
50
|
+
appendDiagnostic(root, viewId, {
|
|
51
|
+
source: "runner",
|
|
52
|
+
level: "warn",
|
|
53
|
+
code: "host_crash_owner_changed",
|
|
54
|
+
message: `Skipped crash finalize: host record no longer belongs to this instance (${message})`,
|
|
55
|
+
details: { error: message, ownerChanged: result.ownerChanged },
|
|
56
|
+
});
|
|
57
|
+
} catch { /* best effort */ }
|
|
58
|
+
} catch { /* best effort */ }
|
|
59
|
+
return failed;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
writeHost(root, failed);
|
|
63
|
+
} catch { /* best effort */ }
|
|
64
|
+
appendCrashDiagnostic(root, viewId, message);
|
|
65
|
+
return failed;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @param {string} root @param {string} viewId @param {string} message */
|
|
69
|
+
function appendCrashDiagnostic(root, viewId, message) {
|
|
70
|
+
try {
|
|
71
|
+
appendDiagnostic(root, viewId, {
|
|
72
|
+
source: "runner",
|
|
73
|
+
level: "error",
|
|
74
|
+
code: "host_crashed",
|
|
75
|
+
message: `PTY host crashed: ${message}`,
|
|
76
|
+
details: { error: message },
|
|
77
|
+
});
|
|
78
|
+
} catch { /* best effort */ }
|
|
79
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real control-endpoint probe (issue #70).
|
|
3
|
+
*
|
|
4
|
+
* Connects to a PTY host control socket/pipe, performs the JSONL hello
|
|
5
|
+
* handshake, and classifies the outcome via `classifyProbeResult`. This is
|
|
6
|
+
* the ONLY "is this endpoint actually usable" authority — path existence and
|
|
7
|
+
* host.json state are hints, never proof (spec §7.1).
|
|
8
|
+
*
|
|
9
|
+
* Side-effectful by design (net + fs); all pure decisions live in
|
|
10
|
+
* host-coordination.mjs.
|
|
11
|
+
*/
|
|
12
|
+
import { createConnection } from "node:net";
|
|
13
|
+
import { lstatSync } from "node:fs";
|
|
14
|
+
import { classifyProbeResult } from "./host-coordination.mjs";
|
|
15
|
+
import { resolveTestMs } from "./test-knobs.mjs";
|
|
16
|
+
|
|
17
|
+
/** Default probe timeout. */
|
|
18
|
+
export const HOST_PROBE_TIMEOUT_MS = 250;
|
|
19
|
+
/** Poll cadence between attach-resolver probes / retries (issue #70 Task 12). */
|
|
20
|
+
export const HOST_PROBE_RETRY_MS = 150;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dynamic reader for HOST_PROBE_TIMEOUT_MS (issue #95 F4): env set after module
|
|
24
|
+
* load must still reach the probe, so read the knob at each use site.
|
|
25
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
26
|
+
* @returns {number}
|
|
27
|
+
*/
|
|
28
|
+
export function resolveHostProbeTimeoutMs(env = process.env) {
|
|
29
|
+
return resolveTestMs(env, "AGENT_BOARD_TEST_HOST_PROBE_TIMEOUT_MS", HOST_PROBE_TIMEOUT_MS);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} ProbeResult
|
|
34
|
+
* @property {"ready"|"starting"|"stale"|"occupied"|"missing"|"unknown"} classification
|
|
35
|
+
* @property {boolean} connected
|
|
36
|
+
* @property {boolean} protocolValid
|
|
37
|
+
* @property {boolean} ready
|
|
38
|
+
* @property {string|null} viewId
|
|
39
|
+
* @property {string|null} instanceId
|
|
40
|
+
* @property {string|null} state
|
|
41
|
+
* @property {string|null} errorCode
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Probe a host control endpoint via a real connection + JSONL hello.
|
|
46
|
+
*
|
|
47
|
+
* `connect` is injectable for tests; the default accepts anything
|
|
48
|
+
* `net.createConnection` accepts (a socket path / pipe name, or a
|
|
49
|
+
* `{ host, port }` address object) and must return a socket-like object with
|
|
50
|
+
* the usual `'connect' | 'data' | 'error'` events and a `.destroy()`.
|
|
51
|
+
*
|
|
52
|
+
* The probe socket is ALWAYS destroyed before the promise resolves.
|
|
53
|
+
*
|
|
54
|
+
* @param {string|{host:string,port:number}} socketPath
|
|
55
|
+
* @param {{ timeoutMs?: number, expectedViewId?: string|null, expectedInstanceId?: string|null, connect?: (target: any) => any }} [opts]
|
|
56
|
+
* @returns {Promise<ProbeResult>}
|
|
57
|
+
*/
|
|
58
|
+
export function probeHost(socketPath, opts = {}) {
|
|
59
|
+
const timeoutMs = opts.timeoutMs ?? resolveHostProbeTimeoutMs();
|
|
60
|
+
const expectedViewId = opts.expectedViewId ?? null;
|
|
61
|
+
const expectedInstanceId = opts.expectedInstanceId ?? null;
|
|
62
|
+
const connect = opts.connect ?? createConnection;
|
|
63
|
+
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
/** @type {any} */ let socket;
|
|
66
|
+
/** @type {NodeJS.Timeout|null} */ let timer = null;
|
|
67
|
+
let settled = false;
|
|
68
|
+
let buffer = "";
|
|
69
|
+
/** @type {ProbeResult} */ let result;
|
|
70
|
+
|
|
71
|
+
const finish = () => {
|
|
72
|
+
if (settled) return;
|
|
73
|
+
settled = true;
|
|
74
|
+
if (timer) clearTimeout(timer);
|
|
75
|
+
try { socket?.destroy(); } catch { /* best effort */ }
|
|
76
|
+
resolve(result);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** @param {string} errorCode */ const finishError = (errorCode) => {
|
|
80
|
+
const snapshot = {
|
|
81
|
+
connected: false,
|
|
82
|
+
protocolValid: false,
|
|
83
|
+
errorCode,
|
|
84
|
+
isSocket: false,
|
|
85
|
+
};
|
|
86
|
+
// Only a refused Unix-domain socket path can be considered stale, and
|
|
87
|
+
// even then only when the path really is a socket file (a plain file
|
|
88
|
+
// or directory at the same path must stay "unknown" — never unlink it).
|
|
89
|
+
if (errorCode === "ECONNREFUSED" && process.platform !== "win32" && typeof socketPath === "string") {
|
|
90
|
+
try {
|
|
91
|
+
snapshot.isSocket = lstatSync(socketPath).isSocket() === true;
|
|
92
|
+
} catch {
|
|
93
|
+
snapshot.isSocket = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
result = buildResult(snapshot, null);
|
|
97
|
+
finish();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
socket = connect(socketPath);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
result = buildResult({ connected: false, protocolValid: false, errorCode: errCode(err), isSocket: false }, null);
|
|
104
|
+
finish();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
timer = setTimeout(() => finishError("TIMEOUT"), timeoutMs);
|
|
109
|
+
|
|
110
|
+
socket.on("error", (err) => {
|
|
111
|
+
finishError(errCode(err));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
socket.on("connect", () => {
|
|
115
|
+
try {
|
|
116
|
+
socket.write(JSON.stringify({ type: "hello", clientId: "probe", wantOutput: false }) + "\n");
|
|
117
|
+
} catch (err) {
|
|
118
|
+
finishError(errCode(err));
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
socket.on("data", (chunk) => {
|
|
123
|
+
if (settled) return;
|
|
124
|
+
buffer += chunk.toString("utf8");
|
|
125
|
+
const newline = buffer.indexOf("\n");
|
|
126
|
+
if (newline < 0) return;
|
|
127
|
+
const line = buffer.slice(0, newline).trim();
|
|
128
|
+
const parsed = parseReply(line);
|
|
129
|
+
result = buildResultFromReply(parsed, expectedViewId, expectedInstanceId);
|
|
130
|
+
finish();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// A close before any data settles the probe as an error-path unknown.
|
|
134
|
+
socket.on("close", () => {
|
|
135
|
+
if (!settled) finishError("CLOSED");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} line
|
|
142
|
+
* @returns {{ ok: boolean, status: any }}
|
|
143
|
+
*/
|
|
144
|
+
function parseReply(line) {
|
|
145
|
+
if (!line) return { ok: false, status: null };
|
|
146
|
+
try {
|
|
147
|
+
const msg = JSON.parse(line);
|
|
148
|
+
if (!msg || typeof msg !== "object") return { ok: false, status: null };
|
|
149
|
+
if (msg.type !== "hello" && msg.type !== "status") return { ok: false, status: null };
|
|
150
|
+
if (!msg.status || typeof msg.status !== "object") return { ok: false, status: null };
|
|
151
|
+
return { ok: true, status: msg.status };
|
|
152
|
+
} catch {
|
|
153
|
+
return { ok: false, status: null };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Assemble the classify snapshot from a parsed reply.
|
|
159
|
+
* @param {{ ok: boolean, status: any }} parsed
|
|
160
|
+
* @param {string|null} expectedViewId
|
|
161
|
+
* @param {string|null} expectedInstanceId
|
|
162
|
+
*/
|
|
163
|
+
function buildResultFromReply(parsed, expectedViewId, expectedInstanceId) {
|
|
164
|
+
const status = parsed.ok ? parsed.status : null;
|
|
165
|
+
const viewMatch = expectedViewId == null ? true : status?.viewId === expectedViewId;
|
|
166
|
+
const instanceMatch = expectedInstanceId == null ? true : status?.instanceId === expectedInstanceId;
|
|
167
|
+
// Legacy hosts (pre-instanceId protocol) report alive without a readyAt
|
|
168
|
+
// field; a legacy probe (no expectedInstanceId) must still classify them
|
|
169
|
+
// ready so upgrade-window attach keeps working (spec §10.1 / Task 2 ruling).
|
|
170
|
+
const readyAt = status?.readyAt ?? (expectedInstanceId == null && status?.state === "alive" ? 1 : null);
|
|
171
|
+
return buildResult(
|
|
172
|
+
{
|
|
173
|
+
connected: true,
|
|
174
|
+
protocolValid: parsed.ok,
|
|
175
|
+
viewMatch,
|
|
176
|
+
instanceMatch,
|
|
177
|
+
state: status?.state ?? null,
|
|
178
|
+
readyAt,
|
|
179
|
+
},
|
|
180
|
+
status,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {{ connected: boolean, protocolValid: boolean, viewMatch?: boolean, instanceMatch?: boolean, state?: string|null, readyAt?: number|null, errorCode?: string|null, isSocket?: boolean }} snapshot
|
|
186
|
+
* @param {any} status
|
|
187
|
+
* @returns {ProbeResult}
|
|
188
|
+
*/
|
|
189
|
+
function buildResult(snapshot, status) {
|
|
190
|
+
const classification = classifyProbeResult(snapshot);
|
|
191
|
+
return {
|
|
192
|
+
classification,
|
|
193
|
+
connected: snapshot.connected === true,
|
|
194
|
+
protocolValid: snapshot.protocolValid === true,
|
|
195
|
+
ready: classification === "ready",
|
|
196
|
+
viewId: status?.viewId ?? null,
|
|
197
|
+
instanceId: status?.instanceId ?? null,
|
|
198
|
+
state: snapshot.state ?? null,
|
|
199
|
+
errorCode: snapshot.errorCode ?? null,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** @param {unknown} err */
|
|
204
|
+
function errCode(err) {
|
|
205
|
+
if (err && typeof err === "object" && typeof /** @type {any} */ (err).code === "string") return /** @type {any} */ (err).code;
|
|
206
|
+
return "UNKNOWN";
|
|
207
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Control-socket client identity helpers (issue #103).
|
|
2
|
+
*
|
|
3
|
+
* Client ids travel in the `hello` handshake. `helloBookkeeping` turns the
|
|
4
|
+
* classification into the explicit socket policy: probes and the resident
|
|
5
|
+
* editor-state reporter stay out of `attachedClients` / `attachedEver`, and
|
|
6
|
+
* probes additionally never trigger a host.json write — counting or writing
|
|
7
|
+
* either would pin every host against warm-host reclaim (issue #75 / #103 §C /
|
|
8
|
+
* #130).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Read-only endpoint probe (attach resolver); never counts as attached. */
|
|
12
|
+
export const CLIENT_ID_PROBE = "probe";
|
|
13
|
+
/** Resident editor-state reporter inside a hosted child; never counts as attached. */
|
|
14
|
+
export const CLIENT_ID_EDITOR_REPORTER = "editor-reporter";
|
|
15
|
+
/**
|
|
16
|
+
* Resident frame-witness reporter inside a hosted child (issue #222 FW-03/FW-04).
|
|
17
|
+
* Bookkeeping-only like the editor reporter: it must never keep a host attached or flip
|
|
18
|
+
* attachedEver, or warm-host reclaim never fires.
|
|
19
|
+
*/
|
|
20
|
+
export const CLIENT_ID_FRAME_REPORTER = "frame-reporter";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {{ type?: string, clientId?: string } | null | undefined} msg
|
|
24
|
+
* @returns {"probe" | "editor-reporter" | "frame-reporter" | "client"}
|
|
25
|
+
*/
|
|
26
|
+
export function classifyClientHello(msg) {
|
|
27
|
+
const clientId = msg && typeof msg.clientId === "string" ? msg.clientId : "";
|
|
28
|
+
if (clientId === CLIENT_ID_PROBE) return "probe";
|
|
29
|
+
if (clientId === CLIENT_ID_EDITOR_REPORTER) return "editor-reporter";
|
|
30
|
+
if (clientId === CLIENT_ID_FRAME_REPORTER) return "frame-reporter";
|
|
31
|
+
return "client";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Socket bookkeeping implied by a hello classification (issue #103 §C / #130).
|
|
36
|
+
*
|
|
37
|
+
* `probe` and `editor-reporter` are bookkeeping-only: neither may keep
|
|
38
|
+
* `attachedClients` — whose sole source is the runner's `clients` set —
|
|
39
|
+
* non-zero, or warm-host reclaim never fires and hosts leak (issue #75). Both
|
|
40
|
+
* therefore leave `clients` at hello time, while their socket stays writable so
|
|
41
|
+
* probe replies and editor_state keep flowing.
|
|
42
|
+
*
|
|
43
|
+
* `persist: false` for probes is deliberate: the attach resolver probes every
|
|
44
|
+
* HOST_PROBE_RETRY_MS (see host-probe.mjs), so one host.json write per probe
|
|
45
|
+
* would amplify fenced writes — the exact cost the runner's probe-socket write
|
|
46
|
+
* suppression exists to avoid. Dropping the socket from `clients` already makes
|
|
47
|
+
* the in-memory count right, and the next heartbeat (<=1s) persists it.
|
|
48
|
+
*
|
|
49
|
+
* `suppressCloseWrite: true` is the other half of that suppression: close is the
|
|
50
|
+
* wrong moment to flush a record a probe never changed.
|
|
51
|
+
*
|
|
52
|
+
* An unrecognized kind falls back to the real-client row on purpose — counting
|
|
53
|
+
* an unknown connection is the conservative direction (it can only delay
|
|
54
|
+
* reclaim, never risk killing a host with a live client).
|
|
55
|
+
*
|
|
56
|
+
* @param {"probe" | "editor-reporter" | "frame-reporter" | "client"} kind
|
|
57
|
+
* @returns {{ keepInClients: boolean, registerReporter: boolean,
|
|
58
|
+
* flipAttachedEver: boolean, persist: boolean, suppressCloseWrite: boolean }}
|
|
59
|
+
*/
|
|
60
|
+
export function helloBookkeeping(kind) {
|
|
61
|
+
switch (kind) {
|
|
62
|
+
case "probe":
|
|
63
|
+
return { keepInClients: false, registerReporter: false, flipAttachedEver: false, persist: false, suppressCloseWrite: true };
|
|
64
|
+
case "editor-reporter":
|
|
65
|
+
return { keepInClients: false, registerReporter: true, flipAttachedEver: false, persist: true, suppressCloseWrite: false };
|
|
66
|
+
case "frame-reporter":
|
|
67
|
+
// Same policy as the editor reporter (issue #222 FW-04): bookkeeping-only, no
|
|
68
|
+
// attachEver flip, but the socket stays writable so enable/intent keep flowing.
|
|
69
|
+
return { keepInClients: false, registerReporter: false, flipAttachedEver: false, persist: true, suppressCloseWrite: false };
|
|
70
|
+
default:
|
|
71
|
+
return { keepInClients: true, registerReporter: false, flipAttachedEver: true, persist: true, suppressCloseWrite: false };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** ID generation for views and runs. */
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
/** @param {string} prefix @returns {string} e.g. "view_8f3a1c2b". */
|
|
5
|
+
export function genId(prefix) {
|
|
6
|
+
return `${prefix}_${randomBytes(5).toString("hex")}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** @returns {string} a new view (row) id. */
|
|
10
|
+
export const newViewId = () => genId("view");
|
|
11
|
+
|
|
12
|
+
/** @returns {string} a new run id. */
|
|
13
|
+
export const newRunId = () => genId("run");
|
|
14
|
+
|
|
15
|
+
/** @returns {string} a new follow-up queue item id. */
|
|
16
|
+
export const newFollowUpId = () => genId("follow");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Derive a short, filesystem-safe slug from a free-text task, for display names.
|
|
20
|
+
* @param {string} text
|
|
21
|
+
* @param {number} [maxWords]
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function slugifyTask(text, maxWords = 5) {
|
|
25
|
+
const words = String(text)
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[`'"]/g, "")
|
|
28
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
29
|
+
.trim()
|
|
30
|
+
.split(/\s+/)
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.slice(0, maxWords);
|
|
33
|
+
const slug = words.join("-");
|
|
34
|
+
return slug || "task";
|
|
35
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IME cursor-rect flicker fix (issue #28).
|
|
3
|
+
*
|
|
4
|
+
* pi-tui's doRender() emits each frame as separate terminal.write() calls:
|
|
5
|
+
*
|
|
6
|
+
* 1. ESC[?2026h ...content... ESC[?2026l (differential/full frame, sync block)
|
|
7
|
+
* 2. ESC[<n>A/B ESC[<col>G (positionHardwareCursor park write)
|
|
8
|
+
* 3. ESC[?25l (hideCursor - bypasses write())
|
|
9
|
+
*
|
|
10
|
+
* The park sequences sit OUTSIDE the synchronized-output block. Terminals that
|
|
11
|
+
* honor ?2026 (WezTerm et al.) present a frame and report the IME cursor
|
|
12
|
+
* rectangle at each ?2026l boundary, so every frame produces two cursor-rect
|
|
13
|
+
* reports at different positions (diff-write end vs parked input line) and the
|
|
14
|
+
* IME candidate window bounces at frame rate. E2E measured on WezTerm + fcitx5:
|
|
15
|
+
* ~20 position changes / 4s with the split writes, 0 with the park inside the
|
|
16
|
+
* block (see issue #28 for the full experiment).
|
|
17
|
+
*
|
|
18
|
+
* This module wraps a Terminal instance's write/hideCursor/showCursor at runtime
|
|
19
|
+
* and folds the out-of-block park/hide sequences back INSIDE the frame's sync
|
|
20
|
+
* block, re-emitting the frame as a single write. Content is byte-identical up
|
|
21
|
+
* to reordering of the trailing ?2026l; nothing is dropped or added.
|
|
22
|
+
*
|
|
23
|
+
* Safety properties (issue #28):
|
|
24
|
+
* - No match -> passthrough: any write that isn't a pure cursor-park/hide
|
|
25
|
+
* sequence flushes the held frame unchanged first, preserving byte order.
|
|
26
|
+
* - The three writes happen inside one synchronous doRender() stack, so a
|
|
27
|
+
* process.nextTick flush is enough to see them all; the added latency is
|
|
28
|
+
* sub-millisecond.
|
|
29
|
+
* - Uninstall restores the original methods; a WeakMap makes overlapping
|
|
30
|
+
* installs on the same terminal refcounted and idempotent.
|
|
31
|
+
* - Kill switch: AGENT_BOARD_IME_FIX=0 disables installation entirely.
|
|
32
|
+
*
|
|
33
|
+
* If pi-tui ever folds positionHardwareCursor into the sync block upstream,
|
|
34
|
+
* the "pure park sequence following a sync-end write" pattern stops matching
|
|
35
|
+
* and this wrapper degrades to a passthrough (one buffered write per frame,
|
|
36
|
+
* same bytes) - no behavior change.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const ESC = String.fromCharCode(27);
|
|
40
|
+
/** ESC[?2026l - end of a synchronized-output block. */
|
|
41
|
+
const SYNC_END = ESC + "[?2026l";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Cursor sequences positionHardwareCursor() emits after a frame: relative row
|
|
45
|
+
* moves (ESC[<n>A / ESC[<n>B, n optional) and an absolute column set
|
|
46
|
+
* (ESC[<col>G), plus the cursor visibility toggles (ESC[?25l/h). These are
|
|
47
|
+
* the ONLY sequences safe to fold into the block - anything else (line clears,
|
|
48
|
+
* absolute positioning, OSC/DCS queries, content) flushes the held frame.
|
|
49
|
+
*/
|
|
50
|
+
const PARK_SEQUENCE = new RegExp(
|
|
51
|
+
"^(?:" + ESC + "\\[\\d*[AB]|" + ESC + "\\[\\d+G|" + ESC + "\\[\\?25[hl])*$",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/** True when a write ends a synchronized-output block (pi-tui frame writes do). */
|
|
55
|
+
export function endsWithSyncEnd(data) {
|
|
56
|
+
return typeof data === "string" && data.endsWith(SYNC_END);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True when data is exclusively cursor park/visibility sequences (see above). */
|
|
60
|
+
export function isPureCursorParking(data) {
|
|
61
|
+
return typeof data === "string" && data.length > 0 && PARK_SEQUENCE.test(data);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Insert seq just before the trailing SYNC_END of a held frame write. */
|
|
65
|
+
export function mergeIntoSyncBlock(held, seq) {
|
|
66
|
+
return held.slice(0, held.length - SYNC_END.length) + seq + SYNC_END;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** terminal -> active wrapper, so overlapping installs share one wrapper. */
|
|
70
|
+
const activeWrappers = new WeakMap();
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Each wrapTerminalWrites() call returns its own guarded handle: idempotent
|
|
74
|
+
* per handle, refcounted across handles — the patch is torn down only when
|
|
75
|
+
* the LAST caller uninstalls (CR round 1, issue-1).
|
|
76
|
+
*/
|
|
77
|
+
function refcountedUninstall(entry) {
|
|
78
|
+
let done = false;
|
|
79
|
+
return () => {
|
|
80
|
+
if (done) return;
|
|
81
|
+
done = true;
|
|
82
|
+
entry.refs -= 1;
|
|
83
|
+
if (entry.refs > 0) return;
|
|
84
|
+
entry.teardown();
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Patch write/hideCursor/showCursor on a terminal instance so each frame's
|
|
90
|
+
* out-of-block park/hide sequences are folded into the frame's sync block and
|
|
91
|
+
* emitted as one write. Returns an uninstall function (idempotent, drops the
|
|
92
|
+
* patch when the last caller uninstalls).
|
|
93
|
+
*/
|
|
94
|
+
export function wrapTerminalWrites(terminal) {
|
|
95
|
+
if (!terminal || typeof terminal.write !== "function" || typeof terminal.hideCursor !== "function" || typeof terminal.showCursor !== "function") {
|
|
96
|
+
throw new Error("terminal must expose write/hideCursor/showCursor");
|
|
97
|
+
}
|
|
98
|
+
const existing = activeWrappers.get(terminal);
|
|
99
|
+
if (existing) {
|
|
100
|
+
existing.refs += 1;
|
|
101
|
+
return refcountedUninstall(existing);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const original = {
|
|
105
|
+
write: terminal.write.bind(terminal),
|
|
106
|
+
hideCursor: terminal.hideCursor.bind(terminal),
|
|
107
|
+
showCursor: terminal.showCursor.bind(terminal),
|
|
108
|
+
};
|
|
109
|
+
/** Frame write ending in SYNC_END, accumulating park/hide merges. */
|
|
110
|
+
let held = null;
|
|
111
|
+
let flushScheduled = false;
|
|
112
|
+
|
|
113
|
+
const flush = () => {
|
|
114
|
+
if (held === null) return;
|
|
115
|
+
const out = held;
|
|
116
|
+
held = null;
|
|
117
|
+
original.write(out);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const scheduleFlush = () => {
|
|
121
|
+
if (flushScheduled) return;
|
|
122
|
+
flushScheduled = true;
|
|
123
|
+
process.nextTick(() => {
|
|
124
|
+
flushScheduled = false;
|
|
125
|
+
flush();
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Fold a park/hide seq into the held frame, or emit it standalone. */
|
|
130
|
+
const mergeOrEmit = (seq, directEmit) => {
|
|
131
|
+
if (held !== null) {
|
|
132
|
+
held = mergeIntoSyncBlock(held, seq);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
directEmit();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
terminal.write = (data) => {
|
|
139
|
+
if (typeof data !== "string" || data.length === 0) {
|
|
140
|
+
flush();
|
|
141
|
+
original.write(data);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (held !== null) {
|
|
145
|
+
// Only a pure park/hide burst may join the held frame; anything
|
|
146
|
+
// else means this isn't a doRender park tail - flush unchanged.
|
|
147
|
+
if (isPureCursorParking(data)) {
|
|
148
|
+
held = mergeIntoSyncBlock(held, data);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
flush();
|
|
152
|
+
}
|
|
153
|
+
if (endsWithSyncEnd(data)) {
|
|
154
|
+
held = data;
|
|
155
|
+
scheduleFlush();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
original.write(data);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
terminal.hideCursor = () => mergeOrEmit(ESC + "[?25l", original.hideCursor);
|
|
162
|
+
terminal.showCursor = () => mergeOrEmit(ESC + "[?25h", original.showCursor);
|
|
163
|
+
|
|
164
|
+
const entry = { refs: 1, teardown: null };
|
|
165
|
+
entry.teardown = () => {
|
|
166
|
+
flush();
|
|
167
|
+
terminal.write = original.write;
|
|
168
|
+
terminal.hideCursor = original.hideCursor;
|
|
169
|
+
terminal.showCursor = original.showCursor;
|
|
170
|
+
activeWrappers.delete(terminal);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
activeWrappers.set(terminal, entry);
|
|
174
|
+
return refcountedUninstall(entry);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Install the coalescer on a TUI's terminal. Never throws: on any surprise
|
|
179
|
+
* (shape change upstream, kill switch) it returns null and behavior stays
|
|
180
|
+
* exactly as today.
|
|
181
|
+
*/
|
|
182
|
+
export function installImeCursorCoalesce(tui) {
|
|
183
|
+
if (process.env.AGENT_BOARD_IME_FIX === "0") return null;
|
|
184
|
+
try {
|
|
185
|
+
const terminal = tui && tui.terminal;
|
|
186
|
+
if (!terminal || typeof terminal.write !== "function" || typeof terminal.hideCursor !== "function" || typeof terminal.showCursor !== "function") {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return wrapTerminalWrites(terminal);
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve how to invoke the `pi` worker and the `node` runner across install shapes:
|
|
3
|
+
* - pi as `dist/cli.js` under system node → piCommand=node, piArgsPrefix=[cli.js]
|
|
4
|
+
* - pi as a bun-compiled binary → piCommand="pi" (on PATH)
|
|
5
|
+
* - generic node/bun runtime → piCommand=execPath
|
|
6
|
+
* Mirrors the logic in the official subagent example's getPiInvocation().
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
|
|
11
|
+
/** @returns {boolean} true when process.execPath is a generic node/bun runtime (not a compiled app). */
|
|
12
|
+
function isGenericRuntime() {
|
|
13
|
+
const name = path.basename(process.execPath).toLowerCase();
|
|
14
|
+
return /^(node|bun)(\.exe)?$/.test(name);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compute `{ piCommand, piArgsPrefix }` for spawning the pi worker.
|
|
19
|
+
* @returns {{ piCommand: string, piArgsPrefix: string[] }}
|
|
20
|
+
*/
|
|
21
|
+
export function resolvePiInvocation() {
|
|
22
|
+
const script = process.argv[1];
|
|
23
|
+
const isBunVirtual = typeof script === "string" && script.startsWith("/$bunfs/root/");
|
|
24
|
+
if (script && !isBunVirtual && existsSync(script)) {
|
|
25
|
+
// Running the pi cli script under a runtime — re-run the same script.
|
|
26
|
+
return { piCommand: process.execPath, piArgsPrefix: [script] };
|
|
27
|
+
}
|
|
28
|
+
if (!isGenericRuntime()) {
|
|
29
|
+
// Compiled pi binary invoked directly.
|
|
30
|
+
return { piCommand: process.execPath, piArgsPrefix: [] };
|
|
31
|
+
}
|
|
32
|
+
// Fallback: rely on `pi` being on PATH.
|
|
33
|
+
return { piCommand: "pi", piArgsPrefix: [] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a real `node` executable to run the detached `.mjs` runner.
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export function resolveNode() {
|
|
41
|
+
if (isGenericRuntime()) return process.execPath;
|
|
42
|
+
return "node";
|
|
43
|
+
}
|