@reticlehq/server 3.8.1 → 3.9.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/LICENSE +201 -104
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/bin/launch.js +13 -0
- package/bin/package.json +3 -0
- package/bin/reticle.js +66 -8
- package/dist/chromium-launch-options.d.ts +25 -0
- package/dist/chromium-launch-options.js +21 -1
- package/dist/command/cli/adhoc-verdict.js +34 -7
- package/dist/command/cli/answers/gate-exit.d.ts +6 -2
- package/dist/command/cli/answers/gate-exit.js +12 -2
- package/dist/command/cli/ci-hint.d.ts +44 -0
- package/dist/command/cli/ci-hint.js +72 -0
- package/dist/command/cli/cli-doctor.d.ts +6 -1
- package/dist/command/cli/cli-doctor.js +45 -4
- package/dist/command/cli/cli-flow-commands.d.ts +7 -1
- package/dist/command/cli/cli-flow-commands.js +39 -17
- package/dist/command/cli/cli-kill.d.ts +21 -0
- package/dist/command/cli/cli-kill.js +46 -3
- package/dist/command/cli/cli-parse.d.ts +5 -2
- package/dist/command/cli/cli-parse.js +10 -5
- package/dist/command/cli/cli-usage.js +3 -2
- package/dist/command/cli/cli-verify.d.ts +6 -1
- package/dist/command/cli/cli-verify.js +32 -13
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
- package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
- package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
- package/dist/command/cli/doctor/doctor-fix.js +89 -0
- package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/doctor-repairs.d.ts +64 -0
- package/dist/command/cli/doctor-repairs.js +214 -0
- package/dist/command/cli/drive/drive-command.js +3 -7
- package/dist/command/cli/first-use-checks.d.ts +41 -0
- package/dist/command/cli/first-use-checks.js +127 -0
- package/dist/command/cli/first-verdict-node.d.ts +3 -0
- package/dist/command/cli/first-verdict-node.js +189 -0
- package/dist/command/cli/first-verdict.d.ts +126 -0
- package/dist/command/cli/first-verdict.js +267 -0
- package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
- package/dist/command/cli/launch/sdk-version-match.js +3 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
- package/dist/command/cli/mcp-command.js +13 -4
- package/dist/command/cli/ports/port-holder.d.ts +2 -0
- package/dist/command/cli/ports/port-holder.js +11 -3
- package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
- package/dist/command/cli/setup-mcp-cli.js +98 -23
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +50 -17
- package/dist/command/cli/status-fields.js +17 -3
- package/dist/command/cli/try-command.d.ts +7 -0
- package/dist/command/cli/try-command.js +11 -3
- package/dist/command/cli.js +70 -33
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
- package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
- package/dist/command/daemon/daemon-argv.d.ts +9 -0
- package/dist/command/daemon/daemon-argv.js +11 -0
- package/dist/command/daemon/daemon.d.ts +10 -3
- package/dist/command/daemon/daemon.js +17 -6
- package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
- package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
- package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
- package/dist/command/daemon/lifetime/death-cause.js +130 -0
- package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
- package/dist/command/daemon/lifetime/heartbeat.js +4 -13
- package/dist/command/daemon/port-state.d.ts +14 -0
- package/dist/command/daemon/port-state.js +31 -0
- package/dist/command/setup/agent-configs.d.ts +2 -0
- package/dist/command/setup/agent-configs.js +42 -15
- package/dist/command/setup/agent-next-steps.d.ts +77 -0
- package/dist/command/setup/agent-next-steps.js +284 -0
- package/dist/command/setup/agent-writer.js +11 -6
- package/dist/command/setup/init/init-host.js +2 -0
- package/dist/command/setup/init/init-runtime.d.ts +6 -1
- package/dist/command/setup/init/init-runtime.js +57 -21
- package/dist/command/setup/mcp-launcher.d.ts +3 -0
- package/dist/command/setup/mcp-launcher.js +11 -0
- package/dist/command/setup/setup-command.js +30 -12
- package/dist/command/setup/setup-mcp.d.ts +8 -0
- package/dist/command/setup/setup-mcp.js +47 -20
- package/dist/command/setup/start-dev-server.d.ts +8 -0
- package/dist/command/setup/start-dev-server.js +43 -0
- package/dist/command/version/identity/server-version.d.ts +2 -0
- package/dist/command/version/identity/server-version.js +2 -0
- package/dist/command/version/version-skew.d.ts +7 -0
- package/dist/command/version/version-skew.js +13 -2
- package/dist/features/license/license.d.ts +11 -71
- package/dist/features/license/license.js +43 -136
- package/dist/features/visual/desktop-capture.d.ts +33 -0
- package/dist/features/visual/desktop-capture.js +94 -0
- package/dist/features/visual/step-shots.js +139 -35
- package/dist/features/visual/visual-tools.d.ts +0 -18
- package/dist/features/visual/visual-tools.js +2 -81
- package/dist/import-graph.js +5 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +14 -5
- package/dist/judgement/runs/run-ledger.d.ts +7 -5
- package/dist/judgement/runs/run-ledger.js +98 -69
- package/dist/judgement/runs/verification-sync.d.ts +6 -3
- package/dist/judgement/runs/verification-sync.js +21 -10
- package/dist/language/flows/flow-store-for-session.js +7 -5
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
- package/dist/language/flows/recording/tape/recordings.js +41 -44
- package/dist/language/flows/replay.d.ts +17 -7
- package/dist/language/flows/replay.js +95 -44
- package/dist/launch-chromium.d.ts +1 -0
- package/dist/launch-chromium.js +49 -12
- package/dist/memory/cloud/sync-screenshots.js +6 -1
- package/dist/portal/bridge/bridge.d.ts +13 -4
- package/dist/portal/bridge/bridge.js +10 -9
- package/dist/portal/input/drive-error.d.ts +6 -0
- package/dist/portal/input/drive-error.js +9 -0
- package/dist/portal/input/network-mock.js +3 -2
- package/dist/portal/input/real-input-press.d.ts +34 -0
- package/dist/portal/input/real-input-press.js +150 -0
- package/dist/portal/input/real-input.d.ts +21 -23
- package/dist/portal/input/real-input.js +153 -194
- package/dist/portal/input/viewport-tools.js +6 -3
- package/dist/portal/pool/browser-pool.d.ts +5 -1
- package/dist/portal/pool/browser-pool.js +32 -38
- package/dist/portal/pool/context-permissions.d.ts +20 -17
- package/dist/portal/pool/context-permissions.js +63 -1
- package/dist/portal/pool/csp-widen.d.ts +62 -0
- package/dist/portal/pool/csp-widen.js +132 -0
- package/dist/portal/pool/lease-page-error.d.ts +29 -0
- package/dist/portal/pool/lease-page-error.js +18 -0
- package/dist/portal/pool/page-error-journal.d.ts +57 -0
- package/dist/portal/pool/page-error-journal.js +181 -0
- package/dist/portal/pool/playwright-launcher.js +21 -8
- package/dist/portal/pool/pool-contract.d.ts +22 -0
- package/dist/portal/pool/pool-lease.d.ts +35 -2
- package/dist/portal/pool/pool-lease.js +23 -0
- package/dist/portal/pool/zero-install.js +69 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
- package/dist/portal/session/dev-server/dev-command.js +56 -8
- package/dist/portal/session/no-session-diagnosis.js +10 -3
- package/dist/portal/session/no-session-next-action.d.ts +12 -0
- package/dist/portal/session/no-session-next-action.js +44 -4
- package/dist/portal/session/no-session-watch.js +3 -1
- package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
- package/dist/portal/session/page-commands/undeclared-command.js +16 -7
- package/dist/portal/session/recorded-gaps.d.ts +1 -1
- package/dist/portal/session/recorded-gaps.js +5 -2
- package/dist/portal/session/session-manager.js +40 -21
- package/dist/portal/session/session.js +4 -3
- package/dist/portal/session/zero-install-first-call.d.ts +20 -1
- package/dist/portal/session/zero-install-first-call.js +34 -2
- package/dist/start-wiring.d.ts +29 -2
- package/dist/start-wiring.js +73 -9
- package/dist/surface/mcp/mcp-proxy.js +10 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
- package/dist/surface/mcp/peer/agents-connected.js +58 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
- package/dist/surface/tools/act/act-preflight.js +37 -17
- package/dist/surface/tools/act/act-target.js +3 -1
- package/dist/surface/tools/act/after-match.d.ts +7 -0
- package/dist/surface/tools/act/after-match.js +33 -0
- package/dist/surface/tools/act/resolve-target.d.ts +2 -0
- package/dist/surface/tools/act/resolve-target.js +35 -8
- package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
- package/dist/surface/tools/act/sequence-grade.js +8 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -0
- package/dist/surface/tools/act-tools.js +12 -12
- package/dist/surface/tools/assert/assert-verdict.js +28 -1
- package/dist/surface/tools/browser-tools.js +75 -7
- package/dist/surface/tools/dynamic-tools.js +2 -1
- package/dist/surface/tools/empty-session-list.d.ts +2 -0
- package/dist/surface/tools/empty-session-list.js +4 -2
- package/dist/surface/tools/error-recovery.js +43 -22
- package/dist/surface/tools/invoke-tool.js +27 -20
- package/dist/surface/tools/lease-hint.d.ts +7 -0
- package/dist/surface/tools/lease-hint.js +10 -3
- package/dist/surface/tools/lease-readiness.d.ts +21 -0
- package/dist/surface/tools/lease-readiness.js +47 -0
- package/dist/surface/tools/lease-seed.d.ts +2 -0
- package/dist/surface/tools/lease-seed.js +2 -1
- package/dist/surface/tools/lease-tools.js +43 -15
- package/dist/surface/tools/real-input-attempt.js +70 -31
- package/dist/surface/tools/repeat-hint.d.ts +40 -0
- package/dist/surface/tools/repeat-hint.js +171 -0
- package/dist/surface/tools/run-report.d.ts +2 -2
- package/dist/surface/tools/run-report.js +27 -15
- package/dist/surface/tools/tool-kit.d.ts +12 -0
- package/dist/surface/tools/tool-kit.js +7 -0
- package/dist/surface/tools/tools.js +26 -13
- package/dist/telemetry/argument-shape.js +16 -7
- package/dist/telemetry/cli-telemetry.d.ts +10 -1
- package/dist/telemetry/cli-telemetry.js +15 -0
- package/dist/telemetry/init-telemetry.d.ts +2 -1
- package/dist/telemetry/init-telemetry.js +47 -2
- package/dist/telemetry/install-trace.js +23 -4
- package/dist/telemetry/onboarding-funnel.d.ts +19 -2
- package/dist/telemetry/onboarding-funnel.js +25 -3
- package/dist/telemetry/proxy-telemetry.d.ts +2 -1
- package/dist/telemetry/proxy-telemetry.js +2 -2
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +67 -27
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +60 -28
- package/dist/telemetry/tool-refused.d.ts +2 -2
- package/dist/telemetry/tool-refused.js +9 -2
- package/docs/architecture.md +3 -6
- package/docs/cli/doctor.mdx +41 -2
- package/docs/cli/help.mdx +4 -2
- package/docs/cli/license.mdx +4 -5
- package/docs/cli/setup.mdx +13 -2
- package/docs/cli/status.mdx +4 -0
- package/docs/cli.mdx +1 -1
- package/docs/deploy-checks.md +25 -0
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +15 -20
- package/docs/environment.mdx +1 -0
- package/docs/first-drive.md +1 -1
- package/docs/install-manual.mdx +16 -1
- package/docs/packages/engine.mdx +1 -1
- package/docs/packages/react.mdx +14 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +1 -0
- package/docs/packages.mdx +1 -1
- package/docs/platform-integration.md +1 -1
- package/docs/skill.md +1 -1
- package/docs/telemetry.md +4 -2
- package/package.json +14 -10
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
|
@@ -178,7 +178,7 @@ const INIT_CMD = '`npx @reticlehq/server init`';
|
|
|
178
178
|
* against it, and it has to be read before the "nothing is listening" sentence rather than after —
|
|
179
179
|
* the reader acts on the first claim.
|
|
180
180
|
*/
|
|
181
|
-
function slowListenerClause(facts) {
|
|
181
|
+
function slowListenerClause(facts, after = '') {
|
|
182
182
|
const slow = facts.slowListeners ?? [];
|
|
183
183
|
if (0 === slow.length)
|
|
184
184
|
return '';
|
|
@@ -186,7 +186,7 @@ function slowListenerClause(facts) {
|
|
|
186
186
|
const subject = 1 === slow.length ? `Port ${ports} ACCEPTED` : `Ports ${ports} ACCEPTED`;
|
|
187
187
|
return (`${subject} a connection but did not answer in time, which is what a server-rendered dev ` +
|
|
188
188
|
'server compiling its first route looks like — so something IS running there and you should ' +
|
|
189
|
-
|
|
189
|
+
`open it rather than start it.${after}`);
|
|
190
190
|
}
|
|
191
191
|
/**
|
|
192
192
|
* The commonest first-run state there is (#320).
|
|
@@ -481,7 +481,7 @@ export function explainNoSession(facts) {
|
|
|
481
481
|
// is expected rather than diagnostic.
|
|
482
482
|
if (!initialized) {
|
|
483
483
|
return reason(NoSessionReason.NO_LISTENER_NO_CONFIG, 'no browser session connected. Two things to weigh, and neither of them is proof. ' +
|
|
484
|
-
`(1) ${slowListenerClause(facts)}Nothing is listening on the ports Reticle scans, so the ` +
|
|
484
|
+
`(1) ${slowListenerClause(facts, ' Setting that aside: ')}Nothing is listening on the ports Reticle scans, so the ` +
|
|
485
485
|
'dev server may not be running: START IT YOURSELF, in the background, using the command ' +
|
|
486
486
|
'in `next_action`, and tell the human in one line that it is running. ' +
|
|
487
487
|
`(2) There is no \`.reticle.json\` in ${where}, which is the file ${INIT_CMD} writes, so ` +
|
|
@@ -493,6 +493,13 @@ export function explainNoSession(facts) {
|
|
|
493
493
|
'the Vite or Babel plugin carries the SDK without that file at all.' +
|
|
494
494
|
`${searchedClause(facts)}${siblingListenerClause(facts)} ${leaseAdvice(URL_THEN_LEASE, facts)}`);
|
|
495
495
|
}
|
|
496
|
+
// A wired project whose own port accepted a connection and answered nothing yet is compiling,
|
|
497
|
+
// not down. "Start it yourself" here starts a second dev server.
|
|
498
|
+
if ((facts.slowListeners ?? []).length > 0) {
|
|
499
|
+
return reason(NoSessionReason.NO_LISTENER, `${stallClause(facts)}no browser session connected, and this daemon has never seen one. ` +
|
|
500
|
+
`${slowListenerClause(facts)} Do not start a second dev server. ` +
|
|
501
|
+
`Wait for the first compile to finish, then open the app (the command is in \`next_action\`). ${RETRY}`, `${leaseAdvice(URL_THEN_LEASE, facts)}${siblingListenerClause(facts)}`);
|
|
502
|
+
}
|
|
496
503
|
return reason(NoSessionReason.NO_LISTENER, `${stallClause(facts)}no browser session connected, and this daemon has never seen one. ${openTheApp(facts)} ` +
|
|
497
504
|
'Nothing is listening on the ports Reticle scans either, and the usual reason is a dev ' +
|
|
498
505
|
'server that is not running: start it yourself in the background with the command in ' +
|
|
@@ -102,6 +102,11 @@ interface NextActionFacts {
|
|
|
102
102
|
* project's own runner (see `initCommandIn`). Absent means the generic npx spelling.
|
|
103
103
|
*/
|
|
104
104
|
init?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Several direct child folders are apps with a dev script, and this folder has no package.json.
|
|
107
|
+
* Absent or short means there is no choice to hand the agent. See `childDevApps`.
|
|
108
|
+
*/
|
|
109
|
+
childApps?: readonly string[];
|
|
105
110
|
}
|
|
106
111
|
/** The port a tombstone URL names, when it names one — anything unparseable names no port. */
|
|
107
112
|
export declare function portOfUrl(url: string): number | undefined;
|
|
@@ -123,6 +128,13 @@ export declare const INIT_COMMAND = "npx @reticlehq/server init";
|
|
|
123
128
|
* agent asks first. The lease changes nothing, so it needs no question.
|
|
124
129
|
*/
|
|
125
130
|
export declare function wiringConsent(init?: string): string;
|
|
131
|
+
/**
|
|
132
|
+
* The offer for the first session in an unwired web app: check it now with the lease, then ask
|
|
133
|
+
* before `init`. The one text for the plugin's SessionStart hook and the first tool call of each
|
|
134
|
+
* agent connection. The hook cannot import this, so it keeps a copy, and
|
|
135
|
+
* `plugin-init-nudge.test.ts` fails when the two are not the same.
|
|
136
|
+
*/
|
|
137
|
+
export declare function firstSessionOffer(init?: string): string;
|
|
126
138
|
/** The zero-install call, with the URL only when it can be named without guessing. */
|
|
127
139
|
export declare function zeroInstallLease(url: string | undefined): string;
|
|
128
140
|
export declare function nextActionFor(facts: NextActionFacts): NoSessionNextAction;
|
|
@@ -18,8 +18,9 @@ import { detectNonJsEcosystem, noPackageJsonMessage } from '@reticlehq/init';
|
|
|
18
18
|
export function portOfUrl(url) {
|
|
19
19
|
try {
|
|
20
20
|
const raw = new URL(url).port;
|
|
21
|
-
if ('' === raw)
|
|
21
|
+
if ('' === raw) {
|
|
22
22
|
return undefined;
|
|
23
|
+
}
|
|
23
24
|
const port = Number(raw);
|
|
24
25
|
return Number.isSafeInteger(port) && port > 0 ? port : undefined;
|
|
25
26
|
}
|
|
@@ -45,8 +46,9 @@ export function departedPortGone(facts) {
|
|
|
45
46
|
* for a url that does not parse, or one with no port to name (a desktop webview's opaque origin).
|
|
46
47
|
*/
|
|
47
48
|
function reopenableUrl(raw) {
|
|
48
|
-
if (raw === undefined)
|
|
49
|
+
if (raw === undefined) {
|
|
49
50
|
return undefined;
|
|
51
|
+
}
|
|
50
52
|
let url;
|
|
51
53
|
try {
|
|
52
54
|
url = new URL(raw);
|
|
@@ -54,10 +56,12 @@ function reopenableUrl(raw) {
|
|
|
54
56
|
catch {
|
|
55
57
|
return undefined;
|
|
56
58
|
}
|
|
57
|
-
if ('http:' !== url.protocol && 'https:' !== url.protocol)
|
|
59
|
+
if ('http:' !== url.protocol && 'https:' !== url.protocol) {
|
|
58
60
|
return undefined;
|
|
59
|
-
|
|
61
|
+
}
|
|
62
|
+
for (const param of Object.values(RETICLE_URL_PARAM)) {
|
|
60
63
|
url.searchParams.delete(param);
|
|
64
|
+
}
|
|
61
65
|
const port = '' === url.port ? ('https:' === url.protocol ? 443 : 80) : Number(url.port);
|
|
62
66
|
return { url: url.toString(), port };
|
|
63
67
|
}
|
|
@@ -78,6 +82,17 @@ export function wiringConsent(init = INIT_COMMAND) {
|
|
|
78
82
|
'build config and starts the dev server. Ask the user before you wire the app. If they ' +
|
|
79
83
|
`agree, run \`${init}\` in the app's folder. If they decline, do not ask again.`);
|
|
80
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The offer for the first session in an unwired web app: check it now with the lease, then ask
|
|
87
|
+
* before `init`. The one text for the plugin's SessionStart hook and the first tool call of each
|
|
88
|
+
* agent connection. The hook cannot import this, so it keeps a copy, and
|
|
89
|
+
* `plugin-init-nudge.test.ts` fails when the two are not the same.
|
|
90
|
+
*/
|
|
91
|
+
export function firstSessionOffer(init = INIT_COMMAND) {
|
|
92
|
+
return (`This web app can be checked now with no install: \`${zeroInstallLease(undefined)}\` opens ` +
|
|
93
|
+
"the running app in Reticle's own browser and changes no file. To keep Reticle wired to this " +
|
|
94
|
+
`app: ${wiringConsent(init)}`);
|
|
95
|
+
}
|
|
81
96
|
const OPEN_COMMAND = `${CLI} open`;
|
|
82
97
|
const LOCALHOST = 'http://localhost';
|
|
83
98
|
/**
|
|
@@ -243,6 +258,19 @@ export function nextActionFor(facts) {
|
|
|
243
258
|
'args: { action: "acquire", url, projectId } }, or restart the daemon from the app\'s own directory.',
|
|
244
259
|
};
|
|
245
260
|
}
|
|
261
|
+
// A wired project whose only port accepted a connection and answered nothing yet: a dev server
|
|
262
|
+
// compiling its first route. It is up, so a start command would start a second one.
|
|
263
|
+
const compiling = facts.slowListeners?.[0];
|
|
264
|
+
if (true === facts.initialized && 0 === facts.listening.length && compiling !== undefined) {
|
|
265
|
+
return {
|
|
266
|
+
action: NoSessionAction.OPEN_APP,
|
|
267
|
+
command: `${OPEN_COMMAND} ${LOCALHOST}:${String(compiling)}`,
|
|
268
|
+
port: compiling,
|
|
269
|
+
reason: `port ${String(compiling)} accepted a connection but did not answer in time, which is a dev ` +
|
|
270
|
+
'server compiling its first route. The app IS running. Do not start a second dev server. ' +
|
|
271
|
+
'Wait for the compile to finish, then open it.',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
246
274
|
if (0 === facts.listening.length || departedPortIsGone) {
|
|
247
275
|
const dev = facts.dev;
|
|
248
276
|
// The start advice below follows a departed session whose port is gone. Name it, so the restart
|
|
@@ -255,6 +283,18 @@ export function nextActionFor(facts) {
|
|
|
255
283
|
? "nothing answers on the port this project's app was last on, so the app is probably not running — "
|
|
256
284
|
: notThisProject(facts.otherListeners);
|
|
257
285
|
if (dev === undefined) {
|
|
286
|
+
const childApps = facts.childApps ?? [];
|
|
287
|
+
if (1 < childApps.length) {
|
|
288
|
+
return {
|
|
289
|
+
action: NoSessionAction.START_DEV_SERVER,
|
|
290
|
+
reason: notRunning +
|
|
291
|
+
`but this folder has no package.json, and ${childApps.length} child folders are apps ` +
|
|
292
|
+
`with a dev script: ${childApps.join(', ')}. Reticle cannot tell which one you mean, so ` +
|
|
293
|
+
'there is no command to hand you. Pick one, then run Reticle from that folder or ask ' +
|
|
294
|
+
'the human which app to start.' +
|
|
295
|
+
stoppedNote,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
258
298
|
if (facts.exists !== undefined && !facts.exists('package.json')) {
|
|
259
299
|
const ecosystem = detectNonJsEcosystem(facts.exists);
|
|
260
300
|
if (ecosystem !== undefined) {
|
|
@@ -17,7 +17,7 @@ import { join, resolve } from 'node:path';
|
|
|
17
17
|
import { callingClientDirectory } from '../../hooks/client-directory.js';
|
|
18
18
|
import { registeredElsewhere } from '../../memory/recall/registered-projects.js';
|
|
19
19
|
import { explainNoSession } from './no-session-diagnosis.js';
|
|
20
|
-
import { detectDevCommandInProject, initCommandIn } from './dev-server/dev-command.js';
|
|
20
|
+
import { childDevApps, detectDevCommandInProject, initCommandIn, } from './dev-server/dev-command.js';
|
|
21
21
|
import { nextActionFor, portOfUrl, renderNextAction } from './no-session-next-action.js';
|
|
22
22
|
import { DEV_SERVER_PORTS, readProjectFramework, readProjectId, readProjectIsDesktop, readProjectPort, } from '../../command/cli/ports/resolve/cli-port.js';
|
|
23
23
|
import { discoverProjectConfigs } from '../../command/cli/config/config-discovery.js';
|
|
@@ -480,6 +480,7 @@ export function startNoSessionWatch(options) {
|
|
|
480
480
|
...(known.departedTo === undefined ? {} : { departedTo: known.departedTo }),
|
|
481
481
|
}),
|
|
482
482
|
dev: detectDevCommandInProject(dir),
|
|
483
|
+
childApps: childDevApps(dir),
|
|
483
484
|
init: initCommandIn(dir),
|
|
484
485
|
});
|
|
485
486
|
return { facts, next };
|
|
@@ -540,6 +541,7 @@ export function startNoSessionWatch(options) {
|
|
|
540
541
|
// Read when asked, like everything else here: a `package.json` can gain a dev script, and a
|
|
541
542
|
// daemon that cached "there is none" at boot would keep saying so for the rest of the day.
|
|
542
543
|
dev: detectDevCommandInProject(directory),
|
|
544
|
+
childApps: childDevApps(directory),
|
|
543
545
|
// This project's own runner and app folder, so the agent can wire it itself.
|
|
544
546
|
init: initCommandIn(directory),
|
|
545
547
|
});
|
|
@@ -18,8 +18,14 @@
|
|
|
18
18
|
* answers rather than a set lookup at the call site.
|
|
19
19
|
*/
|
|
20
20
|
import { type CommandResult } from '@reticlehq/core';
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Why nothing was sent, in a sentence, or undefined when the command should go.
|
|
23
|
+
*
|
|
24
|
+
* `behind` is the caller's sentence for a page SDK older than this daemon. A newer command
|
|
25
|
+
* (`render`, `match`) is then missing because of the version, and the list of commands alone
|
|
26
|
+
* does not say that.
|
|
27
|
+
*/
|
|
28
|
+
export declare function refusalForUndeclared(name: string, declared: readonly string[] | undefined, behind?: string): string | undefined;
|
|
23
29
|
/**
|
|
24
30
|
* The refusal as a finished result, or undefined when the command should be sent.
|
|
25
31
|
*
|
|
@@ -27,4 +33,4 @@ export declare function refusalForUndeclared(name: string, declared: readonly st
|
|
|
27
33
|
* cannot get the envelope subtly wrong -- an `ok: true` refusal would be read as a command that
|
|
28
34
|
* worked, which is the one shape this must never produce.
|
|
29
35
|
*/
|
|
30
|
-
export declare function refusedResult(name: string, declared: readonly string[] | undefined, id: string): CommandResult | undefined;
|
|
36
|
+
export declare function refusedResult(name: string, declared: readonly string[] | undefined, id: string, behind?: string): CommandResult | undefined;
|
|
@@ -18,16 +18,25 @@
|
|
|
18
18
|
* answers rather than a set lookup at the call site.
|
|
19
19
|
*/
|
|
20
20
|
import { MessageKind } from '@reticlehq/core';
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Why nothing was sent, in a sentence, or undefined when the command should go.
|
|
23
|
+
*
|
|
24
|
+
* `behind` is the caller's sentence for a page SDK older than this daemon. A newer command
|
|
25
|
+
* (`render`, `match`) is then missing because of the version, and the list of commands alone
|
|
26
|
+
* does not say that.
|
|
27
|
+
*/
|
|
28
|
+
export function refusalForUndeclared(name, declared, behind) {
|
|
23
29
|
// Too old to say. Unknown, never "nothing".
|
|
24
|
-
if (declared === undefined)
|
|
30
|
+
if (declared === undefined) {
|
|
25
31
|
return undefined;
|
|
26
|
-
|
|
32
|
+
}
|
|
33
|
+
if (declared.includes(name)) {
|
|
27
34
|
return undefined;
|
|
35
|
+
}
|
|
28
36
|
return (`this page does not serve "${name}". It declared ${String(declared.length)} command(s) when ` +
|
|
29
37
|
`it connected, and this is not one of them: ${declared.join(', ')}. Nothing was sent, so ` +
|
|
30
|
-
'nothing happened -- this is a refusal, not a failure of the app.'
|
|
38
|
+
'nothing happened -- this is a refusal, not a failure of the app.' +
|
|
39
|
+
(behind === undefined ? '' : ` ${behind}`));
|
|
31
40
|
}
|
|
32
41
|
/**
|
|
33
42
|
* The refusal as a finished result, or undefined when the command should be sent.
|
|
@@ -36,8 +45,8 @@ export function refusalForUndeclared(name, declared) {
|
|
|
36
45
|
* cannot get the envelope subtly wrong -- an `ok: true` refusal would be read as a command that
|
|
37
46
|
* worked, which is the one shape this must never produce.
|
|
38
47
|
*/
|
|
39
|
-
export function refusedResult(name, declared, id) {
|
|
40
|
-
const error = refusalForUndeclared(name, declared);
|
|
48
|
+
export function refusedResult(name, declared, id, behind) {
|
|
49
|
+
const error = refusalForUndeclared(name, declared, behind);
|
|
41
50
|
return error === undefined
|
|
42
51
|
? undefined
|
|
43
52
|
: { kind: MessageKind.COMMAND_RESULT, id, ok: false, error };
|
|
@@ -49,7 +49,7 @@ export declare function probeMarkers(tabs: readonly ProbedTab[]): Promise<boolea
|
|
|
49
49
|
/**
|
|
50
50
|
* The `engineBlindSpots` field for a verdict result: what this engine could not observe in the
|
|
51
51
|
* session so far. Notes this verdict's own comparison first, so a `compare` clears the arithmetic
|
|
52
|
-
* spot on the result that ran it. `claims`: the inline intent,
|
|
52
|
+
* spot on the result that ran it. `claims`: the inline intent, or else the run's declared intents.
|
|
53
53
|
*/
|
|
54
54
|
export declare function engineBlindSpotsField(sessionId: string, predicate: Predicate | undefined, intent: unknown, declared: readonly {
|
|
55
55
|
statement: string;
|
|
@@ -162,12 +162,15 @@ const comparedSeen = new Set();
|
|
|
162
162
|
/**
|
|
163
163
|
* The `engineBlindSpots` field for a verdict result: what this engine could not observe in the
|
|
164
164
|
* session so far. Notes this verdict's own comparison first, so a `compare` clears the arithmetic
|
|
165
|
-
* spot on the result that ran it. `claims`: the inline intent,
|
|
165
|
+
* spot on the result that ran it. `claims`: the inline intent, or else the run's declared intents.
|
|
166
166
|
*/
|
|
167
167
|
export function engineBlindSpotsField(sessionId, predicate, intent, declared) {
|
|
168
168
|
if (computesComparison(predicate))
|
|
169
169
|
comparedSeen.add(sessionId);
|
|
170
|
-
|
|
170
|
+
// A call that names its own intent is judged on that intent alone. The run's open intents can be
|
|
171
|
+
// an earlier task's, and judging them reported that task's claim on this one (#1938).
|
|
172
|
+
const own = text(intent);
|
|
173
|
+
const claims = own === undefined ? declared.map((each) => each.statement) : [own];
|
|
171
174
|
return {
|
|
172
175
|
[ENGINE_BLIND_SPOTS_FIELD]: engineBlindSpots({
|
|
173
176
|
canvasSeen: canvasSeen.has(sessionId),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NO_SESSION_CONNECTED_ERROR, originOf, samePath } from '@reticlehq/core';
|
|
1
|
+
import { NO_SESSION_CONNECTED_ERROR, NO_SESSION_NAVIGATE_FIX, originOf, samePath, } from '@reticlehq/core';
|
|
2
2
|
import { notePendingNoSessionReason } from '../../telemetry/tool-refused.js';
|
|
3
3
|
import { declareDrivenRedactionKeys, forgetDrivenRedactionKeys, } from '../input/driven-redaction.js';
|
|
4
4
|
import { Session } from './session.js';
|
|
@@ -14,8 +14,9 @@ export { originOf };
|
|
|
14
14
|
* no scope, return the list unchanged (legacy single-/multi-session behavior).
|
|
15
15
|
*/
|
|
16
16
|
function scopeSessions(sessions, scope) {
|
|
17
|
-
if (scope === undefined)
|
|
17
|
+
if (scope === undefined) {
|
|
18
18
|
return sessions;
|
|
19
|
+
}
|
|
19
20
|
if (scope.projectId !== undefined) {
|
|
20
21
|
return sessions.filter((s) => s.projectId === scope.projectId);
|
|
21
22
|
}
|
|
@@ -124,8 +125,9 @@ export class SessionManager {
|
|
|
124
125
|
}
|
|
125
126
|
add(session) {
|
|
126
127
|
this.#everConnected = true;
|
|
127
|
-
if (session.projectId !== undefined)
|
|
128
|
+
if (session.projectId !== undefined) {
|
|
128
129
|
this.#connectedProjects.add(session.projectId);
|
|
130
|
+
}
|
|
129
131
|
// Ordering, not lifetime: this is what makes a remembered refusal distinguishable from a live
|
|
130
132
|
// one. Here for the same reason `#everConnected` is — `add` is the one method every path that
|
|
131
133
|
// registers a session goes through.
|
|
@@ -177,8 +179,9 @@ export class SessionManager {
|
|
|
177
179
|
}
|
|
178
180
|
#parked = new Set();
|
|
179
181
|
remove(session) {
|
|
180
|
-
if (this.#sessions.get(session.id) !== session)
|
|
182
|
+
if (this.#sessions.get(session.id) !== session) {
|
|
181
183
|
return false;
|
|
184
|
+
}
|
|
182
185
|
session.rejectAll(SESSION_DISCONNECTED_REASON);
|
|
183
186
|
forgetDrivenRedactionKeys(session.id);
|
|
184
187
|
// Recorded, not forgotten: a session that comes back needs its gap measured, and a listing after
|
|
@@ -189,10 +192,12 @@ export class SessionManager {
|
|
|
189
192
|
}
|
|
190
193
|
#rememberTombstone(session) {
|
|
191
194
|
const origin = originOf(session.url);
|
|
192
|
-
if (origin === undefined)
|
|
195
|
+
if (origin === undefined) {
|
|
193
196
|
return;
|
|
194
|
-
|
|
197
|
+
}
|
|
198
|
+
if (this.#tombstones.has(session.id)) {
|
|
195
199
|
this.#tombstones.delete(session.id);
|
|
200
|
+
}
|
|
196
201
|
this.#siblingsAtDeparture.set(session.id, new Set([...this.#sessions.values()]
|
|
197
202
|
.filter((s) => s !== session && openedBeforeDeparture(s, session))
|
|
198
203
|
.map((s) => s.id)));
|
|
@@ -206,16 +211,18 @@ export class SessionManager {
|
|
|
206
211
|
});
|
|
207
212
|
while (this.#tombstones.size > MAX_TOMBSTONES) {
|
|
208
213
|
const oldest = this.#tombstones.keys().next().value;
|
|
209
|
-
if (oldest === undefined)
|
|
214
|
+
if (oldest === undefined) {
|
|
210
215
|
break;
|
|
216
|
+
}
|
|
211
217
|
this.#tombstones.delete(oldest);
|
|
212
218
|
this.#siblingsAtDeparture.delete(oldest);
|
|
213
219
|
}
|
|
214
220
|
}
|
|
215
221
|
#successorOf(sessionId) {
|
|
216
222
|
const departed = this.#tombstones.get(sessionId);
|
|
217
|
-
if (departed === undefined)
|
|
223
|
+
if (departed === undefined) {
|
|
218
224
|
return undefined;
|
|
225
|
+
}
|
|
219
226
|
const siblings = this.#siblingsAtDeparture.get(sessionId);
|
|
220
227
|
const picked = pickDocumentSuccessor(this.all()
|
|
221
228
|
.filter((s) => true !== siblings?.has(s.id))
|
|
@@ -224,8 +231,9 @@ export class SessionManager {
|
|
|
224
231
|
url: s.url,
|
|
225
232
|
...(s.projectId === undefined ? {} : { projectId: s.projectId }),
|
|
226
233
|
})), departed);
|
|
227
|
-
if (picked === undefined)
|
|
234
|
+
if (picked === undefined) {
|
|
228
235
|
return undefined;
|
|
236
|
+
}
|
|
229
237
|
return this.#sessions.get(picked.id);
|
|
230
238
|
}
|
|
231
239
|
get(sessionId) {
|
|
@@ -396,8 +404,9 @@ export class SessionManager {
|
|
|
396
404
|
*/
|
|
397
405
|
noteClosure(reason, at, page) {
|
|
398
406
|
this.#recentClosures.push({ at, reason, ...(page === undefined ? {} : { page }) });
|
|
399
|
-
if (this.#recentClosures.length > MAX_REMEMBERED_CLOSURES)
|
|
407
|
+
if (this.#recentClosures.length > MAX_REMEMBERED_CLOSURES) {
|
|
400
408
|
this.#recentClosures.shift();
|
|
409
|
+
}
|
|
401
410
|
this.#connectedSinceLastClosure = false;
|
|
402
411
|
}
|
|
403
412
|
/** The most recent bridge-initiated close, if any. */
|
|
@@ -443,8 +452,9 @@ export class SessionManager {
|
|
|
443
452
|
*/
|
|
444
453
|
lastKnown() {
|
|
445
454
|
let last;
|
|
446
|
-
for (const record of this.#tombstones.values())
|
|
455
|
+
for (const record of this.#tombstones.values()) {
|
|
447
456
|
last = record;
|
|
457
|
+
}
|
|
448
458
|
return last;
|
|
449
459
|
}
|
|
450
460
|
/**
|
|
@@ -456,8 +466,9 @@ export class SessionManager {
|
|
|
456
466
|
lastKnownFor(projectId) {
|
|
457
467
|
let last;
|
|
458
468
|
for (const record of this.#tombstones.values()) {
|
|
459
|
-
if (record.projectId === projectId)
|
|
469
|
+
if (record.projectId === projectId) {
|
|
460
470
|
last = record;
|
|
471
|
+
}
|
|
461
472
|
}
|
|
462
473
|
return last;
|
|
463
474
|
}
|
|
@@ -496,9 +507,10 @@ export class SessionManager {
|
|
|
496
507
|
const ended = sessionId.startsWith(LEASE_ID_PREFIX)
|
|
497
508
|
? ` Lease '${sessionId}' has ended (it expired or was released); lease a new tab to go on.`
|
|
498
509
|
: '';
|
|
499
|
-
if (0 === own.length)
|
|
510
|
+
if (0 === own.length) {
|
|
500
511
|
return (`no connected session with id '${sessionId}'.${ended} ` +
|
|
501
512
|
'Omit sessionId to let Reticle scope to your project, or lease a tab of your own.');
|
|
513
|
+
}
|
|
502
514
|
const named = own.map((s) => `'${s.id}' (${s.url})`).join(', ');
|
|
503
515
|
return (`no connected session with id '${sessionId}'.${ended} Your app's tabs: ${named}. ` +
|
|
504
516
|
'Retry with one of those, or omit sessionId entirely and let Reticle scope to your project.');
|
|
@@ -528,7 +540,7 @@ export class SessionManager {
|
|
|
528
540
|
// diagnosis is computed HERE and the refusal is classified from the message downstream, so
|
|
529
541
|
// without this every no-session refusal lands in one undifferentiated bucket (#615).
|
|
530
542
|
notePendingNoSessionReason(this.#noSessionReason?.());
|
|
531
|
-
throw new Error(`${hint ?? NO_SESSION_CONNECTED_ERROR}${refusal}`);
|
|
543
|
+
throw new Error(`${hint ?? NO_SESSION_CONNECTED_ERROR}${refusal}${NO_SESSION_NAVIGATE_FIX}`);
|
|
532
544
|
}
|
|
533
545
|
// Scope to the agent's active project FIRST, so a stray tab from another app/origin (e.g. a
|
|
534
546
|
// leftover dashboard on a different port) is structurally unselectable — it never enters the
|
|
@@ -541,8 +553,9 @@ export class SessionManager {
|
|
|
541
553
|
const byCheckout = this.#checkoutScope === undefined
|
|
542
554
|
? { sessions: inProject }
|
|
543
555
|
: scopeToCallerCheckout(inProject, this.#checkoutScope.caller(), this.#checkoutScope.ofTab);
|
|
544
|
-
if (byCheckout.refusal !== undefined)
|
|
556
|
+
if (byCheckout.refusal !== undefined) {
|
|
545
557
|
throw new Error(byCheckout.refusal);
|
|
558
|
+
}
|
|
546
559
|
const all = byCheckout.sessions;
|
|
547
560
|
if (0 === all.length) {
|
|
548
561
|
// Sessions exist, but none belong to the scoped project — never fall back to a foreign tab.
|
|
@@ -553,8 +566,9 @@ export class SessionManager {
|
|
|
553
566
|
}
|
|
554
567
|
if (1 === all.length) {
|
|
555
568
|
const [only] = all;
|
|
556
|
-
if (only === undefined)
|
|
569
|
+
if (only === undefined) {
|
|
557
570
|
throw new Error('session lookup failed');
|
|
571
|
+
}
|
|
558
572
|
only.markAgentActivity();
|
|
559
573
|
return only;
|
|
560
574
|
}
|
|
@@ -601,8 +615,9 @@ export class SessionManager {
|
|
|
601
615
|
// Sort candidates by recency (ascending lastSeenMs = most recently active first).
|
|
602
616
|
candidates.sort((a, b) => a.ms - b.ms);
|
|
603
617
|
const [best, runnerUp] = candidates;
|
|
604
|
-
if (best === undefined)
|
|
618
|
+
if (best === undefined) {
|
|
605
619
|
throw new Error('session lookup failed');
|
|
620
|
+
}
|
|
606
621
|
// Only auto-select if there is a clear winner.
|
|
607
622
|
//
|
|
608
623
|
// When at least one non-throttled (focused/visible) session exists, require a >1 s recency
|
|
@@ -649,18 +664,21 @@ export class SessionManager {
|
|
|
649
664
|
* dropped. A tab whose checkout cannot be read stays, because it can be the agent's own.
|
|
650
665
|
*/
|
|
651
666
|
export function scopeToCallerCheckout(sessions, caller, checkoutOfTab) {
|
|
652
|
-
if (caller === undefined)
|
|
667
|
+
if (caller === undefined) {
|
|
653
668
|
return { sessions: [...sessions] };
|
|
669
|
+
}
|
|
654
670
|
const placed = sessions.map((session) => ({ session, checkout: checkoutOfTab(session) }));
|
|
655
671
|
const mine = placed.filter((p) => isSame(p.checkout?.root, caller.root)).map((p) => p.session);
|
|
656
|
-
if (mine.length > 0)
|
|
672
|
+
if (mine.length > 0) {
|
|
657
673
|
return { sessions: mine };
|
|
674
|
+
}
|
|
658
675
|
const siblings = placed.filter((p) => isSame(p.checkout?.commonDir, caller.commonDir));
|
|
659
676
|
if (siblings.length !== placed.length) {
|
|
660
677
|
return { sessions: placed.filter((p) => !siblings.includes(p)).map((p) => p.session) };
|
|
661
678
|
}
|
|
662
|
-
if (0 === siblings.length)
|
|
679
|
+
if (0 === siblings.length) {
|
|
663
680
|
return { sessions: [] };
|
|
681
|
+
}
|
|
664
682
|
const listed = siblings
|
|
665
683
|
.map((p) => `${describeCheckout(p.checkout)} (sessionId '${p.session.id}', ${p.session.url})`)
|
|
666
684
|
.join(', ');
|
|
@@ -675,8 +693,9 @@ export function scopeToCallerCheckout(sessions, caller, checkoutOfTab) {
|
|
|
675
693
|
const isSame = (a, b) => a !== undefined && samePath(a, b);
|
|
676
694
|
/** `/wt/a (branch feat/a)`, or the folder alone on a detached HEAD. */
|
|
677
695
|
export function describeCheckout(checkout) {
|
|
678
|
-
if (checkout === undefined)
|
|
696
|
+
if (checkout === undefined) {
|
|
679
697
|
return 'an unknown folder';
|
|
698
|
+
}
|
|
680
699
|
return checkout.branch === undefined
|
|
681
700
|
? checkout.root
|
|
682
701
|
: `${checkout.root} (branch ${checkout.branch})`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { noteOwnReload, notePageLoaded } from '../../features/harness/drive-runs.js';
|
|
2
2
|
import { asProjectId } from '@reticlehq/core';
|
|
3
3
|
import { narrationArgs } from './narration-args.js';
|
|
4
|
+
import { sdkBehindNote } from '../../command/version/version-skew.js';
|
|
4
5
|
import { refusedResult } from './page-commands/undeclared-command.js';
|
|
5
6
|
import { impactSnapshot, recordImpact } from '../../memory/impact/impact-recorder.js';
|
|
6
7
|
import { callingAgent } from '../../hooks/coding-agents.js';
|
|
@@ -580,9 +581,9 @@ export class Session {
|
|
|
580
581
|
this.recordActedRef(ref);
|
|
581
582
|
if (name === ReticleCommand.REFRESH)
|
|
582
583
|
noteOwnReload(this.id, this.#clock());
|
|
583
|
-
// Refused before it is sent
|
|
584
|
-
|
|
585
|
-
const refused = refusedResult(name, this.commands, `${COMMAND_ID_PREFIX}refused
|
|
584
|
+
// Refused before it is sent when the page did not declare it. See `undeclared-command.ts`.
|
|
585
|
+
const behind = sdkBehindNote(this.sdkVersion);
|
|
586
|
+
const refused = refusedResult(name, this.commands, `${COMMAND_ID_PREFIX}refused`, behind);
|
|
586
587
|
if (refused !== undefined)
|
|
587
588
|
return Promise.resolve(refused);
|
|
588
589
|
// The page already re-dialled under this same id. Nothing has been sent yet, so handing the
|
|
@@ -22,10 +22,28 @@ export declare function within(child: string | undefined, parent: string): boole
|
|
|
22
22
|
export declare function projectAppUrlFinder(io: Pick<FirstCallIo, 'project' | 'serving' | 'servingDirectory'>): () => Promise<string | undefined>;
|
|
23
23
|
/** One lease per project for the daemon's life: a failure is reported once, never retried. */
|
|
24
24
|
export declare function firstCallOpener(io: FirstCallIo): () => Promise<FirstCallOpened | undefined>;
|
|
25
|
+
/**
|
|
26
|
+
* The first-session offer, on the first tool call of each agent connection.
|
|
27
|
+
*
|
|
28
|
+
* The plugin's SessionStart hook says it when a session starts. A plugin installed in an open
|
|
29
|
+
* session and loaded with `/reload-plugins` fires no SessionStart, so the server says it too. Only
|
|
30
|
+
* the first call of a connection looks: an offer on a later call reads as noise.
|
|
31
|
+
*/
|
|
32
|
+
export interface FirstSessionOfferIo {
|
|
33
|
+
/** The unwired web app this call is about; undefined when wired, not a web app, or no project. */
|
|
34
|
+
project: () => string | undefined;
|
|
35
|
+
/** Whether an app is attached now. */
|
|
36
|
+
connected: () => boolean;
|
|
37
|
+
/** Called once for each offer that goes out, with its project. */
|
|
38
|
+
offered: (dir: string) => void;
|
|
39
|
+
}
|
|
40
|
+
/** The offer for this connection's first call, or undefined. Each attach id gets one look. */
|
|
41
|
+
export declare function firstSessionOfferGate(io: FirstSessionOfferIo): (attachId: string) => string | undefined;
|
|
25
42
|
/**
|
|
26
43
|
* The daemon's two answers to an app nobody wired, built together because both ask the same
|
|
27
44
|
* question first — which unwired web app is this caller in. `firstRun` names it when the agent
|
|
28
|
-
* lists sessions; `openUnwiredApp` opens it zero-install on a page-needing call
|
|
45
|
+
* lists sessions; `openUnwiredApp` opens it zero-install on a page-needing call;
|
|
46
|
+
* `firstSessionOffer` says the offer on the first call of each agent connection.
|
|
29
47
|
*/
|
|
30
48
|
export declare function agentWiring(sessions: {
|
|
31
49
|
list(): readonly unknown[];
|
|
@@ -33,4 +51,5 @@ export declare function agentWiring(sessions: {
|
|
|
33
51
|
firstRun: FirstRunWiring;
|
|
34
52
|
openUnwiredApp: () => Promise<FirstCallOpened | undefined>;
|
|
35
53
|
projectAppUrl: () => Promise<string | undefined>;
|
|
54
|
+
firstSessionOffer: (attachId: string) => string | undefined;
|
|
36
55
|
};
|
|
@@ -21,8 +21,11 @@ import { DEV_SERVER_PORTS } from '../../command/cli/ports/resolve/cli-port.js';
|
|
|
21
21
|
import { callingClientDirectory } from '../../hooks/client-directory.js';
|
|
22
22
|
import { detectDevCommandInProject, initCommandIn } from './dev-server/dev-command.js';
|
|
23
23
|
import { probeDevServerStates } from './dev-server/dev-server-probe.js';
|
|
24
|
+
import { NextStepOffered } from '@reticlehq/core';
|
|
25
|
+
import { readProjectId } from '../../command/cli/ports/resolve/cli-port.js';
|
|
26
|
+
import { noteNextStepOffered } from '../../telemetry/onboarding-firsts.js';
|
|
24
27
|
import { firstRunWiring } from './first-run-wiring.js';
|
|
25
|
-
import { wiringConsent } from './no-session-next-action.js';
|
|
28
|
+
import { firstSessionOffer, wiringConsent } from './no-session-next-action.js';
|
|
26
29
|
import { servingDirectoryOf } from './serving-directory.js';
|
|
27
30
|
const LOCALHOST = 'http://localhost';
|
|
28
31
|
/** The path with symlinks resolved when it exists (lsof reports `/private/tmp`, callers `/tmp`). */
|
|
@@ -87,10 +90,32 @@ export function firstCallOpener(io) {
|
|
|
87
90
|
return { url, notice: openedNotice(url, initCommandIn(dir)) };
|
|
88
91
|
};
|
|
89
92
|
}
|
|
93
|
+
/** The offer for this connection's first call, or undefined. Each attach id gets one look. */
|
|
94
|
+
export function firstSessionOfferGate(io) {
|
|
95
|
+
// ponytail: one short id per MCP connection for the daemon's life; bound it if a daemon ever
|
|
96
|
+
// serves connections by the million.
|
|
97
|
+
const looked = new Set();
|
|
98
|
+
return (attachId) => {
|
|
99
|
+
if (looked.has(attachId)) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
looked.add(attachId);
|
|
103
|
+
if (io.connected()) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const dir = io.project();
|
|
107
|
+
if (dir === undefined) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
io.offered(dir);
|
|
111
|
+
return firstSessionOffer(initCommandIn(dir));
|
|
112
|
+
};
|
|
113
|
+
}
|
|
90
114
|
/**
|
|
91
115
|
* The daemon's two answers to an app nobody wired, built together because both ask the same
|
|
92
116
|
* question first — which unwired web app is this caller in. `firstRun` names it when the agent
|
|
93
|
-
* lists sessions; `openUnwiredApp` opens it zero-install on a page-needing call
|
|
117
|
+
* lists sessions; `openUnwiredApp` opens it zero-install on a page-needing call;
|
|
118
|
+
* `firstSessionOffer` says the offer on the first call of each agent connection.
|
|
94
119
|
*/
|
|
95
120
|
export function agentWiring(sessions, lease) {
|
|
96
121
|
const firstRun = firstRunWiring();
|
|
@@ -107,5 +132,12 @@ export function agentWiring(sessions, lease) {
|
|
|
107
132
|
firstRun,
|
|
108
133
|
openUnwiredApp: firstCallOpener(io),
|
|
109
134
|
projectAppUrl: projectAppUrlFinder({ ...io, project: callerFolder }),
|
|
135
|
+
firstSessionOffer: firstSessionOfferGate({
|
|
136
|
+
...io,
|
|
137
|
+
// The offer leads with the lease, so it is counted as a lease offer.
|
|
138
|
+
offered: (dir) => {
|
|
139
|
+
noteNextStepOffered(NextStepOffered.LEASE, () => readProjectId(dir) !== undefined);
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
110
142
|
};
|
|
111
143
|
}
|