@agentproto/runtime 2.10.0 → 2.11.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/README.md +1 -1
- package/dist/catalog-models.d.ts +7 -1
- package/dist/catalog-models.mjs +1 -1
- package/dist/catalog-models.mjs.map +1 -1
- package/dist/index.d.ts +28 -6
- package/dist/index.mjs +717 -2898
- package/dist/index.mjs.map +1 -1
- package/dist/pr-provenance.d.ts +11 -1
- package/dist/pr-provenance.mjs +10 -1
- package/dist/pr-provenance.mjs.map +1 -1
- package/dist/resume-strategies.mjs.map +1 -1
- package/dist/session-story.d.ts +1 -1
- package/dist/session-story.mjs.map +1 -1
- package/package.json +11 -15
- package/dist/session-story-panel.d.ts +0 -26
- package/dist/session-story-panel.mjs +0 -970
- package/dist/session-story-panel.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { EventEmitter } from 'events';
|
|
|
11
11
|
import { listAuthProfiles, eligibleProfiles, getAuthProfile, createAuthProfile, AuthProfileValidationError, deleteAuthProfile, KeychainStore, removeAuthProfile, addAuthProfile, setAuthProfileEnabled, setAuthProfileModels, refreshAuthProfileModels, credentialIdentity } from '@agentproto/auth';
|
|
12
12
|
import { resolveRecipeMethod, resolveSourceSpec } from '@agentproto/secrets/provision/recipe';
|
|
13
13
|
import { getModelProvider, resolveContextWindow, formatTokens, resolvePricing, LLM_PRICING_CATALOG, resolvePricingExact, MODEL_ALIASES } from '@agentproto/model-catalog/llm';
|
|
14
|
-
import { resolveCustomRoute, registerCustomRoute,
|
|
14
|
+
import { resolveCustomRoute, registerCustomRoute, formatModelRef, resolveLlmModelRoute, tryParseModelRef, stripRouteSuffix, stripFixedNativeVendor } from '@agentproto/model-catalog/route-identity';
|
|
15
15
|
import { findAnthropicGatewayPreset, anthropicGatewayPresetList, getAnthropicGatewayPreset } from '@agentproto/provider-presets';
|
|
16
16
|
import * as providers_store_star from '@agentproto/providers-store';
|
|
17
17
|
import { makeAdapterLister, makeAdapterResolver, makeCredsStore, discoverAdapterPackages, makeSetupLedger, makeListTool, makeSetupTool } from '@agentproto/provider-kit';
|
|
@@ -20,11 +20,13 @@ import { inferLegacyModeKind, parseModelSwitchCommand, isModelSwitchAcknowledgem
|
|
|
20
20
|
import { loadSandboxConfig, resolveCommandSandbox, COMMAND_SANDBOX_MODE_ENV } from '@agentproto/command-sandbox';
|
|
21
21
|
import { CatalogProviderSchema, getModelsByProvider, getStaticModelProvider } from '@agentproto/model-catalog';
|
|
22
22
|
import { createBrainManager, parseKnowledgeConfig } from '@agentproto/workspace-brain';
|
|
23
|
+
import { makeSessionsPanelApp, makeAgentsOverviewApp, makeBureauSessionsApp, makeSessionStoryPanelApp, makeLiveSessionApp, sessionsPanelApp, agentsOverviewApp, bureauSessionsApp, sessionStoryApp, liveSessionApp } from '@agentproto/apps';
|
|
23
24
|
import matter2 from 'gray-matter';
|
|
24
25
|
import { createServer } from 'http';
|
|
25
26
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
26
27
|
import { WebSocketServer } from 'ws';
|
|
27
28
|
import { loadAppHandle } from '@agentproto/app-kit';
|
|
29
|
+
import { loadAgent } from '@agentproto/agent';
|
|
28
30
|
import { normalizeToolId } from '@agentproto/driver';
|
|
29
31
|
import { createIngestionClient } from '@agentproto/telemetry-langfuse';
|
|
30
32
|
import { resolveRedactor } from '@agentproto/redaction';
|
|
@@ -239,6 +241,14 @@ function appendFooterOnce(body, footer) {
|
|
|
239
241
|
function hasProvenanceFooter(body) {
|
|
240
242
|
return new RegExp(`<sub>[^\\n]*${MARKER}`).test(body);
|
|
241
243
|
}
|
|
244
|
+
function footerHasCost(body) {
|
|
245
|
+
const m = new RegExp(`<sub>[^\\n]*${MARKER}[^\\n]*</sub>`).exec(body);
|
|
246
|
+
return m !== null && /\$\d/.test(m[0]);
|
|
247
|
+
}
|
|
248
|
+
function replaceProvenanceFooter(body, footer) {
|
|
249
|
+
if (!hasProvenanceFooter(body)) return appendFooterOnce(body, footer);
|
|
250
|
+
return body.replace(FOOTER_BLOCK_RE, footer);
|
|
251
|
+
}
|
|
242
252
|
function parseGhPrCreate(command, args, stdout) {
|
|
243
253
|
if (basename(command) !== "gh") return null;
|
|
244
254
|
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
@@ -279,7 +289,7 @@ function pickExecutorSession(sessions, cwd) {
|
|
|
279
289
|
if (live.length > 0) return live[0];
|
|
280
290
|
return [...candidates].sort(byRecency)[0];
|
|
281
291
|
}
|
|
282
|
-
var MARKER, fmtTokens, buildFooter;
|
|
292
|
+
var MARKER, fmtTokens, buildFooter, FOOTER_BLOCK_RE;
|
|
283
293
|
var init_pr_provenance = __esm({
|
|
284
294
|
"src/pr-provenance.ts"() {
|
|
285
295
|
MARKER = "@agentproto-bot";
|
|
@@ -324,6 +334,7 @@ var init_pr_provenance = __esm({
|
|
|
324
334
|
---
|
|
325
335
|
<sub>${parts.join(" \xB7 ")}</sub>`;
|
|
326
336
|
};
|
|
337
|
+
FOOTER_BLOCK_RE = new RegExp(`(?:\\n+---)?\\n*<sub>[^\\n]*${MARKER}[^\\n]*</sub>`);
|
|
327
338
|
}
|
|
328
339
|
});
|
|
329
340
|
function sessionTranscriptDir(sessionId, baseDir) {
|
|
@@ -3831,60 +3842,6 @@ function resolveWorktreeIdentity(cwd) {
|
|
|
3831
3842
|
dir = parent;
|
|
3832
3843
|
}
|
|
3833
3844
|
}
|
|
3834
|
-
function normalizeModelForWire(model, opts) {
|
|
3835
|
-
if (opts.routeSelection === "derived-from-model") {
|
|
3836
|
-
return stripRouteSuffix(model);
|
|
3837
|
-
}
|
|
3838
|
-
const nativeVendor = opts.gateway ?? opts.fixedProvider;
|
|
3839
|
-
if (nativeVendor) {
|
|
3840
|
-
return stripFixedNativeVendor(model, nativeVendor);
|
|
3841
|
-
}
|
|
3842
|
-
return stripRouteSuffix(model);
|
|
3843
|
-
}
|
|
3844
|
-
|
|
3845
|
-
// src/launch-config.ts
|
|
3846
|
-
function buildRouteAwareLaunchConfig(input) {
|
|
3847
|
-
const prefix = input.prefix ?? "agent_start";
|
|
3848
|
-
const baseOptions = input.options ?? {};
|
|
3849
|
-
const hasExplicitBaseUrlOption = typeof baseOptions.base_url === "string" && baseOptions.base_url.length > 0;
|
|
3850
|
-
const gatewayResolvedBaseUrl = input.authSpec?.baseUrl;
|
|
3851
|
-
const resolvedBaseUrl = gatewayResolvedBaseUrl ?? input.route?.baseUrl;
|
|
3852
|
-
let routedOptions = baseOptions;
|
|
3853
|
-
if (!hasExplicitBaseUrlOption && typeof resolvedBaseUrl === "string" && resolvedBaseUrl.length > 0) {
|
|
3854
|
-
const declaresBaseUrl = input.declaredOptions?.some((o) => o.id === "base_url") ?? false;
|
|
3855
|
-
const declaredOptionsKnown = input.declaredOptions !== void 0;
|
|
3856
|
-
if (declaresBaseUrl || !declaredOptionsKnown) {
|
|
3857
|
-
routedOptions = { ...baseOptions, base_url: resolvedBaseUrl };
|
|
3858
|
-
} else if (input.routeSelection === "derived-from-model") {
|
|
3859
|
-
routedOptions = baseOptions;
|
|
3860
|
-
} else {
|
|
3861
|
-
throw new Error(
|
|
3862
|
-
`${prefix}: adapter "${input.adapter}" cannot be routed through gateway "${input.route?.gateway ?? "unknown"}" \u2014 it declares no \`base_url\` option and does not derive its route from the model id (routeSelection !== "derived-from-model"), so the resolved gateway endpoint has nowhere to go. Declare a \`base_url\` option on the adapter manifest, or mark it \`routeSelection: "derived-from-model"\` if it already resolves its own gateway from the model prefix.`
|
|
3863
|
-
);
|
|
3864
|
-
}
|
|
3865
|
-
}
|
|
3866
|
-
const effectiveOptions = normalizeSkillsOption(
|
|
3867
|
-
input.skills ?? [],
|
|
3868
|
-
routedOptions,
|
|
3869
|
-
input.declaredOptions
|
|
3870
|
-
);
|
|
3871
|
-
const wireModel = input.model ? normalizeModelForWire(input.model, {
|
|
3872
|
-
routeSelection: input.routeSelection,
|
|
3873
|
-
gateway: input.route?.gateway,
|
|
3874
|
-
fixedProvider: input.adapterProvider
|
|
3875
|
-
}) : void 0;
|
|
3876
|
-
const out = {};
|
|
3877
|
-
if (Object.keys(effectiveOptions).length > 0) {
|
|
3878
|
-
out.options = effectiveOptions;
|
|
3879
|
-
}
|
|
3880
|
-
if (wireModel) {
|
|
3881
|
-
out.wireModel = wireModel;
|
|
3882
|
-
}
|
|
3883
|
-
if (resolvedBaseUrl) {
|
|
3884
|
-
out.resolvedBaseUrl = resolvedBaseUrl;
|
|
3885
|
-
}
|
|
3886
|
-
return out;
|
|
3887
|
-
}
|
|
3888
3845
|
var WIDENING_ROUTES = ["openrouter", "requesty", "huggingface"];
|
|
3889
3846
|
var VENDOR_COMPATIBILITY_ROUTES = {
|
|
3890
3847
|
xai: ["xai", "xai-anthropic"]
|
|
@@ -4279,6 +4236,67 @@ function modelWalletIneligibleMessage(opts) {
|
|
|
4279
4236
|
return `${opts.prefix}: model "${opts.model}" is not serviceable on the resolved ${wallet} (adapter "${opts.adapter}") and would 404 upstream. This model bills route "${primary}"${also} \u2014 re-spawn on it: set route.gateway="${primary}" with an eligible "${primary}" api-key profile (access.profileRef). This guard only rejects; it never switches wallets for you.`;
|
|
4280
4237
|
}
|
|
4281
4238
|
|
|
4239
|
+
// src/model-wire.ts
|
|
4240
|
+
function normalizeModelForWire(model, opts) {
|
|
4241
|
+
if (opts.routeSelection === "derived-from-model") {
|
|
4242
|
+
const bare = stripRouteSuffix(model);
|
|
4243
|
+
if (opts.modelDerivedApiKey && opts.gateway && WIDENING_ROUTES.includes(opts.gateway) && !bare.startsWith(`${opts.gateway}/`)) {
|
|
4244
|
+
return `${opts.gateway}/${bare}`;
|
|
4245
|
+
}
|
|
4246
|
+
return bare;
|
|
4247
|
+
}
|
|
4248
|
+
const nativeVendor = opts.gateway ?? opts.fixedProvider;
|
|
4249
|
+
if (nativeVendor) {
|
|
4250
|
+
return stripFixedNativeVendor(model, nativeVendor);
|
|
4251
|
+
}
|
|
4252
|
+
return stripRouteSuffix(model);
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
// src/launch-config.ts
|
|
4256
|
+
function buildRouteAwareLaunchConfig(input) {
|
|
4257
|
+
const prefix = input.prefix ?? "agent_start";
|
|
4258
|
+
const baseOptions = input.options ?? {};
|
|
4259
|
+
const hasExplicitBaseUrlOption = typeof baseOptions.base_url === "string" && baseOptions.base_url.length > 0;
|
|
4260
|
+
const gatewayResolvedBaseUrl = input.authSpec?.baseUrl;
|
|
4261
|
+
const resolvedBaseUrl = gatewayResolvedBaseUrl ?? input.route?.baseUrl;
|
|
4262
|
+
let routedOptions = baseOptions;
|
|
4263
|
+
if (!hasExplicitBaseUrlOption && typeof resolvedBaseUrl === "string" && resolvedBaseUrl.length > 0) {
|
|
4264
|
+
const declaresBaseUrl = input.declaredOptions?.some((o) => o.id === "base_url") ?? false;
|
|
4265
|
+
const declaredOptionsKnown = input.declaredOptions !== void 0;
|
|
4266
|
+
if (declaresBaseUrl || !declaredOptionsKnown) {
|
|
4267
|
+
routedOptions = { ...baseOptions, base_url: resolvedBaseUrl };
|
|
4268
|
+
} else if (input.routeSelection === "derived-from-model") {
|
|
4269
|
+
routedOptions = baseOptions;
|
|
4270
|
+
} else {
|
|
4271
|
+
throw new Error(
|
|
4272
|
+
`${prefix}: adapter "${input.adapter}" cannot be routed through gateway "${input.route?.gateway ?? "unknown"}" \u2014 it declares no \`base_url\` option and does not derive its route from the model id (routeSelection !== "derived-from-model"), so the resolved gateway endpoint has nowhere to go. Declare a \`base_url\` option on the adapter manifest, or mark it \`routeSelection: "derived-from-model"\` if it already resolves its own gateway from the model prefix.`
|
|
4273
|
+
);
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
const effectiveOptions = normalizeSkillsOption(
|
|
4277
|
+
input.skills ?? [],
|
|
4278
|
+
routedOptions,
|
|
4279
|
+
input.declaredOptions
|
|
4280
|
+
);
|
|
4281
|
+
const wireModel = input.model ? normalizeModelForWire(input.model, {
|
|
4282
|
+
routeSelection: input.routeSelection,
|
|
4283
|
+
gateway: input.route?.gateway,
|
|
4284
|
+
fixedProvider: input.adapterProvider,
|
|
4285
|
+
modelDerivedApiKey: input.modelDerivedApiKey
|
|
4286
|
+
}) : void 0;
|
|
4287
|
+
const out = {};
|
|
4288
|
+
if (Object.keys(effectiveOptions).length > 0) {
|
|
4289
|
+
out.options = effectiveOptions;
|
|
4290
|
+
}
|
|
4291
|
+
if (wireModel) {
|
|
4292
|
+
out.wireModel = wireModel;
|
|
4293
|
+
}
|
|
4294
|
+
if (resolvedBaseUrl) {
|
|
4295
|
+
out.resolvedBaseUrl = resolvedBaseUrl;
|
|
4296
|
+
}
|
|
4297
|
+
return out;
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4282
4300
|
// src/context-continuity.ts
|
|
4283
4301
|
var CONTEXT_CONTINUITY_DEFAULTS = {
|
|
4284
4302
|
mode: "ask",
|
|
@@ -6248,6 +6266,7 @@ async function spawnAgentSession(deps2, input) {
|
|
|
6248
6266
|
declaredOptions: resolved?.declaredOptions,
|
|
6249
6267
|
routeSelection: resolved?.routeSelection,
|
|
6250
6268
|
adapterProvider: resolved?.authDescriptor?.provider,
|
|
6269
|
+
modelDerivedApiKey: resolved?.authDescriptor?.modelDerivedApiKey,
|
|
6251
6270
|
skills: spawnDefaults.skills,
|
|
6252
6271
|
prefix: "agent_start"
|
|
6253
6272
|
});
|
|
@@ -6313,7 +6332,13 @@ async function spawnAgentSession(deps2, input) {
|
|
|
6313
6332
|
const existing = claims.get(key);
|
|
6314
6333
|
if (existing) {
|
|
6315
6334
|
const result = await existing.result;
|
|
6316
|
-
|
|
6335
|
+
if (result.ok) {
|
|
6336
|
+
console.warn(
|
|
6337
|
+
`[agent_start] dedupe hit (${dedupeSource}): returning existing session ${result.descriptor.id}${input.label ? ` (label "${input.label}")` : ""} for a repeated spawn (adapter ${input.adapter}, cwd ${cwd})`
|
|
6338
|
+
);
|
|
6339
|
+
return { ...result, deduped: true, dedupeSource };
|
|
6340
|
+
}
|
|
6341
|
+
return result;
|
|
6317
6342
|
}
|
|
6318
6343
|
let resolveClaim;
|
|
6319
6344
|
claims.set(key, {
|
|
@@ -6352,6 +6377,7 @@ async function spawnAgentSession(deps2, input) {
|
|
|
6352
6377
|
...worktreeAutoProvisioned ? { worktreeAutoProvisioned: true } : {},
|
|
6353
6378
|
...resolved?.routeSelection !== void 0 ? { routeSelection: resolved.routeSelection } : {},
|
|
6354
6379
|
...resolved?.authDescriptor?.provider !== void 0 ? { adapterProvider: resolved.authDescriptor.provider } : {},
|
|
6380
|
+
...resolved?.authDescriptor?.modelDerivedApiKey !== void 0 ? { modelDerivedApiKey: resolved.authDescriptor.modelDerivedApiKey } : {},
|
|
6355
6381
|
...input.model ? { model: input.model } : defaultModel2 ? { model: defaultModel2 } : {},
|
|
6356
6382
|
...input.mode ? { mode: input.mode } : {},
|
|
6357
6383
|
...input.effort ? { effort: input.effort } : {},
|
|
@@ -6589,7 +6615,11 @@ ${asyncPrompt}`;
|
|
|
6589
6615
|
// the descriptor's `parentSessionId` so the child can discover
|
|
6590
6616
|
// who spawned it without a registry round-trip. Absent on a
|
|
6591
6617
|
// parentless root spawn.
|
|
6592
|
-
...parentSessionId ? { [PARENT_SESSION_ID_ENV]: parentSessionId } : {}
|
|
6618
|
+
...parentSessionId ? { [PARENT_SESSION_ID_ENV]: parentSessionId } : {},
|
|
6619
|
+
// App identity (APP_ID_ENV's doc, sessions.ts) — set only for an
|
|
6620
|
+
// `app_run` spawn, so a daemon-tool proxy the child builds can
|
|
6621
|
+
// auto-fill `appId` into an `app_*` call that omits one.
|
|
6622
|
+
...input.appId ? { [APP_ID_ENV]: input.appId } : {}
|
|
6593
6623
|
},
|
|
6594
6624
|
onActivity: () => {
|
|
6595
6625
|
if (liveSessionId) registry.pulseActivity(liveSessionId);
|
|
@@ -6629,6 +6659,7 @@ ${effectivePrompt}`;
|
|
|
6629
6659
|
harness: input.harness ?? input.adapter,
|
|
6630
6660
|
...resolved?.routeSelection !== void 0 ? { routeSelection: resolved.routeSelection } : {},
|
|
6631
6661
|
...resolved?.authDescriptor?.provider !== void 0 ? { adapterProvider: resolved.authDescriptor.provider } : {},
|
|
6662
|
+
...resolved?.authDescriptor?.modelDerivedApiKey !== void 0 ? { modelDerivedApiKey: resolved.authDescriptor.modelDerivedApiKey } : {},
|
|
6632
6663
|
...input.model ? { model: input.model } : defaultModel ? { model: defaultModel } : {},
|
|
6633
6664
|
...input.mode ? { mode: input.mode } : {},
|
|
6634
6665
|
...input.effort ? { effort: input.effort } : {},
|
|
@@ -6874,7 +6905,25 @@ async function bootSandboxAgentSession(opts) {
|
|
|
6874
6905
|
agentSession: createSandboxAgentSessionProxy({ host, remoteSessionId, lifecyclePolicy }),
|
|
6875
6906
|
commandPreview: `sandbox:${providerSlug} \u2192 ${opts.adapter}`,
|
|
6876
6907
|
sandboxId: host.sandboxId,
|
|
6877
|
-
sandboxTeardown: lifecyclePolicy.teardown
|
|
6908
|
+
sandboxTeardown: lifecyclePolicy.teardown,
|
|
6909
|
+
// The proxy flattens the box's stream to text (documented limitation),
|
|
6910
|
+
// so cost/tokens/model never ride the event stream out of the box. Read
|
|
6911
|
+
// them back from the box daemon's own `session_usage` at each turn-end —
|
|
6912
|
+
// the same `readUsage` hook hermes uses for its state.db — so the HOST
|
|
6913
|
+
// descriptor (and every footer / session_usage built from it) carries
|
|
6914
|
+
// the amount the sandboxed session actually spent.
|
|
6915
|
+
...typeof host.usage === "function" ? {
|
|
6916
|
+
readUsage: async () => {
|
|
6917
|
+
const snap = await host.usage(remoteSessionId);
|
|
6918
|
+
const usage = {
|
|
6919
|
+
...typeof snap.model === "string" && snap.model.length > 0 ? { model: snap.model } : {},
|
|
6920
|
+
...typeof snap.costUsd === "number" ? { costUsd: snap.costUsd } : {},
|
|
6921
|
+
...typeof snap.tokensIn === "number" ? { tokensIn: snap.tokensIn } : {},
|
|
6922
|
+
...typeof snap.tokensOut === "number" ? { tokensOut: snap.tokensOut } : {}
|
|
6923
|
+
};
|
|
6924
|
+
return Object.keys(usage).length > 0 ? usage : null;
|
|
6925
|
+
}
|
|
6926
|
+
} : {}
|
|
6878
6927
|
};
|
|
6879
6928
|
}
|
|
6880
6929
|
function sandboxAuthFromResolved(auth) {
|
|
@@ -8440,6 +8489,7 @@ function adapterConfigDirFor(sessionId) {
|
|
|
8440
8489
|
var SESSION_ID_ENV = "AGENTPROTO_SESSION_ID";
|
|
8441
8490
|
var WORKSPACE_SLUG_ENV = "AGENTPROTO_WORKSPACE_SLUG";
|
|
8442
8491
|
var PARENT_SESSION_ID_ENV = "AGENTPROTO_PARENT_SESSION_ID";
|
|
8492
|
+
var APP_ID_ENV = "AGENTPROTO_APP_ID";
|
|
8443
8493
|
var SessionNotAliveError = class extends Error {
|
|
8444
8494
|
sessionId;
|
|
8445
8495
|
status;
|
|
@@ -9877,6 +9927,9 @@ ${message}`;
|
|
|
9877
9927
|
try {
|
|
9878
9928
|
const usage2 = await rt.readUsage();
|
|
9879
9929
|
if (usage2) {
|
|
9930
|
+
if (typeof usage2.model === "string" && usage2.model.length > 0 && rt.desc.model === void 0) {
|
|
9931
|
+
rt.desc.model = usage2.model;
|
|
9932
|
+
}
|
|
9880
9933
|
if (usage2.costUsd !== void 0) {
|
|
9881
9934
|
rt.desc.costUsd = usage2.costUsd;
|
|
9882
9935
|
rt.adapterReportedCost = true;
|
|
@@ -10151,6 +10204,7 @@ ${message}`;
|
|
|
10151
10204
|
harness: input.harness ?? input.adapterSlug,
|
|
10152
10205
|
...input.routeSelection !== void 0 ? { routeSelection: input.routeSelection } : {},
|
|
10153
10206
|
...input.adapterProvider !== void 0 ? { adapterProvider: input.adapterProvider } : {},
|
|
10207
|
+
...input.modelDerivedApiKey !== void 0 ? { modelDerivedApiKey: input.modelDerivedApiKey } : {},
|
|
10154
10208
|
// ACP-level session id — sticks across daemon restart so
|
|
10155
10209
|
// `agentproto sessions restart <id>` can pass it as
|
|
10156
10210
|
// `resumeSessionId` and the adapter reattaches to the prior
|
|
@@ -10276,6 +10330,7 @@ ${message}`;
|
|
|
10276
10330
|
harness: input.harness ?? input.adapterSlug,
|
|
10277
10331
|
...input.routeSelection !== void 0 ? { routeSelection: input.routeSelection } : {},
|
|
10278
10332
|
...input.adapterProvider !== void 0 ? { adapterProvider: input.adapterProvider } : {},
|
|
10333
|
+
...input.modelDerivedApiKey !== void 0 ? { modelDerivedApiKey: input.modelDerivedApiKey } : {},
|
|
10279
10334
|
...input.label ? { label: input.label } : {},
|
|
10280
10335
|
...input.title ? { title: input.title } : {},
|
|
10281
10336
|
...input.label ? { renamedByUser: false } : {},
|
|
@@ -10812,7 +10867,8 @@ ${message}`;
|
|
|
10812
10867
|
}
|
|
10813
10868
|
let routeSelection = rt.desc.routeSelection;
|
|
10814
10869
|
let adapterProvider = rt.desc.adapterProvider;
|
|
10815
|
-
|
|
10870
|
+
let modelDerivedApiKey = rt.desc.modelDerivedApiKey;
|
|
10871
|
+
const needsAdapterMetadata = routeSelection === void 0 || modelDerivedApiKey === void 0 || routeSelection !== "derived-from-model" && adapterProvider === void 0;
|
|
10816
10872
|
const adapterSlug = rt.desc.adapterSlug ?? rt.adapterSlug;
|
|
10817
10873
|
if (needsAdapterMetadata && resolveAgentAdapter && adapterSlug) {
|
|
10818
10874
|
try {
|
|
@@ -10828,6 +10884,11 @@ ${message}`;
|
|
|
10828
10884
|
rt.desc.adapterProvider = adapterProvider;
|
|
10829
10885
|
hydrated = true;
|
|
10830
10886
|
}
|
|
10887
|
+
if (modelDerivedApiKey === void 0 && resolved?.authDescriptor?.modelDerivedApiKey !== void 0) {
|
|
10888
|
+
modelDerivedApiKey = resolved.authDescriptor.modelDerivedApiKey;
|
|
10889
|
+
rt.desc.modelDerivedApiKey = modelDerivedApiKey;
|
|
10890
|
+
hydrated = true;
|
|
10891
|
+
}
|
|
10831
10892
|
if (hydrated) schedulePersist();
|
|
10832
10893
|
} catch {
|
|
10833
10894
|
}
|
|
@@ -10835,7 +10896,8 @@ ${message}`;
|
|
|
10835
10896
|
const wireModel = normalizeModelForWire(modelId, {
|
|
10836
10897
|
routeSelection,
|
|
10837
10898
|
gateway: rt.desc.route?.gateway,
|
|
10838
|
-
fixedProvider: adapterProvider
|
|
10899
|
+
fixedProvider: adapterProvider,
|
|
10900
|
+
modelDerivedApiKey
|
|
10839
10901
|
});
|
|
10840
10902
|
const result = await rt.agentSession.setModel(wireModel);
|
|
10841
10903
|
if (result.applied) {
|
|
@@ -11569,6 +11631,15 @@ async function stampFooterOnPr(input) {
|
|
|
11569
11631
|
if (view.exitCode !== 0) return { stamped: false, reason: `gh pr view exit ${view.exitCode}` };
|
|
11570
11632
|
const body = view.stdout.replace(/\n+$/, "");
|
|
11571
11633
|
const alreadyStamped = hasProvenanceFooter(body);
|
|
11634
|
+
if (alreadyStamped && input.refresh === true) {
|
|
11635
|
+
if (footerHasCost(body) || !footerHasCost(footer)) {
|
|
11636
|
+
return { stamped: true, url: input.prUrl, number: input.prNumber, sessionId: input.session.id, alreadyStamped, refreshed: false };
|
|
11637
|
+
}
|
|
11638
|
+
const refreshedBody = replaceProvenanceFooter(body, footer);
|
|
11639
|
+
const edit = await run(["pr", "edit", input.prUrl, "--body", refreshedBody], input.cwd);
|
|
11640
|
+
if (edit.exitCode !== 0) return { stamped: false, reason: `gh pr edit exit ${edit.exitCode}` };
|
|
11641
|
+
return { stamped: true, url: input.prUrl, number: input.prNumber, sessionId: input.session.id, alreadyStamped, refreshed: true };
|
|
11642
|
+
}
|
|
11572
11643
|
if (!alreadyStamped) {
|
|
11573
11644
|
const newBody = appendFooterOnce(body, footer);
|
|
11574
11645
|
const edit = await run(["pr", "edit", input.prUrl, "--body", newBody], input.cwd);
|
|
@@ -13989,6 +14060,7 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
|
|
|
13989
14060
|
declaredOptions: resolved.declaredOptions,
|
|
13990
14061
|
routeSelection: resolved.routeSelection,
|
|
13991
14062
|
adapterProvider: resolved.authDescriptor?.provider,
|
|
14063
|
+
modelDerivedApiKey: resolved.authDescriptor?.modelDerivedApiKey,
|
|
13992
14064
|
prefix: "restart"
|
|
13993
14065
|
});
|
|
13994
14066
|
} catch (err) {
|
|
@@ -14031,6 +14103,7 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
|
|
|
14031
14103
|
harness: effHarness,
|
|
14032
14104
|
...resolved.routeSelection !== void 0 ? { routeSelection: resolved.routeSelection } : {},
|
|
14033
14105
|
...resolved.authDescriptor?.provider !== void 0 ? { adapterProvider: resolved.authDescriptor.provider } : {},
|
|
14106
|
+
...resolved.authDescriptor?.modelDerivedApiKey !== void 0 ? { modelDerivedApiKey: resolved.authDescriptor.modelDerivedApiKey } : {},
|
|
14034
14107
|
...prev.label ? { label: prev.label } : {},
|
|
14035
14108
|
...prev.mcpServers ? { mcpServers: prev.mcpServers } : {},
|
|
14036
14109
|
...effModel ? { model: effModel } : {},
|
|
@@ -18127,528 +18200,48 @@ function registerMcpApps(server, apps) {
|
|
|
18127
18200
|
);
|
|
18128
18201
|
}
|
|
18129
18202
|
}
|
|
18130
|
-
|
|
18131
|
-
|
|
18132
|
-
|
|
18133
|
-
|
|
18134
|
-
|
|
18135
|
-
|
|
18136
|
-
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
|
|
18141
|
-
// Replay the last context so a late subscriber isn't stuck blind.
|
|
18142
|
-
if (_hostContext){ try { cb(_hostContext); } catch(_) {} }
|
|
18143
|
-
}
|
|
18144
|
-
function _setHostContext(ctx){
|
|
18145
|
-
if (!ctx || typeof ctx !== 'object') return;
|
|
18146
|
-
// ui/notifications/host-context-changed carries only the changed keys \u2014
|
|
18147
|
-
// merge, matching the official ext-apps App behaviour.
|
|
18148
|
-
_hostContext = Object.assign({}, _hostContext || {}, ctx);
|
|
18149
|
-
for (var i = 0; i < _hostContextHandlers.length; i++){
|
|
18150
|
-
try { _hostContextHandlers[i](_hostContext); } catch(_) {}
|
|
18151
|
-
}
|
|
18152
|
-
}
|
|
18153
|
-
function rpcRequest(method, params){
|
|
18154
|
-
return new Promise(function(resolve, reject){
|
|
18155
|
-
var id = _nextId++;
|
|
18156
|
-
_pending[id] = {resolve: resolve, reject: reject};
|
|
18157
|
-
post({jsonrpc: '2.0', id: id, method: method, params: params || {}});
|
|
18158
|
-
});
|
|
18159
|
-
}
|
|
18160
|
-
function rpcNotify(method, params){ post({jsonrpc: '2.0', method: method, params: params || {}}); }
|
|
18161
|
-
function onHostNotification(cb){ _notifyHandlers.push(cb); }
|
|
18162
|
-
window.addEventListener('message', function(evt){
|
|
18163
|
-
var msg = evt.data;
|
|
18164
|
-
if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return;
|
|
18165
|
-
if (msg.id != null && msg.method == null){
|
|
18166
|
-
var p = _pending[msg.id];
|
|
18167
|
-
if (!p) return;
|
|
18168
|
-
delete _pending[msg.id];
|
|
18169
|
-
if (msg.error) p.reject(new Error(msg.error.message || ('rpc error ' + msg.error.code)));
|
|
18170
|
-
else p.resolve(msg.result);
|
|
18171
|
-
return;
|
|
18172
|
-
}
|
|
18173
|
-
if (msg.method){
|
|
18174
|
-
if (msg.method === 'ui/notifications/host-context-changed'){
|
|
18175
|
-
_setHostContext(msg.params || {});
|
|
18176
|
-
}
|
|
18177
|
-
for (var i = 0; i < _notifyHandlers.length; i++){
|
|
18178
|
-
try { _notifyHandlers[i](msg.method, msg.params || {}); } catch(_) {}
|
|
18179
|
-
}
|
|
18180
|
-
}
|
|
18181
|
-
});
|
|
18182
|
-
function initBridge(){
|
|
18183
|
-
return rpcRequest('ui/initialize', {
|
|
18184
|
-
appInfo: {name: ${JSON.stringify(appName)}, version: '0.1.0'},
|
|
18185
|
-
appCapabilities: {availableDisplayModes: ['inline', 'fullscreen', 'pip']},
|
|
18186
|
-
protocolVersion: '2026-01-26'
|
|
18187
|
-
}).then(function(result){
|
|
18188
|
-
// The initialize result carries the initial hostContext (displayMode +
|
|
18189
|
-
// availableDisplayModes) \u2014 capture it before notifying the host.
|
|
18190
|
-
if (result && result.hostContext) _setHostContext(result.hostContext);
|
|
18191
|
-
rpcNotify('ui/notifications/initialized', {});
|
|
18192
|
-
});
|
|
18193
|
-
}
|
|
18194
|
-
function requestDisplayMode(mode){
|
|
18195
|
-
return rpcRequest('ui/request-display-mode', {mode: mode});
|
|
18196
|
-
}
|
|
18197
|
-
function callTool(name, args){
|
|
18198
|
-
return rpcRequest('tools/call', {name: name, arguments: args || {}}).then(function(result){
|
|
18199
|
-
if (result.isError){
|
|
18200
|
-
var e = (result.content && result.content[0] && result.content[0].text) || 'tool error';
|
|
18201
|
-
throw new Error(e);
|
|
18202
|
-
}
|
|
18203
|
-
var text = (result.content && result.content[0] && result.content[0].text) || '{}';
|
|
18204
|
-
return JSON.parse(text);
|
|
18205
|
-
});
|
|
18206
|
-
}
|
|
18207
|
-
|
|
18208
|
-
// \u2500\u2500 Display-mode toggle buttons (NO auto-request) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
18209
|
-
// Injected by the shared bridge so every panel gets them without touching
|
|
18210
|
-
// its own markup. Mirrors guilde canvas.app.ts canvasShellHtml(): the
|
|
18211
|
-
// panel stays inline by default; the user expands on demand. Buttons only
|
|
18212
|
-
// appear for modes the host advertises in hostContext.availableDisplayModes.
|
|
18213
|
-
(function(){
|
|
18214
|
-
function mount(){
|
|
18215
|
-
var style = document.createElement('style');
|
|
18216
|
-
style.textContent = '#dm,#pin{display:none;position:fixed;top:8px;z-index:10;'
|
|
18217
|
-
+ 'border:1px solid #d0d0d0;background:#fff;color:#1a1a1a;'
|
|
18218
|
-
+ 'font:600 13px/1 system-ui,sans-serif;padding:7px 12px;border-radius:6px;'
|
|
18219
|
-
+ 'cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.18)}'
|
|
18220
|
-
+ '#dm{right:8px}#pin{right:118px}'
|
|
18221
|
-
+ '#dm:hover,#pin:hover{background:#f2f2f2;border-color:#b0b0b0}'
|
|
18222
|
-
+ '@media (prefers-color-scheme:dark){'
|
|
18223
|
-
+ '#dm,#pin{border-color:#555;background:#2a2a2a;color:#f0f0f0;box-shadow:0 1px 4px rgba(0,0,0,.5)}'
|
|
18224
|
-
+ '#dm:hover,#pin:hover{background:#333;border-color:#777}}';
|
|
18225
|
-
document.head.appendChild(style);
|
|
18226
|
-
|
|
18227
|
-
var btn = document.createElement('button');
|
|
18228
|
-
btn.id = 'dm'; btn.type = 'button'; btn.title = "Basculer l'affichage";
|
|
18229
|
-
var pin = document.createElement('button');
|
|
18230
|
-
pin.id = 'pin'; pin.type = 'button'; pin.title = '\xC9pingler sur le c\xF4t\xE9 (pip)';
|
|
18231
|
-
document.body.appendChild(pin);
|
|
18232
|
-
document.body.appendChild(btn);
|
|
18233
|
-
|
|
18234
|
-
function has(avail, m){ return !!avail && avail.indexOf(m) >= 0; }
|
|
18235
|
-
|
|
18236
|
-
// Re-sync button visibility + label from the current host context.
|
|
18237
|
-
function syncBtn(ctx){
|
|
18238
|
-
ctx = ctx || {};
|
|
18239
|
-
var avail = ctx.availableDisplayModes;
|
|
18240
|
-
// Diagnostic: what does THIS host actually advertise? (inline/fullscreen/pip)
|
|
18241
|
-
console.log('[mcp-app] displayMode=', ctx.displayMode,
|
|
18242
|
-
'availableDisplayModes=', avail);
|
|
18243
|
-
|
|
18244
|
-
// Fullscreen toggle button.
|
|
18245
|
-
if (has(avail, 'fullscreen')){
|
|
18246
|
-
btn.style.display = 'block';
|
|
18247
|
-
btn.textContent = (ctx.displayMode === 'fullscreen') ? '\u2921 R\xE9duire' : '\u2922 Agrandir';
|
|
18248
|
-
} else { btn.style.display = 'none'; }
|
|
18249
|
-
|
|
18250
|
-
// Dedicated pip ("pinned on side") button \u2014 only if the host advertises pip.
|
|
18251
|
-
if (has(avail, 'pip')){
|
|
18252
|
-
pin.style.display = 'block';
|
|
18253
|
-
pin.textContent = (ctx.displayMode === 'pip') ? '\u2921 D\xE9tacher' : '\u{1F4CC} \xC9pingler';
|
|
18254
|
-
} else { pin.style.display = 'none'; }
|
|
18255
|
-
}
|
|
18256
|
-
|
|
18257
|
-
onHostContext(syncBtn);
|
|
18258
|
-
|
|
18259
|
-
btn.addEventListener('click', function(){
|
|
18260
|
-
var ctx = getHostContext() || {};
|
|
18261
|
-
var inPanel = (ctx.displayMode === 'fullscreen' || ctx.displayMode === 'pip');
|
|
18262
|
-
requestDisplayMode(inPanel ? 'inline' : 'fullscreen').catch(function(){});
|
|
18263
|
-
});
|
|
18264
|
-
|
|
18265
|
-
pin.addEventListener('click', function(){
|
|
18266
|
-
var ctx = getHostContext() || {};
|
|
18267
|
-
requestDisplayMode(ctx.displayMode === 'pip' ? 'inline' : 'pip').catch(function(){});
|
|
18268
|
-
});
|
|
18269
|
-
}
|
|
18270
|
-
if (document.body) mount();
|
|
18271
|
-
else document.addEventListener('DOMContentLoaded', mount);
|
|
18272
|
-
})();`;
|
|
18273
|
-
}
|
|
18274
|
-
|
|
18275
|
-
// src/sessions-panel.ts
|
|
18276
|
-
var PANEL_HTML = `<!DOCTYPE html>
|
|
18277
|
-
<html lang="en">
|
|
18278
|
-
<head>
|
|
18279
|
-
<meta charset="UTF-8">
|
|
18280
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
18281
|
-
<title>agentproto sessions</title>
|
|
18282
|
-
<style>
|
|
18283
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
18284
|
-
:root{
|
|
18285
|
-
--bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
|
|
18286
|
-
--text:#e6edf3;--text2:#8b949e;
|
|
18287
|
-
--green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
|
|
18288
|
-
}
|
|
18289
|
-
html,body{height:100%;font-family:Menlo,Monaco,'Courier New',monospace;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}
|
|
18290
|
-
#app{display:flex;height:100%}
|
|
18291
|
-
#sidebar{width:220px;min-width:180px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
|
|
18292
|
-
#sidebar-hdr{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
|
18293
|
-
#sidebar-hdr h1{font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.06em}
|
|
18294
|
-
#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:15px;line-height:1;padding:2px 4px;border-radius:4px;font-family:inherit}
|
|
18295
|
-
#refresh-btn:hover{color:var(--text);background:var(--bg3)}
|
|
18296
|
-
#session-list{flex:1;overflow-y:auto;padding:4px 0}
|
|
18297
|
-
.si{padding:8px 12px;cursor:pointer;border-left:2px solid transparent}
|
|
18298
|
-
.si:hover{background:var(--bg3)}
|
|
18299
|
-
.si.active{background:var(--bg3);border-left-color:var(--blue)}
|
|
18300
|
-
.sn{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
18301
|
-
.sm{font-size:11px;color:var(--text2);margin-top:2px;display:flex;gap:6px;align-items:center}
|
|
18302
|
-
.badge{display:inline-block;padding:1px 5px;border-radius:10px;font-size:10px;font-weight:600}
|
|
18303
|
-
.br{background:rgba(63,185,80,.15);color:var(--green)}
|
|
18304
|
-
.bs{background:rgba(88,166,255,.15);color:var(--blue)}
|
|
18305
|
-
.be{background:rgba(139,148,158,.1);color:var(--text2)}
|
|
18306
|
-
.bk{background:rgba(248,81,73,.1);color:var(--red)}
|
|
18307
|
-
.berr{background:rgba(248,81,73,.2);color:var(--red)}
|
|
18308
|
-
#main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
|
|
18309
|
-
#toolbar{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2)}
|
|
18310
|
-
#session-title{font-size:12px;font-weight:500;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
18311
|
-
.abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}
|
|
18312
|
-
.abtn:hover{background:var(--border)}
|
|
18313
|
-
.abtn:disabled{opacity:.4;cursor:default}
|
|
18314
|
-
.abtn.danger{border-color:var(--red);color:var(--red)}
|
|
18315
|
-
.abtn.danger:hover{background:rgba(248,81,73,.1)}
|
|
18316
|
-
#output{flex:1;overflow-y:auto;padding:8px 12px;background:var(--bg);font-size:12px;line-height:1.6}
|
|
18317
|
-
.line{white-space:pre-wrap;word-break:break-all}
|
|
18318
|
-
#empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text2);gap:8px;text-align:center;padding:24px}
|
|
18319
|
-
#empty .ico{font-size:32px}
|
|
18320
|
-
#statusbar{padding:4px 12px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
|
|
18321
|
-
.errmsg{color:var(--red);padding:12px}
|
|
18322
|
-
/* ANSI SGR */
|
|
18323
|
-
.ab{font-weight:bold}.ad{opacity:.6}.ai{font-style:italic}.au{text-decoration:underline}
|
|
18324
|
-
.f0{color:#21262d}.f1{color:#f85149}.f2{color:#3fb950}.f3{color:#d29922}
|
|
18325
|
-
.f4{color:#58a6ff}.f5{color:#bc8cff}.f6{color:#39d353}.f7{color:#e6edf3}
|
|
18326
|
-
.f8{color:#8b949e}.f9{color:#ff7b72}.f10{color:#56d364}.f11{color:#e3b341}
|
|
18327
|
-
.f12{color:#79c0ff}.f13{color:#d2a8ff}.f14{color:#56d364}.f15{color:#f0f6fc}
|
|
18328
|
-
</style>
|
|
18329
|
-
</head>
|
|
18330
|
-
<body>
|
|
18331
|
-
<div id="app">
|
|
18332
|
-
<div id="sidebar">
|
|
18333
|
-
<div id="sidebar-hdr">
|
|
18334
|
-
<h1>Sessions</h1>
|
|
18335
|
-
<button id="refresh-btn" title="Refresh" onclick="doRefresh()">↻</button>
|
|
18336
|
-
</div>
|
|
18337
|
-
<div id="session-list"></div>
|
|
18338
|
-
</div>
|
|
18339
|
-
<div id="main">
|
|
18340
|
-
<div id="toolbar" style="display:none">
|
|
18341
|
-
<span id="session-title"></span>
|
|
18342
|
-
<button class="abtn danger" id="kill-btn" onclick="doKill()">Kill</button>
|
|
18343
|
-
</div>
|
|
18344
|
-
<div id="output">
|
|
18345
|
-
<div id="empty"><div class="ico">⚡</div><div>Select a session</div></div>
|
|
18346
|
-
</div>
|
|
18347
|
-
<div id="statusbar">Connecting to bridge…</div>
|
|
18348
|
-
</div>
|
|
18349
|
-
</div>
|
|
18350
|
-
<script>
|
|
18351
|
-
${panelBridgeScript("agentproto-sessions-panel")}
|
|
18352
|
-
|
|
18353
|
-
// ============================================================
|
|
18354
|
-
// ANSI-to-HTML renderer (SGR codes: colors 0-15, bold/dim/italic/underline)
|
|
18355
|
-
// ============================================================
|
|
18356
|
-
|
|
18357
|
-
function escHtml(s) {
|
|
18358
|
-
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
18359
|
-
}
|
|
18360
|
-
|
|
18361
|
-
function ansiToHtml(raw) {
|
|
18362
|
-
// Strip CR overwrite sequences (progress bars: CR without LF)
|
|
18363
|
-
var text = raw.replace(/[^\\n]*\\r([^\\n])/g, '$1').replace(/\\r/g, '');
|
|
18364
|
-
var bold = false, dim = false, italic = false, underline = false, fg = -1;
|
|
18365
|
-
var out = '';
|
|
18366
|
-
// Split on ESC [ ... m (SGR) sequences; keep delimiters
|
|
18367
|
-
var ESC = '\x1B';
|
|
18368
|
-
var parts = text.split(/([\x1B]\\[[0-9;]*m)/);
|
|
18369
|
-
for (var pi = 0; pi < parts.length; pi++) {
|
|
18370
|
-
var part = parts[pi];
|
|
18371
|
-
if (part.length === 0) continue;
|
|
18372
|
-
if (part.charCodeAt(0) === 0x1b && part[1] === '[') {
|
|
18373
|
-
// Parse SGR parameters
|
|
18374
|
-
var inner = part.slice(2, part.length - 1); // strip ESC[ and m
|
|
18375
|
-
var codes = inner === '' ? [0] : inner.split(';').map(Number);
|
|
18376
|
-
for (var ci = 0; ci < codes.length; ci++) {
|
|
18377
|
-
var c = codes[ci];
|
|
18378
|
-
if (c === 0) { bold = false; dim = false; italic = false; underline = false; fg = -1; }
|
|
18379
|
-
else if (c === 1) bold = true;
|
|
18380
|
-
else if (c === 2) dim = true;
|
|
18381
|
-
else if (c === 3) italic = true;
|
|
18382
|
-
else if (c === 4) underline = true;
|
|
18383
|
-
else if (c === 22) { bold = false; dim = false; }
|
|
18384
|
-
else if (c === 23) italic = false;
|
|
18385
|
-
else if (c === 24) underline = false;
|
|
18386
|
-
else if (c >= 30 && c <= 37) fg = c - 30;
|
|
18387
|
-
else if (c === 39) fg = -1;
|
|
18388
|
-
else if (c >= 90 && c <= 97) fg = c - 90 + 8;
|
|
18389
|
-
}
|
|
18390
|
-
} else {
|
|
18391
|
-
// Text segment \u2014 strip any remaining non-printable ESC sequences
|
|
18392
|
-
var safe = escHtml(part).replace(/\x1B\\[[^m]*[A-Za-z]/g, '');
|
|
18393
|
-
if (safe === '') continue;
|
|
18394
|
-
var cls = '';
|
|
18395
|
-
if (bold) cls += ' ab';
|
|
18396
|
-
if (dim) cls += ' ad';
|
|
18397
|
-
if (italic) cls += ' ai';
|
|
18398
|
-
if (underline) cls += ' au';
|
|
18399
|
-
if (fg >= 0 && fg <= 15) cls += ' f' + fg;
|
|
18400
|
-
cls = cls.trim();
|
|
18401
|
-
out += cls ? '<span class="' + cls + '">' + safe + '</span>' : safe;
|
|
18402
|
-
}
|
|
18403
|
-
}
|
|
18404
|
-
return out;
|
|
18405
|
-
}
|
|
18406
|
-
|
|
18407
|
-
// ============================================================
|
|
18408
|
-
// App state
|
|
18409
|
-
// ============================================================
|
|
18410
|
-
|
|
18411
|
-
var sessions = [];
|
|
18412
|
-
var activeId = null;
|
|
18413
|
-
var outputLines = [];
|
|
18414
|
-
var nextCursor = 0;
|
|
18415
|
-
var pollTimer = null;
|
|
18416
|
-
var pollActive = false;
|
|
18417
|
-
|
|
18418
|
-
function setStatus(msg) {
|
|
18419
|
-
document.getElementById('statusbar').textContent = msg;
|
|
18420
|
-
}
|
|
18421
|
-
|
|
18422
|
-
// ============================================================
|
|
18423
|
-
// Session list
|
|
18424
|
-
// ============================================================
|
|
18425
|
-
|
|
18426
|
-
function badgeClass(status) {
|
|
18427
|
-
if (status === 'running') return 'br';
|
|
18428
|
-
if (status === 'starting') return 'bs';
|
|
18429
|
-
if (status === 'killed') return 'bk';
|
|
18430
|
-
if (status === 'error') return 'berr';
|
|
18431
|
-
return 'be'; // exited
|
|
18432
|
-
}
|
|
18433
|
-
|
|
18434
|
-
// Derive a turn-aware display badge. A session's 'status' only tracks process
|
|
18435
|
-
// liveness, so an agent-cli process stays "running" while idle between turns.
|
|
18436
|
-
// Read 'busy' / 'awaitingInput' to show real activity instead:
|
|
18437
|
-
// working \u2014 a turn is in flight (busy)
|
|
18438
|
-
// waiting \u2014 turn ended on awaiting-input (needs a reply)
|
|
18439
|
-
// idle \u2014 process alive, no turn running (last turn done)
|
|
18440
|
-
function displayBadge(s) {
|
|
18441
|
-
if (s.status === 'running' && s.kind === 'agent-cli') {
|
|
18442
|
-
if (s.busy) return { label: 'working', cls: 'br' };
|
|
18443
|
-
if (s.awaitingInput) return { label: 'waiting', cls: 'bs' };
|
|
18444
|
-
return { label: 'idle', cls: 'be' };
|
|
18445
|
-
}
|
|
18446
|
-
return { label: s.status, cls: badgeClass(s.status) };
|
|
18447
|
-
}
|
|
18448
|
-
|
|
18449
|
-
function renderSidebar() {
|
|
18450
|
-
var el = document.getElementById('session-list');
|
|
18451
|
-
if (sessions.length === 0) {
|
|
18452
|
-
el.innerHTML = '<div style="padding:12px;color:var(--text2);font-size:11px">No sessions</div>';
|
|
18453
|
-
return;
|
|
18454
|
-
}
|
|
18455
|
-
var html = '';
|
|
18456
|
-
for (var i = 0; i < sessions.length; i++) {
|
|
18457
|
-
var s = sessions[i];
|
|
18458
|
-
var label = s.label || s.name || (s.command ? s.command.split('/').pop() : null) || s.id.slice(0, 8);
|
|
18459
|
-
var db = displayBadge(s);
|
|
18460
|
-
var active = s.id === activeId ? ' active' : '';
|
|
18461
|
-
// blockedOn (set while the turn waits on a spawned sub-agent or a
|
|
18462
|
-
// shell command) rides next to the status badge; waiting-on-user is
|
|
18463
|
-
// NOT here \u2014 that's awaitingInput, a different signal.
|
|
18464
|
-
var blocked = '';
|
|
18465
|
-
if (s.blockedOn === 'subagent') blocked = '<span class="badge bs">🧩 sous-agent</span>';
|
|
18466
|
-
else if (s.blockedOn === 'command') blocked = '<span class="badge bs">⏳ commande</span>';
|
|
18467
|
-
html += '<div class="si' + active + '" onclick="selectSession(\\'' + s.id + '\\')">'
|
|
18468
|
-
+ '<div class="sn">' + escHtml(label) + '</div>'
|
|
18469
|
-
+ '<div class="sm">'
|
|
18470
|
-
+ '<span class="badge ' + db.cls + '">' + db.label + '</span>'
|
|
18471
|
-
+ blocked
|
|
18472
|
-
+ '<span>' + escHtml(s.kind || '') + '</span>'
|
|
18473
|
-
+ '</div></div>';
|
|
18474
|
-
}
|
|
18475
|
-
el.innerHTML = html;
|
|
18476
|
-
}
|
|
18477
|
-
|
|
18478
|
-
function loadSessions() {
|
|
18479
|
-
// {kind:'all'} is session_list's live-able default \u2014 agent-CLI + terminal/
|
|
18480
|
-
// PTY only. kind:"command" rows (a shell-execution log, not a resumable
|
|
18481
|
-
// session) are excluded unless includeCommands is explicitly passed.
|
|
18482
|
-
return callTool('session_list', {kind: 'all'}).then(function(data) {
|
|
18483
|
-
sessions = data.sessions || [];
|
|
18484
|
-
renderSidebar();
|
|
18485
|
-
setStatus(sessions.length + ' session' + (sessions.length === 1 ? '' : 's') + ' \xB7 ' + new Date().toLocaleTimeString());
|
|
18486
|
-
}).catch(function(e) {
|
|
18487
|
-
setStatus('Error: ' + e.message);
|
|
18488
|
-
});
|
|
18489
|
-
}
|
|
18490
|
-
|
|
18491
|
-
// ============================================================
|
|
18492
|
-
// Output panel
|
|
18493
|
-
// ============================================================
|
|
18494
|
-
|
|
18495
|
-
function renderOutputFull() {
|
|
18496
|
-
var el = document.getElementById('output');
|
|
18497
|
-
if (outputLines.length === 0) {
|
|
18498
|
-
el.innerHTML = '<div id="empty"><div class="ico">⚡</div><div>No output yet</div></div>';
|
|
18499
|
-
return;
|
|
18500
|
-
}
|
|
18501
|
-
var html = '';
|
|
18502
|
-
for (var i = 0; i < outputLines.length; i++) {
|
|
18503
|
-
html += '<div class="line">' + ansiToHtml(outputLines[i]) + '</div>';
|
|
18504
|
-
}
|
|
18505
|
-
el.innerHTML = html;
|
|
18506
|
-
el.scrollTop = el.scrollHeight;
|
|
18507
|
-
}
|
|
18508
|
-
|
|
18509
|
-
function appendLines(lines) {
|
|
18510
|
-
var el = document.getElementById('output');
|
|
18511
|
-
// Remove empty-state placeholder if present
|
|
18512
|
-
var empty = el.querySelector('#empty');
|
|
18513
|
-
if (empty) empty.parentNode.removeChild(empty);
|
|
18514
|
-
var frag = document.createDocumentFragment();
|
|
18515
|
-
for (var i = 0; i < lines.length; i++) {
|
|
18516
|
-
var div = document.createElement('div');
|
|
18517
|
-
div.className = 'line';
|
|
18518
|
-
div.innerHTML = ansiToHtml(lines[i]);
|
|
18519
|
-
frag.appendChild(div);
|
|
18520
|
-
}
|
|
18521
|
-
el.appendChild(frag);
|
|
18522
|
-
el.scrollTop = el.scrollHeight;
|
|
18523
|
-
}
|
|
18524
|
-
|
|
18525
|
-
function selectSession(id) {
|
|
18526
|
-
activeId = id;
|
|
18527
|
-
outputLines = [];
|
|
18528
|
-
nextCursor = 0;
|
|
18529
|
-
renderSidebar();
|
|
18530
|
-
|
|
18531
|
-
var s = null;
|
|
18532
|
-
for (var i = 0; i < sessions.length; i++) {
|
|
18533
|
-
if (sessions[i].id === id) { s = sessions[i]; break; }
|
|
18534
|
-
}
|
|
18535
|
-
|
|
18536
|
-
var toolbar = document.getElementById('toolbar');
|
|
18537
|
-
toolbar.style.display = 'flex';
|
|
18538
|
-
document.getElementById('session-title').textContent = (s && (s.label || s.name)) || id.slice(0, 12);
|
|
18539
|
-
|
|
18540
|
-
var killBtn = document.getElementById('kill-btn');
|
|
18541
|
-
killBtn.disabled = !s || (s.status !== 'running' && s.status !== 'starting');
|
|
18542
|
-
|
|
18543
|
-
document.getElementById('output').innerHTML = '<div style="padding:12px;color:var(--text2)">Loading…</div>';
|
|
18544
|
-
|
|
18545
|
-
// Initial load
|
|
18546
|
-
callTool('agent_output', {sessionId: id, lastN: 200}).then(function(data) {
|
|
18547
|
-
outputLines = data.lines || [];
|
|
18548
|
-
nextCursor = data.nextCursor || 0;
|
|
18549
|
-
renderOutputFull();
|
|
18550
|
-
}).catch(function(e) {
|
|
18551
|
-
document.getElementById('output').innerHTML = '<div class="errmsg">Error: ' + escHtml(e.message) + '</div>';
|
|
18552
|
-
});
|
|
18553
|
-
}
|
|
18554
|
-
|
|
18555
|
-
// ============================================================
|
|
18556
|
-
// Kill
|
|
18557
|
-
// ============================================================
|
|
18558
|
-
|
|
18559
|
-
function doKill() {
|
|
18560
|
-
if (!activeId) return;
|
|
18561
|
-
var s = null;
|
|
18562
|
-
for (var i = 0; i < sessions.length; i++) {
|
|
18563
|
-
if (sessions[i].id === activeId) { s = sessions[i]; break; }
|
|
18564
|
-
}
|
|
18565
|
-
var toolName = (s && s.pty) ? 'terminal_kill' : 'agent_kill';
|
|
18566
|
-
callTool(toolName, {sessionId: activeId}).then(function() {
|
|
18567
|
-
return doRefresh();
|
|
18568
|
-
}).catch(function(e) {
|
|
18569
|
-
setStatus('Kill failed: ' + e.message);
|
|
18570
|
-
});
|
|
18203
|
+
function makeBuiltinPanelApps(ops) {
|
|
18204
|
+
return [
|
|
18205
|
+
makeSessionsPanelApp({ listSessions: ops.listSessions }),
|
|
18206
|
+
makeAgentsOverviewApp({ listSessions: ops.listSessions }),
|
|
18207
|
+
makeBureauSessionsApp({ listSessions: ops.listSessions }),
|
|
18208
|
+
makeSessionStoryPanelApp({ listSessions: ops.listSessions }),
|
|
18209
|
+
// Live-session widget — resource ui://live_session/view, also bound to
|
|
18210
|
+
// `agent_start` via _meta.ui.resourceUri (agent-tools.ts) so a launch
|
|
18211
|
+
// auto-renders it.
|
|
18212
|
+
makeLiveSessionApp({ httpBaseUrl: ops.httpBaseUrl })
|
|
18213
|
+
];
|
|
18571
18214
|
}
|
|
18572
|
-
|
|
18573
|
-
|
|
18574
|
-
|
|
18575
|
-
|
|
18576
|
-
|
|
18577
|
-
|
|
18578
|
-
|
|
18579
|
-
|
|
18580
|
-
|
|
18581
|
-
|
|
18582
|
-
|
|
18583
|
-
var capturedCursor = nextCursor;
|
|
18584
|
-
p = p.then(function() {
|
|
18585
|
-
return callTool('agent_output', {sessionId: capturedId, since: capturedCursor});
|
|
18586
|
-
}).then(function(data) {
|
|
18587
|
-
if (capturedId !== activeId) return; // user switched sessions
|
|
18588
|
-
var newLines = data.lines || [];
|
|
18589
|
-
if (newLines.length > 0) {
|
|
18590
|
-
outputLines = outputLines.concat(newLines);
|
|
18591
|
-
if (outputLines.length > 2000) outputLines = outputLines.slice(-2000);
|
|
18592
|
-
nextCursor = data.nextCursor || nextCursor;
|
|
18593
|
-
appendLines(newLines);
|
|
18594
|
-
}
|
|
18595
|
-
}).catch(function() {});
|
|
18596
|
-
}
|
|
18597
|
-
p.then(function() {
|
|
18598
|
-
pollActive = false;
|
|
18599
|
-
pollTimer = setTimeout(doPoll, 3000);
|
|
18600
|
-
}).catch(function() {
|
|
18601
|
-
pollActive = false;
|
|
18602
|
-
pollTimer = setTimeout(doPoll, 3000);
|
|
18215
|
+
var PANEL_APP_HANDLES = [
|
|
18216
|
+
sessionsPanelApp,
|
|
18217
|
+
agentsOverviewApp,
|
|
18218
|
+
bureauSessionsApp,
|
|
18219
|
+
sessionStoryApp,
|
|
18220
|
+
liveSessionApp
|
|
18221
|
+
];
|
|
18222
|
+
function builtinPanelCatalogEntries() {
|
|
18223
|
+
const apps = makeBuiltinPanelApps({
|
|
18224
|
+
listSessions: () => [],
|
|
18225
|
+
httpBaseUrl: "http://127.0.0.1:0"
|
|
18603
18226
|
});
|
|
18604
|
-
|
|
18605
|
-
|
|
18606
|
-
|
|
18607
|
-
|
|
18608
|
-
|
|
18609
|
-
|
|
18610
|
-
|
|
18611
|
-
|
|
18612
|
-
|
|
18613
|
-
|
|
18614
|
-
|
|
18227
|
+
return apps.map((app, i) => {
|
|
18228
|
+
const handle = PANEL_APP_HANDLES[i];
|
|
18229
|
+
const slug = (handle.id ?? app.id).replace(/^@[^/]+\//, "");
|
|
18230
|
+
return {
|
|
18231
|
+
appId: handle.id ?? `@agentproto/${slug}`,
|
|
18232
|
+
name: handle.name ?? app.title,
|
|
18233
|
+
description: handle.description ?? app.description ?? app.title,
|
|
18234
|
+
dir: `packages/apps/src/${slug}`,
|
|
18235
|
+
category: "builtin",
|
|
18236
|
+
installed: true,
|
|
18237
|
+
hasUi: true,
|
|
18238
|
+
hasArtifact: false,
|
|
18239
|
+
hasSkill: false,
|
|
18240
|
+
toolId: app.id,
|
|
18241
|
+
resourceUri: `ui://${app.id}/view`
|
|
18242
|
+
};
|
|
18615
18243
|
});
|
|
18616
18244
|
}
|
|
18617
|
-
|
|
18618
|
-
// ============================================================
|
|
18619
|
-
// Boot
|
|
18620
|
-
// ============================================================
|
|
18621
|
-
|
|
18622
|
-
initBridge().then(function() {
|
|
18623
|
-
return loadSessions();
|
|
18624
|
-
}).then(function() {
|
|
18625
|
-
pollTimer = setTimeout(doPoll, 3000);
|
|
18626
|
-
}).catch(function(e) {
|
|
18627
|
-
setStatus('Bridge error: ' + e.message);
|
|
18628
|
-
document.getElementById('output').innerHTML = '<div class="errmsg">Failed to connect to MCP bridge: ' + escHtml(e.message) + '</div>';
|
|
18629
|
-
});
|
|
18630
|
-
</script>
|
|
18631
|
-
</body>
|
|
18632
|
-
</html>`;
|
|
18633
|
-
|
|
18634
|
-
// src/sessions-panel-app.ts
|
|
18635
|
-
var sessionsPanelInputSchema = z.object({
|
|
18636
|
-
filter: z.enum(["running", "all"]).optional().describe(
|
|
18637
|
-
"Which sessions to return. `running` = only alive; `all` = running + recent (default)."
|
|
18638
|
-
)
|
|
18639
|
-
});
|
|
18640
|
-
function makeSessionsPanelApp(ops) {
|
|
18641
|
-
return {
|
|
18642
|
-
id: "agentproto_sessions",
|
|
18643
|
-
title: "Agent Sessions",
|
|
18644
|
-
description: "Open the agentproto sessions panel \u2014 an interactive UI that shows all running and recent agent-CLI and terminal/PTY sessions. Raw shell-command runs are a log, not a resumable session, and don't appear here \u2014 see `command_list`. The panel polls live data and lets you inspect output or kill sessions.",
|
|
18645
|
-
inputSchema: sessionsPanelInputSchema,
|
|
18646
|
-
execute: async (input) => ({
|
|
18647
|
-
sessions: ops.listSessions(input.filter)
|
|
18648
|
-
}),
|
|
18649
|
-
html: PANEL_HTML
|
|
18650
|
-
};
|
|
18651
|
-
}
|
|
18652
18245
|
function summarizeSession(desc, lines, nowMs) {
|
|
18653
18246
|
return {
|
|
18654
18247
|
sessionId: desc.id,
|
|
@@ -18687,1164 +18280,6 @@ function registerSummarizeSessionTool(server, ops) {
|
|
|
18687
18280
|
}
|
|
18688
18281
|
);
|
|
18689
18282
|
}
|
|
18690
|
-
var agentsOverviewInputSchema = z.object({
|
|
18691
|
-
filter: z.enum(["running", "all"]).optional().describe("`running` = only alive agent sessions; `all` = running + recent (default).")
|
|
18692
|
-
});
|
|
18693
|
-
function makeAgentsOverviewApp(ops) {
|
|
18694
|
-
return {
|
|
18695
|
-
id: "agentproto_agents_overview",
|
|
18696
|
-
title: "Agents \u2014 vue claire",
|
|
18697
|
-
description: "Open the agents overview \u2014 a plain-language card per agent session with one human sentence (what it's doing / last said) and a coarse state (\xE0 traiter / au travail / en attente / termin\xE9). Polls live and asks the server to summarise each session.",
|
|
18698
|
-
inputSchema: agentsOverviewInputSchema,
|
|
18699
|
-
execute: async (input) => ({ sessions: ops.listSessions(input.filter) }),
|
|
18700
|
-
html: AGENTS_OVERVIEW_HTML
|
|
18701
|
-
};
|
|
18702
|
-
}
|
|
18703
|
-
var AGENTS_OVERVIEW_HTML = `<!DOCTYPE html>
|
|
18704
|
-
<html lang="fr">
|
|
18705
|
-
<head>
|
|
18706
|
-
<meta charset="UTF-8">
|
|
18707
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
18708
|
-
<title>agents \u2014 vue claire</title>
|
|
18709
|
-
<style>
|
|
18710
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
18711
|
-
:root{
|
|
18712
|
-
--bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
|
|
18713
|
-
--text:#e6edf3;--text2:#8b949e;
|
|
18714
|
-
--green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
|
|
18715
|
-
}
|
|
18716
|
-
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}
|
|
18717
|
-
#app{display:flex;flex-direction:column;height:100%}
|
|
18718
|
-
#hdr{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--bg2);flex-shrink:0}
|
|
18719
|
-
#hdr h1{font-size:13px;font-weight:600;letter-spacing:.02em}
|
|
18720
|
-
#hdr .sub{font-size:11px;color:var(--text2);margin-top:2px}
|
|
18721
|
-
#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:16px;line-height:1;padding:4px 6px;border-radius:6px}
|
|
18722
|
-
#refresh-btn:hover{color:var(--text);background:var(--bg3)}
|
|
18723
|
-
#grid{flex:1;overflow-y:auto;padding:14px 16px;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;align-content:start}
|
|
18724
|
-
.card{background:var(--bg2);border:1px solid var(--border);border-left:3px solid var(--border);border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;min-height:96px}
|
|
18725
|
-
.card.s-todo{border-left-color:var(--purple)}
|
|
18726
|
-
.card.s-work{border-left-color:var(--green)}
|
|
18727
|
-
.card.s-wait{border-left-color:var(--yellow)}
|
|
18728
|
-
.card.s-done{border-left-color:var(--text2)}
|
|
18729
|
-
.card-top{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
18730
|
-
.title{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
|
|
18731
|
-
.state{font-size:10px;font-weight:700;padding:2px 8px;border-radius:11px;white-space:nowrap;letter-spacing:.02em}
|
|
18732
|
-
.state.s-todo{background:rgba(188,140,255,.16);color:var(--purple)}
|
|
18733
|
-
.state.s-work{background:rgba(63,185,80,.16);color:var(--green)}
|
|
18734
|
-
.state.s-wait{background:rgba(210,153,34,.16);color:var(--yellow)}
|
|
18735
|
-
.state.s-done{background:rgba(139,148,158,.14);color:var(--text2)}
|
|
18736
|
-
.summary{font-size:12.5px;line-height:1.5;color:var(--text);word-break:break-word}
|
|
18737
|
-
.summary.muted{color:var(--text2);font-style:italic}
|
|
18738
|
-
.meta{font-size:10.5px;color:var(--text2);display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:auto}
|
|
18739
|
-
.dot{width:5px;height:5px;border-radius:50%;background:var(--text2);display:inline-block}
|
|
18740
|
-
#empty{grid-column:1/-1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;padding:48px;text-align:center}
|
|
18741
|
-
#empty .ico{font-size:30px}
|
|
18742
|
-
#statusbar{padding:5px 16px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
|
|
18743
|
-
</style>
|
|
18744
|
-
</head>
|
|
18745
|
-
<body>
|
|
18746
|
-
<div id="app">
|
|
18747
|
-
<div id="hdr">
|
|
18748
|
-
<div>
|
|
18749
|
-
<h1>Agents \u2014 vue claire</h1>
|
|
18750
|
-
<div class="sub">une carte par agent \xB7 r\xE9sum\xE9 + \xE9tat</div>
|
|
18751
|
-
</div>
|
|
18752
|
-
<button id="refresh-btn" title="Rafra\xEEchir" onclick="doRefresh()">↻</button>
|
|
18753
|
-
</div>
|
|
18754
|
-
<div id="grid"><div id="empty"><div class="ico">⚡</div><div>Connexion au bridge…</div></div></div>
|
|
18755
|
-
<div id="statusbar">Connexion…</div>
|
|
18756
|
-
</div>
|
|
18757
|
-
<script>
|
|
18758
|
-
${panelBridgeScript("agentproto-agents-overview")}
|
|
18759
|
-
|
|
18760
|
-
// \u2500\u2500 State \u2500\u2500
|
|
18761
|
-
var REFRESH_MS = 12000;
|
|
18762
|
-
var pollTimer = null, polling = false;
|
|
18763
|
-
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
18764
|
-
function setStatus(m){ document.getElementById('statusbar').textContent = m; }
|
|
18765
|
-
|
|
18766
|
-
var STATE_CLASS = {
|
|
18767
|
-
'\xE0 traiter':'s-todo', 'au travail':'s-work', 'en attente':'s-wait', 'termin\xE9':'s-done'
|
|
18768
|
-
};
|
|
18769
|
-
function stateClass(st){ return STATE_CLASS[st] || 's-wait'; }
|
|
18770
|
-
|
|
18771
|
-
function fmtAgo(iso){
|
|
18772
|
-
if (!iso) return '';
|
|
18773
|
-
var t = Date.parse(iso);
|
|
18774
|
-
if (isNaN(t)) return '';
|
|
18775
|
-
var s = Math.max(0, Math.round((Date.now() - t) / 1000));
|
|
18776
|
-
if (s < 60) return 'il y a ' + s + 's';
|
|
18777
|
-
var m = Math.round(s/60);
|
|
18778
|
-
if (m < 60) return 'il y a ' + m + 'min';
|
|
18779
|
-
var h = Math.round(m/60);
|
|
18780
|
-
return 'il y a ' + h + 'h';
|
|
18781
|
-
}
|
|
18782
|
-
|
|
18783
|
-
function titleOf(s){
|
|
18784
|
-
return s.label || s.name || (s.command ? s.command.split(/\\s+/)[0].split('/').pop() : null) || s.id.slice(0,8);
|
|
18785
|
-
}
|
|
18786
|
-
|
|
18787
|
-
function render(sessions, summaries){
|
|
18788
|
-
var grid = document.getElementById('grid');
|
|
18789
|
-
if (!sessions.length){
|
|
18790
|
-
grid.innerHTML = '<div id="empty"><div class="ico">😴</div><div>Aucune session d\\'agent</div></div>';
|
|
18791
|
-
return;
|
|
18792
|
-
}
|
|
18793
|
-
var html = '';
|
|
18794
|
-
for (var i=0;i<sessions.length;i++){
|
|
18795
|
-
var s = sessions[i];
|
|
18796
|
-
var sum = summaries[s.id] || {};
|
|
18797
|
-
var st = sum.state || 'en attente';
|
|
18798
|
-
var cls = stateClass(st);
|
|
18799
|
-
var summary = sum.summary || 'R\xE9sum\xE9 indisponible.';
|
|
18800
|
-
var muted = !sum.summary ? ' muted' : '';
|
|
18801
|
-
html += '<div class="card ' + cls + '">'
|
|
18802
|
-
+ '<div class="card-top">'
|
|
18803
|
-
+ '<span class="title">' + esc(titleOf(s)) + '</span>'
|
|
18804
|
-
+ '<span class="state ' + cls + '">' + esc(st) + '</span>'
|
|
18805
|
-
+ '</div>'
|
|
18806
|
-
+ '<div class="summary' + muted + '">' + esc(summary) + '</div>'
|
|
18807
|
-
+ '<div class="meta">'
|
|
18808
|
-
+ '<span>' + esc(s.kind || '') + '</span><span class="dot"></span>'
|
|
18809
|
-
+ '<span>' + esc(s.status || '') + '</span>'
|
|
18810
|
-
+ (s.lastOutputAt ? '<span class="dot"></span><span>' + esc(fmtAgo(s.lastOutputAt)) + '</span>' : '')
|
|
18811
|
-
+ '</div>'
|
|
18812
|
-
+ '</div>';
|
|
18813
|
-
}
|
|
18814
|
-
grid.innerHTML = html;
|
|
18815
|
-
}
|
|
18816
|
-
|
|
18817
|
-
function loadAndRender(){
|
|
18818
|
-
return callTool('session_list', {kind:'all'}).then(function(data){
|
|
18819
|
-
var all = data.sessions || [];
|
|
18820
|
-
var agents = all.filter(function(s){ return s.kind === 'agent-cli'; });
|
|
18821
|
-
// Render shells immediately, then fill summaries as they arrive.
|
|
18822
|
-
var summaries = {};
|
|
18823
|
-
render(agents, summaries);
|
|
18824
|
-
setStatus(agents.length + ' agent' + (agents.length===1?'':'s') + ' \xB7 ' + new Date().toLocaleTimeString('fr-FR'));
|
|
18825
|
-
return Promise.all(agents.map(function(s){
|
|
18826
|
-
return callTool('summarize_session', {sessionId: s.id}).then(function(sum){
|
|
18827
|
-
summaries[s.id] = sum;
|
|
18828
|
-
}).catch(function(){ /* leave shell */ });
|
|
18829
|
-
})).then(function(){ render(agents, summaries); });
|
|
18830
|
-
}).catch(function(e){ setStatus('Erreur : ' + e.message); });
|
|
18831
|
-
}
|
|
18832
|
-
|
|
18833
|
-
function doPoll(){
|
|
18834
|
-
if (polling) return;
|
|
18835
|
-
polling = true;
|
|
18836
|
-
loadAndRender().then(function(){
|
|
18837
|
-
polling = false;
|
|
18838
|
-
pollTimer = setTimeout(doPoll, REFRESH_MS);
|
|
18839
|
-
}).catch(function(){
|
|
18840
|
-
polling = false;
|
|
18841
|
-
pollTimer = setTimeout(doPoll, REFRESH_MS);
|
|
18842
|
-
});
|
|
18843
|
-
}
|
|
18844
|
-
function doRefresh(){ if (pollTimer) clearTimeout(pollTimer); return loadAndRender().then(function(){ pollTimer = setTimeout(doPoll, REFRESH_MS); }); }
|
|
18845
|
-
|
|
18846
|
-
initBridge().then(loadAndRender).then(function(){
|
|
18847
|
-
pollTimer = setTimeout(doPoll, REFRESH_MS);
|
|
18848
|
-
}).catch(function(e){
|
|
18849
|
-
setStatus('Bridge : ' + e.message);
|
|
18850
|
-
document.getElementById('grid').innerHTML = '<div id="empty"><div class="ico">⚠</div><div>\xC9chec connexion bridge : ' + esc(e.message) + '</div></div>';
|
|
18851
|
-
});
|
|
18852
|
-
</script>
|
|
18853
|
-
</body>
|
|
18854
|
-
</html>`;
|
|
18855
|
-
var bureauSessionsInputSchema = z.object({
|
|
18856
|
-
filter: z.enum(["running", "all"]).optional().describe("`running` = only alive browser sessions; `all` = running + recent (default).")
|
|
18857
|
-
});
|
|
18858
|
-
function makeBureauSessionsApp(ops) {
|
|
18859
|
-
return {
|
|
18860
|
-
id: "agentproto_bureau_sessions",
|
|
18861
|
-
title: "Bureau \u2014 sessions navigateur",
|
|
18862
|
-
description: "Open the browser-sessions panel \u2014 one row per browser service (adapter, base URL, port, status, uptime). Polls live every ~5 s.",
|
|
18863
|
-
inputSchema: bureauSessionsInputSchema,
|
|
18864
|
-
execute: async (input) => ({
|
|
18865
|
-
sessions: ops.listSessions(input.filter).filter((s) => s.kind === "browser")
|
|
18866
|
-
}),
|
|
18867
|
-
html: BUREAU_SESSIONS_HTML
|
|
18868
|
-
};
|
|
18869
|
-
}
|
|
18870
|
-
var BUREAU_SESSIONS_HTML = `<!DOCTYPE html>
|
|
18871
|
-
<html lang="fr">
|
|
18872
|
-
<head>
|
|
18873
|
-
<meta charset="UTF-8">
|
|
18874
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
18875
|
-
<title>bureau \u2014 sessions navigateur</title>
|
|
18876
|
-
<style>
|
|
18877
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
18878
|
-
:root{
|
|
18879
|
-
--bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
|
|
18880
|
-
--text:#e6edf3;--text2:#8b949e;
|
|
18881
|
-
--green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
|
|
18882
|
-
}
|
|
18883
|
-
*{box-sizing:border-box}
|
|
18884
|
-
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}
|
|
18885
|
-
#app{display:flex;flex-direction:column;height:100%}
|
|
18886
|
-
#hdr{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--bg2);flex-shrink:0}
|
|
18887
|
-
#hdr h1{font-size:13px;font-weight:600}
|
|
18888
|
-
#hdr .sub{font-size:11px;color:var(--text2);margin-top:2px}
|
|
18889
|
-
#refresh-btn{background:none;border:none;cursor:pointer;color:var(--text2);font-size:16px;line-height:1;padding:4px 6px;border-radius:6px}
|
|
18890
|
-
#refresh-btn:hover{color:var(--text);background:var(--bg3)}
|
|
18891
|
-
#list{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px}
|
|
18892
|
-
.row{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:11px 14px;display:flex;align-items:center;gap:14px}
|
|
18893
|
-
.icon{font-size:20px;flex-shrink:0}
|
|
18894
|
-
.body{flex:1;min-width:0}
|
|
18895
|
-
.r1{display:flex;align-items:center;gap:8px}
|
|
18896
|
-
.adapter{font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
18897
|
-
.url{font-family:Menlo,Monaco,'Courier New',monospace;font-size:11.5px;color:var(--blue);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
18898
|
-
.meta{font-size:10.5px;color:var(--text2);margin-top:3px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
18899
|
-
.dot{width:5px;height:5px;border-radius:50%;background:var(--text2);display:inline-block}
|
|
18900
|
-
.badge{font-size:10px;font-weight:700;padding:2px 8px;border-radius:11px;white-space:nowrap}
|
|
18901
|
-
.br{background:rgba(63,185,80,.16);color:var(--green)}
|
|
18902
|
-
.bs{background:rgba(88,166,255,.16);color:var(--blue)}
|
|
18903
|
-
.bk{background:rgba(248,81,73,.14);color:var(--red)}
|
|
18904
|
-
.be{background:rgba(139,148,158,.14);color:var(--text2)}
|
|
18905
|
-
.berr{background:rgba(248,81,73,.22);color:var(--red)}
|
|
18906
|
-
.port{font-family:Menlo,Monaco,'Courier New',monospace}
|
|
18907
|
-
#empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text2);gap:8px;padding:48px;text-align:center}
|
|
18908
|
-
#empty .ico{font-size:30px}
|
|
18909
|
-
#statusbar{padding:5px 16px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
|
|
18910
|
-
</style>
|
|
18911
|
-
</head>
|
|
18912
|
-
<body>
|
|
18913
|
-
<div id="app">
|
|
18914
|
-
<div id="hdr">
|
|
18915
|
-
<div>
|
|
18916
|
-
<h1>Bureau \u2014 sessions navigateur</h1>
|
|
18917
|
-
<div class="sub">services navigateur actifs \xB7 URL / port / statut</div>
|
|
18918
|
-
</div>
|
|
18919
|
-
<button id="refresh-btn" title="Rafra\xEEchir" onclick="doRefresh()">↻</button>
|
|
18920
|
-
</div>
|
|
18921
|
-
<div id="list"><div id="empty"><div class="ico">🌐</div><div>Connexion au bridge…</div></div></div>
|
|
18922
|
-
<div id="statusbar">Connexion…</div>
|
|
18923
|
-
</div>
|
|
18924
|
-
<script>
|
|
18925
|
-
${panelBridgeScript("agentproto-bureau-sessions")}
|
|
18926
|
-
|
|
18927
|
-
// \u2500\u2500 State \u2500\u2500
|
|
18928
|
-
var REFRESH_MS = 5000;
|
|
18929
|
-
var pollTimer = null, polling = false;
|
|
18930
|
-
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
18931
|
-
function setStatus(m){ document.getElementById('statusbar').textContent = m; }
|
|
18932
|
-
|
|
18933
|
-
function badgeClass(st){
|
|
18934
|
-
if (st === 'running') return 'br';
|
|
18935
|
-
if (st === 'starting') return 'bs';
|
|
18936
|
-
if (st === 'killed') return 'bk';
|
|
18937
|
-
if (st === 'error') return 'berr';
|
|
18938
|
-
return 'be';
|
|
18939
|
-
}
|
|
18940
|
-
function fmtUptime(iso, end){
|
|
18941
|
-
if (!iso) return '';
|
|
18942
|
-
var start = Date.parse(iso);
|
|
18943
|
-
if (isNaN(start)) return '';
|
|
18944
|
-
var ref = end ? Date.parse(end) : Date.now();
|
|
18945
|
-
var s = Math.max(0, Math.round((ref - start) / 1000));
|
|
18946
|
-
if (s < 60) return s + 's';
|
|
18947
|
-
var m = Math.floor(s/60), rs = s%60;
|
|
18948
|
-
if (m < 60) return m + 'm ' + rs + 's';
|
|
18949
|
-
var h = Math.floor(m/60), rm = m%60;
|
|
18950
|
-
return h + 'h ' + rm + 'm';
|
|
18951
|
-
}
|
|
18952
|
-
|
|
18953
|
-
function render(rows){
|
|
18954
|
-
var list = document.getElementById('list');
|
|
18955
|
-
if (!rows.length){
|
|
18956
|
-
list.innerHTML = '<div id="empty"><div class="ico">🌐</div><div>Aucune session navigateur</div></div>';
|
|
18957
|
-
return;
|
|
18958
|
-
}
|
|
18959
|
-
var html = '';
|
|
18960
|
-
for (var i=0;i<rows.length;i++){
|
|
18961
|
-
var s = rows[i];
|
|
18962
|
-
var bc = badgeClass(s.status);
|
|
18963
|
-
var url = s.browserBaseUrl || (s.browserPort ? ('http://127.0.0.1:' + s.browserPort) : '');
|
|
18964
|
-
var adapter = s.browserAdapterId || s.label || s.name || 'navigateur';
|
|
18965
|
-
var up = fmtUptime(s.startedAt, s.endedAt);
|
|
18966
|
-
html += '<div class="row">'
|
|
18967
|
-
+ '<div class="icon">🌐</div>'
|
|
18968
|
-
+ '<div class="body">'
|
|
18969
|
-
+ '<div class="r1"><span class="adapter">' + esc(adapter) + '</span>'
|
|
18970
|
-
+ '<span class="badge ' + bc + '">' + esc(s.status || '') + '</span></div>'
|
|
18971
|
-
+ (url ? '<div class="url">' + esc(url) + '</div>' : '')
|
|
18972
|
-
+ '<div class="meta">'
|
|
18973
|
-
+ (s.browserPort ? '<span>port <span class="port">' + esc(s.browserPort) + '</span></span><span class="dot"></span>' : '')
|
|
18974
|
-
+ '<span>' + (up ? 'uptime ' + esc(up) : '') + '</span>'
|
|
18975
|
-
+ '</div>'
|
|
18976
|
-
+ '</div>'
|
|
18977
|
-
+ '</div>';
|
|
18978
|
-
}
|
|
18979
|
-
list.innerHTML = html;
|
|
18980
|
-
}
|
|
18981
|
-
|
|
18982
|
-
function loadAndRender(){
|
|
18983
|
-
return callTool('session_list', {kind:'all'}).then(function(data){
|
|
18984
|
-
var all = data.sessions || [];
|
|
18985
|
-
var browsers = all.filter(function(s){ return s.kind === 'browser'; });
|
|
18986
|
-
render(browsers);
|
|
18987
|
-
setStatus(browsers.length + ' session' + (browsers.length===1?'':'s') + ' \xB7 ' + new Date().toLocaleTimeString('fr-FR'));
|
|
18988
|
-
}).catch(function(e){ setStatus('Erreur : ' + e.message); });
|
|
18989
|
-
}
|
|
18990
|
-
|
|
18991
|
-
function doPoll(){
|
|
18992
|
-
if (polling) return;
|
|
18993
|
-
polling = true;
|
|
18994
|
-
loadAndRender().then(function(){
|
|
18995
|
-
polling = false;
|
|
18996
|
-
pollTimer = setTimeout(doPoll, REFRESH_MS);
|
|
18997
|
-
}).catch(function(){
|
|
18998
|
-
polling = false;
|
|
18999
|
-
pollTimer = setTimeout(doPoll, REFRESH_MS);
|
|
19000
|
-
});
|
|
19001
|
-
}
|
|
19002
|
-
function doRefresh(){ if (pollTimer) clearTimeout(pollTimer); return loadAndRender().then(function(){ pollTimer = setTimeout(doPoll, REFRESH_MS); }); }
|
|
19003
|
-
|
|
19004
|
-
initBridge().then(loadAndRender).then(function(){
|
|
19005
|
-
pollTimer = setTimeout(doPoll, REFRESH_MS);
|
|
19006
|
-
}).catch(function(e){
|
|
19007
|
-
setStatus('Bridge : ' + e.message);
|
|
19008
|
-
document.getElementById('list').innerHTML = '<div id="empty"><div class="ico">⚠</div><div>\xC9chec connexion bridge : ' + esc(e.message) + '</div></div>';
|
|
19009
|
-
});
|
|
19010
|
-
</script>
|
|
19011
|
-
</body>
|
|
19012
|
-
</html>`;
|
|
19013
|
-
|
|
19014
|
-
// src/session-story-panel.ts
|
|
19015
|
-
var SESSION_STORY_PANEL_HTML = `<!doctype html>
|
|
19016
|
-
<html lang="fr">
|
|
19017
|
-
<head>
|
|
19018
|
-
<meta charset="utf-8" />
|
|
19019
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
19020
|
-
<title>session story</title>
|
|
19021
|
-
<style>
|
|
19022
|
-
:root {
|
|
19023
|
-
color-scheme: light;
|
|
19024
|
-
--bg:#faf8f5; --panel:#fffdfa; --line:#ece5da; --line-soft:#f2ede3;
|
|
19025
|
-
--ink:#241f1a; --ink-mute:#7d7060; --ink-faint:#a9997f; --ink-ghost:#b3a893;
|
|
19026
|
-
--accent:#0d7a4f; --accent-soft:#e8f4ec;
|
|
19027
|
-
--gold:#a6701b; --gold-soft:#fff2dc;
|
|
19028
|
-
--blue:#1d4e80; --blue-soft:#e9f1fb;
|
|
19029
|
-
--violet:#5b3fa6; --violet-soft:#ede9ff;
|
|
19030
|
-
--sel:#fbeccd; --red:#b3261e; --red-soft:#fbeae8;
|
|
19031
|
-
}
|
|
19032
|
-
* { box-sizing:border-box; }
|
|
19033
|
-
html,body { height:100%; }
|
|
19034
|
-
body { margin:0; font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif; background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }
|
|
19035
|
-
.app { height:100vh; display:flex; flex-direction:column; }
|
|
19036
|
-
.hidden { display:none !important; }
|
|
19037
|
-
|
|
19038
|
-
/* \u2500\u2500 picker screen \u2500\u2500 */
|
|
19039
|
-
#pickerScreen { height:100vh; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; padding:24px; }
|
|
19040
|
-
#pickerScreen h1 { font-size:15px; font-weight:700; }
|
|
19041
|
-
#pickerList { width:min(520px,90vw); max-height:60vh; overflow-y:auto; border:1px solid var(--line); border-radius:12px; background:var(--panel); }
|
|
19042
|
-
.pk-item { padding:10px 14px; border-bottom:1px solid var(--line-soft); cursor:pointer; display:flex; align-items:center; gap:10px; }
|
|
19043
|
-
.pk-item:last-child { border-bottom:none; }
|
|
19044
|
-
.pk-item:hover { background:var(--line-soft); }
|
|
19045
|
-
.pk-name { flex:1; min-width:0; font-size:13px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
19046
|
-
.pk-meta { flex:none; font-size:10.5px; color:var(--ink-faint); font-weight:600; }
|
|
19047
|
-
.pk-empty { padding:24px; text-align:center; color:var(--ink-mute); font-size:12.5px; }
|
|
19048
|
-
.badge { display:inline-block; padding:1px 8px; border-radius:999px; font-size:10px; font-weight:700; }
|
|
19049
|
-
.badge.running { background:var(--accent-soft); color:var(--accent); }
|
|
19050
|
-
.badge.starting { background:var(--blue-soft); color:var(--blue); }
|
|
19051
|
-
.badge.exited { background:var(--line-soft); color:var(--ink-faint); }
|
|
19052
|
-
.badge.killed, .badge.error { background:var(--red-soft); color:var(--red); }
|
|
19053
|
-
|
|
19054
|
-
/* \u2500\u2500 big picture : mission + plan de sous-t\xE2ches \u2500\u2500 */
|
|
19055
|
-
.hero { flex:none; padding:13px 20px 0; border-bottom:1px solid var(--line); background:var(--panel); }
|
|
19056
|
-
.hero-top { display:flex; align-items:center; gap:13px; }
|
|
19057
|
-
.pulse { width:10px; height:10px; border-radius:50%; background:var(--accent); flex:none;
|
|
19058
|
-
box-shadow:0 0 0 0 rgba(13,122,79,.35); animation:pulse 2.4s infinite; }
|
|
19059
|
-
.pulse.off { background:var(--ink-ghost); animation:none; }
|
|
19060
|
-
@keyframes pulse { 70% { box-shadow:0 0 0 9px rgba(13,122,79,0); } 100% { box-shadow:0 0 0 0 rgba(13,122,79,0); } }
|
|
19061
|
-
.who { min-width:0; flex:1; }
|
|
19062
|
-
.who .h1 { font-size:14.5px; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
19063
|
-
.who .h2 { font-size:12px; color:var(--ink-mute); margin-top:1px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
19064
|
-
.modewrap { display:flex; border:1px solid var(--line); border-radius:9px; overflow:hidden; flex:none; }
|
|
19065
|
-
.modewrap button { border:none; background:var(--panel); color:var(--ink-mute); font-size:11px; font-weight:700; padding:6px 11px; cursor:pointer; }
|
|
19066
|
-
.modewrap button.on { background:var(--ink); color:#fdf9f2; }
|
|
19067
|
-
button.sim, a.sim { border:1px solid var(--line); background:var(--panel); color:var(--ink-mute); font-weight:700;
|
|
19068
|
-
font-size:11.5px; border-radius:8px; padding:6px 12px; cursor:pointer; flex:none; }
|
|
19069
|
-
button.sim.on { background:var(--ink); border-color:var(--ink); color:#fdf9f2; }
|
|
19070
|
-
a.sim { display:inline-flex; align-items:center; text-decoration:none; }
|
|
19071
|
-
a.sim:hover { border-color:var(--ink-ghost); color:var(--ink); }
|
|
19072
|
-
|
|
19073
|
-
/* plan strip : les sous-t\xE2ches, l'avancement d'un coup d'\u0153il */
|
|
19074
|
-
.plan { display:flex; gap:6px; overflow-x:auto; padding:11px 0 12px; scrollbar-width:none; }
|
|
19075
|
-
.plan::-webkit-scrollbar { display:none; }
|
|
19076
|
-
.pt { flex:none; display:flex; align-items:center; gap:6px; font-size:11.5px; font-weight:700; padding:5px 11px;
|
|
19077
|
-
border-radius:999px; border:1px solid var(--line); background:var(--bg); color:var(--ink-mute); cursor:pointer; white-space:nowrap; }
|
|
19078
|
-
.pt:hover { border-color:var(--ink-ghost); }
|
|
19079
|
-
.pt .st { font-size:10px; }
|
|
19080
|
-
.pt.done { color:var(--accent); background:var(--accent-soft); border-color:transparent; }
|
|
19081
|
-
.pt.cur { color:var(--gold); background:var(--gold-soft); border-color:transparent; }
|
|
19082
|
-
.pt.cur .st { animation:blink 1.6s infinite; }
|
|
19083
|
-
@keyframes blink { 50% { opacity:.35; } }
|
|
19084
|
-
|
|
19085
|
-
/* \u2500\u2500 corps \u2500\u2500 */
|
|
19086
|
-
.body { flex:1; display:flex; min-height:0; }
|
|
19087
|
-
.feedcol { flex:1; min-width:320px; display:flex; flex-direction:column; }
|
|
19088
|
-
.feed { flex:1; overflow-y:auto; padding:4px 14px 10px; display:flex; flex-direction:column; scroll-behavior:smooth; }
|
|
19089
|
-
.fspacer { flex:1; }
|
|
19090
|
-
|
|
19091
|
-
/* chapitres */
|
|
19092
|
-
.chap { flex:none; position:sticky; top:0; z-index:5; margin:8px -4px 2px; padding:7px 12px; display:flex; align-items:center; gap:9px;
|
|
19093
|
-
background:color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter:blur(6px);
|
|
19094
|
-
border-radius:9px; cursor:pointer; font-size:11.5px; font-weight:800; letter-spacing:.03em; color:var(--ink-mute); }
|
|
19095
|
-
.chap:hover { color:var(--ink); }
|
|
19096
|
-
.chap .cst { flex:none; width:17px; height:17px; border-radius:50%; display:grid; place-items:center; font-size:9.5px; font-weight:900; }
|
|
19097
|
-
.chap.done .cst { background:var(--accent-soft); color:var(--accent); }
|
|
19098
|
-
.chap.cur .cst { background:var(--gold-soft); color:var(--gold); }
|
|
19099
|
-
.chap .cnum { color:var(--ink-ghost); font-weight:700; }
|
|
19100
|
-
.chap .csum { flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
19101
|
-
.chap .cmeta { flex:none; font-size:10.5px; color:var(--ink-ghost); font-weight:600; }
|
|
19102
|
-
.chap .cchev { flex:none; color:var(--ink-ghost); transition:transform .15s; }
|
|
19103
|
-
.chap.open .cchev { transform:rotate(90deg); }
|
|
19104
|
-
|
|
19105
|
-
.row { flex:none; min-height:44px; margin:1px 0 1px 10px; padding:5px 12px 5px 10px; display:flex; align-items:center; gap:11px;
|
|
19106
|
-
cursor:pointer; border-radius:10px; border-left:3px solid transparent; transition:background .12s; }
|
|
19107
|
-
.row:hover { background:var(--line-soft); }
|
|
19108
|
-
.row[aria-selected="true"] { background:var(--sel); border-left-color:var(--gold); }
|
|
19109
|
-
.row .ico { width:24px; height:24px; border-radius:8px; flex:none; display:grid; place-items:center; font-size:11.5px; font-weight:800; }
|
|
19110
|
-
.ico.k-text { background:var(--blue-soft); color:var(--blue); }
|
|
19111
|
-
.ico.k-edit { background:var(--gold-soft); color:var(--gold); }
|
|
19112
|
-
.ico.k-bash { background:var(--accent-soft); color:var(--accent); }
|
|
19113
|
-
.ico.k-read { background:var(--violet-soft); color:var(--violet); }
|
|
19114
|
-
.ico.k-user { background:var(--ink); color:#fdf9f2; }
|
|
19115
|
-
.row .mid { flex:1; min-width:0; }
|
|
19116
|
-
.row .sum { display:block; font-size:13.5px; line-height:1.35; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
19117
|
-
.row .raw1 { display:block; font-size:10.5px; color:var(--ink-faint); font-family:ui-monospace,Menlo,monospace;
|
|
19118
|
-
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:1px; }
|
|
19119
|
-
body:not(.tech) .row .raw1 { display:none; }
|
|
19120
|
-
.row .route { display:inline-block; font-size:10px; font-weight:800; padding:1px 8px; border-radius:999px; margin-top:2px; }
|
|
19121
|
-
.route.cont { background:var(--blue-soft); color:var(--blue); }
|
|
19122
|
-
.route.newt { background:var(--gold-soft); color:var(--gold); }
|
|
19123
|
-
.row .cnt { flex:none; font-size:10px; font-weight:800; color:var(--gold); background:var(--gold-soft); padding:2px 7px; border-radius:999px; }
|
|
19124
|
-
.row .ts { flex:none; font-size:10.5px; color:var(--ink-ghost); font-variant-numeric:tabular-nums; }
|
|
19125
|
-
@keyframes slidein { from { opacity:0; transform:translateY(6px); } }
|
|
19126
|
-
.row.new { animation:slidein .25s ease-out; }
|
|
19127
|
-
|
|
19128
|
-
/* \u2500\u2500 panneau ancr\xE9 \u2500\u2500 */
|
|
19129
|
-
.panel { flex:none; width:0; overflow:hidden; border-left:1px solid transparent; background:var(--panel);
|
|
19130
|
-
display:flex; flex-direction:column; transition:width .22s ease, border-color .22s; }
|
|
19131
|
-
.panel.open { width:min(430px,46vw); border-left-color:var(--line); }
|
|
19132
|
-
.panel-inner { width:min(430px,46vw); flex:1; display:flex; flex-direction:column; min-height:0; }
|
|
19133
|
-
.phead { flex:none; padding:14px 16px 12px; border-bottom:1px solid var(--line); display:flex; align-items:flex-start; gap:11px; }
|
|
19134
|
-
.phead .ico { width:28px; height:28px; font-size:13px; border-radius:9px; }
|
|
19135
|
-
.phead .tt { min-width:0; flex:1; }
|
|
19136
|
-
.phead .t { font-size:14px; font-weight:700; line-height:1.4; }
|
|
19137
|
-
.phead .s { font-size:11px; color:var(--ink-faint); margin-top:3px; font-variant-numeric:tabular-nums; }
|
|
19138
|
-
.pnav { display:flex; gap:4px; flex:none; }
|
|
19139
|
-
.pnav button { width:26px; height:26px; border:1px solid var(--line); background:var(--panel); border-radius:8px;
|
|
19140
|
-
color:var(--ink-mute); font-size:12px; cursor:pointer; display:grid; place-items:center; }
|
|
19141
|
-
.pnav button:disabled { opacity:.3; cursor:default; }
|
|
19142
|
-
.pbody { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:14px; }
|
|
19143
|
-
.plain { font-size:14px; line-height:1.7; }
|
|
19144
|
-
.plain .why { margin-top:8px; font-size:12.5px; color:var(--ink-mute); line-height:1.6; }
|
|
19145
|
-
.facts { display:flex; flex-wrap:wrap; gap:6px; }
|
|
19146
|
-
.fact { font-size:11px; font-weight:700; background:var(--bg); border:1px solid var(--line); color:var(--ink-mute); padding:4px 10px; border-radius:999px; }
|
|
19147
|
-
.fact.ok { background:var(--accent-soft); border-color:transparent; color:var(--accent); }
|
|
19148
|
-
details.techbox { border:1px solid var(--line); border-radius:12px; background:var(--bg); overflow:hidden; }
|
|
19149
|
-
details.techbox summary { list-style:none; cursor:pointer; padding:10px 14px; font-size:11.5px; font-weight:800;
|
|
19150
|
-
letter-spacing:.04em; text-transform:uppercase; color:var(--ink-faint); display:flex; align-items:center; gap:8px; }
|
|
19151
|
-
details.techbox summary::-webkit-details-marker { display:none; }
|
|
19152
|
-
details.techbox summary::after { content:"\u25B8"; margin-left:auto; transition:transform .15s; }
|
|
19153
|
-
details.techbox[open] summary::after { transform:rotate(90deg); }
|
|
19154
|
-
.techlist { padding:0 12px 12px; display:flex; flex-direction:column; gap:8px; }
|
|
19155
|
-
.titem { border:1px solid var(--line); border-radius:10px; background:var(--panel); overflow:hidden; }
|
|
19156
|
-
.titem .th { padding:8px 12px; font-size:11.5px; font-weight:700; color:var(--ink-mute); display:flex; align-items:center; gap:8px; }
|
|
19157
|
-
.titem .th .copy { margin-left:auto; border:none; background:none; color:var(--ink-ghost); font-size:11px; cursor:pointer; padding:2px 4px; border-radius:5px; }
|
|
19158
|
-
.titem .th .copy:hover { color:var(--ink); background:var(--line-soft); }
|
|
19159
|
-
.titem pre { margin:0; border-top:1px solid var(--line-soft); font-family:ui-monospace,Menlo,monospace; font-size:11.5px;
|
|
19160
|
-
line-height:1.55; color:#4a4236; padding:9px 12px; overflow:auto; max-height:240px; white-space:pre-wrap; word-break:break-word; }
|
|
19161
|
-
.d-text { font-size:13.5px; line-height:1.7; }
|
|
19162
|
-
.d-text p { margin:0 0 8px; }
|
|
19163
|
-
.d-text p:last-child { margin-bottom:0; }
|
|
19164
|
-
.d-text h1, .d-text h2, .d-text h3, .d-text h4, .d-text h5, .d-text h6 { margin:12px 0 6px; line-height:1.3; }
|
|
19165
|
-
.d-text h1:first-child, .d-text h2:first-child, .d-text h3:first-child { margin-top:0; }
|
|
19166
|
-
.d-text ul, .d-text ol { margin:0 0 8px; padding-left:20px; }
|
|
19167
|
-
.d-text code { font-family:ui-monospace,Menlo,monospace; font-size:12.5px; background:var(--bg); border-radius:4px; padding:1px 5px; }
|
|
19168
|
-
.d-text pre { margin:0 0 8px; background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:9px 12px; overflow:auto; }
|
|
19169
|
-
.d-text pre code { background:none; border-radius:0; padding:0; }
|
|
19170
|
-
.d-text table { border-collapse:collapse; margin:0 0 8px; font-size:12.5px; }
|
|
19171
|
-
.d-text th, .d-text td { border:1px solid var(--line); padding:4px 8px; text-align:left; }
|
|
19172
|
-
.d-text a { color:var(--blue); }
|
|
19173
|
-
.pfoot { flex:none; border-top:1px solid var(--line); padding:9px 16px; font-size:11px; color:var(--ink-ghost); display:flex; gap:10px; }
|
|
19174
|
-
.kbd { font-family:ui-monospace,Menlo,monospace; font-size:10px; border:1px solid var(--line); border-bottom-width:2px;
|
|
19175
|
-
border-radius:5px; padding:1px 5px; background:var(--panel); color:var(--ink-mute); }
|
|
19176
|
-
|
|
19177
|
-
/* \u2500\u2500 bo\xEEte d'envoi + routage IA \u2500\u2500 */
|
|
19178
|
-
.composer { flex:none; border-top:1px solid var(--line); background:var(--panel); padding:10px 14px; }
|
|
19179
|
-
.composer .cbar { display:flex; gap:8px; }
|
|
19180
|
-
.composer textarea { flex:1; resize:none; border:1px solid var(--line); border-radius:10px; padding:9px 12px; font:inherit; font-size:13px; max-height:110px; background:var(--bg); }
|
|
19181
|
-
.composer textarea:focus { outline:2px solid #241f1a22; }
|
|
19182
|
-
.composer textarea:disabled { opacity:.5; cursor:not-allowed; }
|
|
19183
|
-
.composer button { border:none; border-radius:10px; padding:0 16px; background:var(--ink); color:#fdf9f2; font-weight:700; font-size:13px; cursor:pointer; }
|
|
19184
|
-
.composer button:disabled { opacity:.4; cursor:not-allowed; }
|
|
19185
|
-
.composer .routing { font-size:11px; color:var(--ink-faint); padding:6px 2px 0; min-height:22px; }
|
|
19186
|
-
.composer .routing .r-cont { color:var(--blue); font-weight:700; }
|
|
19187
|
-
.composer .routing .r-newt { color:var(--gold); font-weight:700; }
|
|
19188
|
-
#statusbar { flex:none; padding:4px 20px; font-size:10.5px; color:var(--ink-ghost); border-top:1px solid var(--line-soft); }
|
|
19189
|
-
</style>
|
|
19190
|
-
</head>
|
|
19191
|
-
<body>
|
|
19192
|
-
<div id="pickerScreen">
|
|
19193
|
-
<h1>Choisis une session</h1>
|
|
19194
|
-
<div id="pickerList"><div class="pk-empty">Connexion…</div></div>
|
|
19195
|
-
</div>
|
|
19196
|
-
|
|
19197
|
-
<div class="app hidden" id="storyScreen">
|
|
19198
|
-
<div class="hero">
|
|
19199
|
-
<div class="hero-top">
|
|
19200
|
-
<span class="pulse" id="pulse"></span>
|
|
19201
|
-
<div class="who">
|
|
19202
|
-
<div class="h1" id="heroTitle"></div>
|
|
19203
|
-
<div class="h2" id="heroSub"></div>
|
|
19204
|
-
</div>
|
|
19205
|
-
<div class="modewrap"><button id="modeSimple" class="on" type="button">Simple</button><button id="modeTech" type="button">Tech</button></div>
|
|
19206
|
-
<a class="sim" id="fullPanelLink" href="#" target="_blank" rel="noopener" title="Ouvrir le panneau complet (Terminal/Chat/JSON/TTY)">↗ panneau complet</a>
|
|
19207
|
-
<button class="sim" id="switchBtn" type="button">⇆ changer</button>
|
|
19208
|
-
</div>
|
|
19209
|
-
<div class="plan" id="plan"></div>
|
|
19210
|
-
</div>
|
|
19211
|
-
|
|
19212
|
-
<div class="body">
|
|
19213
|
-
<div class="feedcol">
|
|
19214
|
-
<div class="feed" id="feed"><div class="fspacer"></div><div id="rows"></div></div>
|
|
19215
|
-
<div class="composer">
|
|
19216
|
-
<div class="cbar">
|
|
19217
|
-
<textarea id="msgBox" rows="1" placeholder="\xC9cris \xE0 l'agent\u2026 (la surcouche classe ton message : suite de la sous-t\xE2che ou nouvelle sous-t\xE2che)"></textarea>
|
|
19218
|
-
<button id="sendBtn" type="button">Envoyer</button>
|
|
19219
|
-
</div>
|
|
19220
|
-
<div class="routing" id="routing"></div>
|
|
19221
|
-
</div>
|
|
19222
|
-
</div>
|
|
19223
|
-
|
|
19224
|
-
<aside class="panel" id="panel" aria-label="D\xE9tail de l'\xE9tape">
|
|
19225
|
-
<div class="panel-inner">
|
|
19226
|
-
<div class="phead">
|
|
19227
|
-
<span class="ico" id="pIco"></span>
|
|
19228
|
-
<div class="tt"><div class="t" id="pTitle"></div><div class="s" id="pSub"></div></div>
|
|
19229
|
-
<div class="pnav"><button id="pPrev" type="button">\u2191</button><button id="pNext" type="button">\u2193</button><button id="pClose" type="button">\u2715</button></div>
|
|
19230
|
-
</div>
|
|
19231
|
-
<div class="pbody" id="pBody"></div>
|
|
19232
|
-
<div class="pfoot"><span class="kbd">\u2191</span><span class="kbd">\u2193</span> naviguer \xB7 <span class="kbd">Esc</span> fermer</div>
|
|
19233
|
-
</div>
|
|
19234
|
-
</aside>
|
|
19235
|
-
</div>
|
|
19236
|
-
<div id="statusbar"></div>
|
|
19237
|
-
</div>
|
|
19238
|
-
|
|
19239
|
-
<script>
|
|
19240
|
-
var $=function(id){ return document.getElementById(id); };
|
|
19241
|
-
var esc=function(s){ return String(s==null?"":s).replace(/[&<>]/g,function(c){ return {"&":"&","<":"<",">":">"}[c]; }); };
|
|
19242
|
-
|
|
19243
|
-
// ============================================================
|
|
19244
|
-
// renderMd \u2014 vanilla-JS port of markdown-lite.ts. Kept
|
|
19245
|
-
// function-for-function identical so the two are easy to diff (same
|
|
19246
|
-
// self-contained-panel constraint as buildStoryJs below): headers,
|
|
19247
|
-
// bold/italic, inline/fenced code, bullet/numbered lists, pipe tables and
|
|
19248
|
-
// links, with every raw text run HTML-escaped before any generated tag
|
|
19249
|
-
// wraps it.
|
|
19250
|
-
// ============================================================
|
|
19251
|
-
function escHtmlMd(s){ return String(s).replace(/[&<>"]/g,function(c){ if(c==='&') return '&'; if(c==='<') return '<'; if(c==='>') return '>'; return '"'; }); }
|
|
19252
|
-
function renderInlineMd(text){
|
|
19253
|
-
var out=escHtmlMd(text);
|
|
19254
|
-
out=out.replace(/\`([^\`]+)\`/g,function(_m,code){ return '<code>'+code+'</code>'; });
|
|
19255
|
-
out=out.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g,function(_m,label,url){ return '<a href="'+url+'" target="_blank" rel="noopener noreferrer">'+label+'</a>'; });
|
|
19256
|
-
out=out.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');
|
|
19257
|
-
out=out.replace(/(^|[^*])\\*([^*]+)\\*(?!\\*)/g,'$1<em>$2</em>');
|
|
19258
|
-
return out;
|
|
19259
|
-
}
|
|
19260
|
-
function isTableSepMd(line){ return /^\\s*\\|?\\s*:?-+:?\\s*(\\|\\s*:?-+:?\\s*)+\\|?\\s*$/.test(line); }
|
|
19261
|
-
function splitRowMd(line){ return line.trim().replace(/^\\|/,'').replace(/\\|$/,'').split('|').map(function(c){ return c.trim(); }); }
|
|
19262
|
-
function renderMd(md){
|
|
19263
|
-
var lines=String(md==null?'':md).replace(/\\r\\n?/g,'\\n').split('\\n');
|
|
19264
|
-
var out=[], para=[], list=null;
|
|
19265
|
-
function flushPara(){ if(para.length){ out.push('<p>'+para.map(renderInlineMd).join('<br>')+'</p>'); para=[]; } }
|
|
19266
|
-
function flushList(){ if(list){ var tag=list.ordered?'ol':'ul'; out.push('<'+tag+'>'+list.items.map(function(i){ return '<li>'+renderInlineMd(i)+'</li>'; }).join('')+'</'+tag+'>'); list=null; } }
|
|
19267
|
-
function flushAll(){ flushPara(); flushList(); }
|
|
19268
|
-
var i=0;
|
|
19269
|
-
while(i<lines.length){
|
|
19270
|
-
var line=lines[i];
|
|
19271
|
-
if(/^\\s*\`\`\`/.test(line)){
|
|
19272
|
-
flushAll();
|
|
19273
|
-
var code=[]; i+=1;
|
|
19274
|
-
while(i<lines.length && !/^\\s*\`\`\`/.test(lines[i])){ code.push(lines[i]); i+=1; }
|
|
19275
|
-
i+=1;
|
|
19276
|
-
out.push('<pre><code>'+escHtmlMd(code.join('\\n'))+'</code></pre>');
|
|
19277
|
-
continue;
|
|
19278
|
-
}
|
|
19279
|
-
var header=line.match(/^(#{1,6})\\s+(.*)$/);
|
|
19280
|
-
if(header){
|
|
19281
|
-
flushAll();
|
|
19282
|
-
var level=header[1].length;
|
|
19283
|
-
out.push('<h'+level+'>'+renderInlineMd(header[2].trim())+'</h'+level+'>');
|
|
19284
|
-
i+=1;
|
|
19285
|
-
continue;
|
|
19286
|
-
}
|
|
19287
|
-
if(/^\\s*\\|/.test(line) && i+1<lines.length && isTableSepMd(lines[i+1])){
|
|
19288
|
-
flushAll();
|
|
19289
|
-
var headCells=splitRowMd(line);
|
|
19290
|
-
i+=2;
|
|
19291
|
-
var bodyRows=[];
|
|
19292
|
-
while(i<lines.length && /^\\s*\\|/.test(lines[i])){ bodyRows.push(splitRowMd(lines[i])); i+=1; }
|
|
19293
|
-
out.push('<table><thead><tr>'+headCells.map(function(c){ return '<th>'+renderInlineMd(c)+'</th>'; }).join('')+'</tr></thead><tbody>'
|
|
19294
|
-
+bodyRows.map(function(r){ return '<tr>'+r.map(function(c){ return '<td>'+renderInlineMd(c)+'</td>'; }).join('')+'</tr>'; }).join('')+'</tbody></table>');
|
|
19295
|
-
continue;
|
|
19296
|
-
}
|
|
19297
|
-
var bullet=line.match(/^\\s*[-*+]\\s+(.*)$/);
|
|
19298
|
-
var numbered=line.match(/^\\s*\\d+\\.\\s+(.*)$/);
|
|
19299
|
-
if(bullet || numbered){
|
|
19300
|
-
flushPara();
|
|
19301
|
-
var ordered=!!numbered;
|
|
19302
|
-
var item=(bullet||numbered)[1];
|
|
19303
|
-
if(!list || list.ordered!==ordered){ flushList(); list={ordered:ordered,items:[]}; }
|
|
19304
|
-
list.items.push(item);
|
|
19305
|
-
i+=1;
|
|
19306
|
-
continue;
|
|
19307
|
-
}
|
|
19308
|
-
if(line.trim()===''){ flushAll(); i+=1; continue; }
|
|
19309
|
-
flushList();
|
|
19310
|
-
para.push(line);
|
|
19311
|
-
i+=1;
|
|
19312
|
-
}
|
|
19313
|
-
flushAll();
|
|
19314
|
-
return out.join('');
|
|
19315
|
-
}
|
|
19316
|
-
|
|
19317
|
-
${panelBridgeScript("agentproto-session-story-panel")}
|
|
19318
|
-
// Best-effort: some hosts forward the triggering tool call's arguments as
|
|
19319
|
-
// a notification so the panel can auto-open the right session. Purely
|
|
19320
|
-
// additive \u2014 the session picker is the reliable path when this never
|
|
19321
|
-
// arrives.
|
|
19322
|
-
var pendingSessionId=null;
|
|
19323
|
-
onHostNotification(function(method, params){
|
|
19324
|
-
if(/tool-input|tool-call/.test(method)){
|
|
19325
|
-
var args=(params && (params.arguments || params.input)) || {};
|
|
19326
|
-
if(args && args.sessionId) pendingSessionId=args.sessionId;
|
|
19327
|
-
}
|
|
19328
|
-
});
|
|
19329
|
-
|
|
19330
|
-
// ============================================================
|
|
19331
|
-
// buildStory \u2014 vanilla-JS port of session-story.ts. Kept
|
|
19332
|
-
// function-for-function identical so the two are easy to diff; the panel
|
|
19333
|
-
// resource must be fully self-contained (no bundler/dynamic import), so it
|
|
19334
|
-
// cannot import the TS module directly.
|
|
19335
|
-
// ============================================================
|
|
19336
|
-
|
|
19337
|
-
var SALIENT_KEYS=["file_path","path","filePath","file","command","pattern","query","q","url","todos","description","prompt"];
|
|
19338
|
-
function truncateStr(v,max){ var o=String(v).replace(/\\s+/g,' ').trim(); return o.length>max? o.slice(0,max-1)+'\u2026':o; }
|
|
19339
|
-
function formatArgValue(v){
|
|
19340
|
-
if(typeof v==='string') return v;
|
|
19341
|
-
if(Array.isArray(v)) return v.length+' item'+(v.length===1?'':'s');
|
|
19342
|
-
if(v && typeof v==='object') return JSON.stringify(v);
|
|
19343
|
-
return String(v);
|
|
19344
|
-
}
|
|
19345
|
-
function pickSalient(args){
|
|
19346
|
-
for(var i=0;i<SALIENT_KEYS.length;i++){
|
|
19347
|
-
var k=SALIENT_KEYS[i], v=args[k];
|
|
19348
|
-
if(v!==undefined && v!==null && v!=='') return formatArgValue(v);
|
|
19349
|
-
}
|
|
19350
|
-
return null;
|
|
19351
|
-
}
|
|
19352
|
-
function formatToolCall(name,args){
|
|
19353
|
-
name=name||'tool';
|
|
19354
|
-
args=(args && typeof args==='object' && !Array.isArray(args)) ? args : {};
|
|
19355
|
-
var salient=pickSalient(args);
|
|
19356
|
-
if(salient!==null){
|
|
19357
|
-
if(name.toLowerCase().indexOf(salient.toLowerCase())>=0) return truncateStr(name,120);
|
|
19358
|
-
return truncateStr(name+' '+salient,120);
|
|
19359
|
-
}
|
|
19360
|
-
if(Object.keys(args).length===0) return name;
|
|
19361
|
-
return truncateStr(name+' '+JSON.stringify(args),120);
|
|
19362
|
-
}
|
|
19363
|
-
function extractText(v){
|
|
19364
|
-
if(v==null) return null;
|
|
19365
|
-
if(typeof v==='string') return v;
|
|
19366
|
-
if(Array.isArray(v)){
|
|
19367
|
-
var parts=v.map(extractText).filter(function(x){ return x!=null; });
|
|
19368
|
-
return parts.length? parts.join('\\n') : null;
|
|
19369
|
-
}
|
|
19370
|
-
if(typeof v==='object'){
|
|
19371
|
-
if(typeof v.text==='string') return v.text;
|
|
19372
|
-
if(typeof v.message==='string') return v.message;
|
|
19373
|
-
if(Array.isArray(v.content)) return extractText(v.content);
|
|
19374
|
-
if(typeof v.error==='string') return v.error;
|
|
19375
|
-
if(v.error && typeof v.error==='object' && typeof v.error.message==='string') return v.error.message;
|
|
19376
|
-
return null;
|
|
19377
|
-
}
|
|
19378
|
-
return null;
|
|
19379
|
-
}
|
|
19380
|
-
function formatToolResult(toolName,result,isError){
|
|
19381
|
-
var text=extractText(result);
|
|
19382
|
-
if(isError){
|
|
19383
|
-
var message=text!=null? text : (result!=null? JSON.stringify(result) : 'failed');
|
|
19384
|
-
var firstLine=String(message).split(/\\r?\\n/)[0] || message;
|
|
19385
|
-
return truncateStr(firstLine,160);
|
|
19386
|
-
}
|
|
19387
|
-
if(text==null) return null;
|
|
19388
|
-
var trimmed=text.trim();
|
|
19389
|
-
if(!trimmed) return null;
|
|
19390
|
-
var lines=trimmed.split(/\\r?\\n/);
|
|
19391
|
-
if(lines.length>1){
|
|
19392
|
-
var bytes=new TextEncoder().encode(trimmed).length;
|
|
19393
|
-
return lines.length+' lines, '+bytes+'B';
|
|
19394
|
-
}
|
|
19395
|
-
return truncateStr(lines[0],160);
|
|
19396
|
-
}
|
|
19397
|
-
|
|
19398
|
-
function classifyKind(toolCalls){
|
|
19399
|
-
if(!toolCalls || toolCalls.length===0) return 'text';
|
|
19400
|
-
var names=toolCalls.map(function(t){ return t.name.toLowerCase(); });
|
|
19401
|
-
if(names.some(function(n){ return /edit|write/.test(n); })) return 'edit';
|
|
19402
|
-
if(names.some(function(n){ return /bash|terminal|command/.test(n); })) return 'bash';
|
|
19403
|
-
if(names.some(function(n){ return /read|grep|glob/.test(n); })) return 'read';
|
|
19404
|
-
return 'text';
|
|
19405
|
-
}
|
|
19406
|
-
var NEW_CHAPTER_RE=/\\b(aussi|autre|ensuite|nouveau|nouvelle|plut[o\xF4]t|maintenant|apr[e\xE8]s \xE7a|il faudrait|peux[- ]tu|on pourrait|ajoute|g[e\xE8]re)\\b/iu;
|
|
19407
|
-
function classifyRoute(text){
|
|
19408
|
-
var newt=NEW_CHAPTER_RE.test(text);
|
|
19409
|
-
if(!newt) return {route:'cont'};
|
|
19410
|
-
var title=text.replace(/[.?!].*$/,'').slice(0,42);
|
|
19411
|
-
return {route:'newt', title:title};
|
|
19412
|
-
}
|
|
19413
|
-
function formatTsJs(ts){
|
|
19414
|
-
if(ts===undefined || ts===null || isNaN(ts)) return '';
|
|
19415
|
-
return new Date(ts).toISOString().slice(11,19);
|
|
19416
|
-
}
|
|
19417
|
-
function firstMeaningfulLine(text){
|
|
19418
|
-
if(!text) return undefined;
|
|
19419
|
-
var lines=text.split('\\n').map(function(l){ return l.trim(); }).filter(function(l){ return l.length>0; });
|
|
19420
|
-
return lines[0];
|
|
19421
|
-
}
|
|
19422
|
-
function lineCountOf(text){
|
|
19423
|
-
var n=(text||'').split('\\n').filter(function(l){ return l.trim().length>0; }).length;
|
|
19424
|
-
return n||1;
|
|
19425
|
-
}
|
|
19426
|
-
function parseArgsJson(s){ try{ return JSON.parse(s); }catch(e){ return {}; } }
|
|
19427
|
-
|
|
19428
|
-
function foldToolStep(assistant,toolResults){
|
|
19429
|
-
var toolCalls=assistant.toolCalls||[];
|
|
19430
|
-
var kind=classifyKind(toolCalls);
|
|
19431
|
-
var count=toolCalls.length||1;
|
|
19432
|
-
var items=[], facts=[];
|
|
19433
|
-
if(assistant.text && assistant.text.trim()) items.push({text:assistant.text.trim()});
|
|
19434
|
-
toolCalls.forEach(function(tc,i){
|
|
19435
|
-
var args=parseArgsJson(tc.args);
|
|
19436
|
-
var h=formatToolCall(tc.name,args);
|
|
19437
|
-
var resultMsg=toolResults[i];
|
|
19438
|
-
var resultText=(resultMsg && resultMsg.text) || '';
|
|
19439
|
-
var isError=resultText.indexOf('[error]')===0;
|
|
19440
|
-
var r=isError? resultText.slice(7).trim() : resultText;
|
|
19441
|
-
items.push({h:h,r:r});
|
|
19442
|
-
var fact=formatToolResult(tc.name,r,isError);
|
|
19443
|
-
if(fact) facts.push(fact);
|
|
19444
|
-
});
|
|
19445
|
-
var firstLine=firstMeaningfulLine(assistant.text);
|
|
19446
|
-
var firstToolCall=toolCalls[0];
|
|
19447
|
-
var sum=firstLine!==undefined? firstLine : (firstToolCall? formatToolCall(firstToolCall.name,parseArgsJson(firstToolCall.args)) : '\u2026');
|
|
19448
|
-
var raw1;
|
|
19449
|
-
if(toolCalls.length===0) raw1='assistant \xB7 '+lineCountOf(assistant.text)+' ligne(s)';
|
|
19450
|
-
else if(toolCalls.length===1) raw1=formatToolCall(firstToolCall.name,parseArgsJson(firstToolCall.args));
|
|
19451
|
-
else raw1=(firstToolCall? firstToolCall.name : 'tool')+' \xD7'+toolCalls.length;
|
|
19452
|
-
return {kind:kind, ts:formatTsJs(assistant.ts), sum:sum, raw1:raw1, count:count, facts:facts, items:items};
|
|
19453
|
-
}
|
|
19454
|
-
function foldUserStep(msg){
|
|
19455
|
-
var text=msg.text||'';
|
|
19456
|
-
return {kind:'user', ts:formatTsJs(msg.ts), sum:'\xAB '+truncateStr(text,80)+' \xBB', raw1:'user \xB7 '+lineCountOf(text)+' ligne(s)', count:1, facts:[], items:[{text:text}], userText:text};
|
|
19457
|
-
}
|
|
19458
|
-
function foldOrphanToolStep(msg){
|
|
19459
|
-
var text=msg.text||'';
|
|
19460
|
-
var isError=text.indexOf('[error]')===0;
|
|
19461
|
-
var r=isError? text.slice(7).trim() : text;
|
|
19462
|
-
var name=msg.toolName||'tool';
|
|
19463
|
-
var fact=formatToolResult(name,r,isError);
|
|
19464
|
-
return {kind:classifyKind([{name:name}]), ts:formatTsJs(msg.ts), sum: msg.toolName? (msg.toolName+' \xB7 r\xE9sultat') : "R\xE9sultat d'outil", raw1: msg.toolName||'tool', count:1, facts: fact?[fact]:[], items:[{h:name,r:r}]};
|
|
19465
|
-
}
|
|
19466
|
-
function foldSystemStep(msg){
|
|
19467
|
-
var text=msg.text||'';
|
|
19468
|
-
var line=firstMeaningfulLine(text);
|
|
19469
|
-
return {kind:'text', ts:formatTsJs(msg.ts), sum: line!==undefined? line : text, raw1:'system', count:1, facts:[], items: text?[{text:text}]:[]};
|
|
19470
|
-
}
|
|
19471
|
-
function foldMessages(messages){
|
|
19472
|
-
var steps=[], i=0;
|
|
19473
|
-
while(i<messages.length){
|
|
19474
|
-
var msg=messages[i];
|
|
19475
|
-
if(msg.role==='user'){ steps.push(foldUserStep(msg)); i+=1; continue; }
|
|
19476
|
-
if(msg.role==='assistant'){
|
|
19477
|
-
var j=i+1, toolResults=[];
|
|
19478
|
-
while(j<messages.length && messages[j].role==='tool'){ toolResults.push(messages[j]); j+=1; }
|
|
19479
|
-
steps.push(foldToolStep(msg,toolResults)); i=j; continue;
|
|
19480
|
-
}
|
|
19481
|
-
if(msg.role==='tool'){ steps.push(foldOrphanToolStep(msg)); i+=1; continue; }
|
|
19482
|
-
steps.push(foldSystemStep(msg)); i+=1;
|
|
19483
|
-
}
|
|
19484
|
-
return steps;
|
|
19485
|
-
}
|
|
19486
|
-
function buildStoryJs(messages){
|
|
19487
|
-
var folded=foldMessages(messages||[]);
|
|
19488
|
-
var chapters=[], steps=[];
|
|
19489
|
-
var currentChapterId, sawFirstUser=false;
|
|
19490
|
-
function closeCurrent(){ var cur=chapters.filter(function(c){ return c.id===currentChapterId; })[0]; if(cur) cur.status='done'; }
|
|
19491
|
-
function openChapter(title){ var id='c'+(chapters.length+1); chapters.push({id:id,title:title,status:'cur'}); return id; }
|
|
19492
|
-
folded.forEach(function(step){
|
|
19493
|
-
var route;
|
|
19494
|
-
if(step.kind==='user' && step.userText!==undefined){
|
|
19495
|
-
if(!sawFirstUser){ sawFirstUser=true; currentChapterId=openChapter('Cadrage'); }
|
|
19496
|
-
else {
|
|
19497
|
-
var verdict=classifyRoute(step.userText);
|
|
19498
|
-
route=verdict.route;
|
|
19499
|
-
if(verdict.route==='newt'){ closeCurrent(); currentChapterId=openChapter(verdict.title||'Nouvelle sous-t\xE2che'); }
|
|
19500
|
-
}
|
|
19501
|
-
} else if(currentChapterId===undefined){ currentChapterId=openChapter('Cadrage'); }
|
|
19502
|
-
var out={chap:currentChapterId, kind:step.kind, ts:step.ts, sum:step.sum, raw1:step.raw1, count:step.count, facts:step.facts, items:step.items};
|
|
19503
|
-
if(route) out.route=route;
|
|
19504
|
-
steps.push(out);
|
|
19505
|
-
});
|
|
19506
|
-
return {chapters:chapters, steps:steps};
|
|
19507
|
-
}
|
|
19508
|
-
|
|
19509
|
-
// ============================================================
|
|
19510
|
-
// App state
|
|
19511
|
-
// ============================================================
|
|
19512
|
-
var sessions=[];
|
|
19513
|
-
var activeSessionId=null;
|
|
19514
|
-
var story={chapters:[], steps:[]};
|
|
19515
|
-
var open={};
|
|
19516
|
-
var selected=-1;
|
|
19517
|
-
var lastSeenOutputAt=null;
|
|
19518
|
-
var pollTimer=null, polling=false;
|
|
19519
|
-
var ICONS={text:["k-text","A"],edit:["k-edit","\u270E"],bash:["k-bash","\u25B8"],read:["k-read","\u2315"],user:["k-user","T"]};
|
|
19520
|
-
var icoSpec=function(k){ return ICONS[k]||ICONS.text; };
|
|
19521
|
-
var chapOf=function(id){ return story.chapters.filter(function(c){ return c.id===id; })[0]; };
|
|
19522
|
-
var curChap=function(){ return story.chapters.filter(function(c){ return c.status==='cur'; })[0] || story.chapters[story.chapters.length-1]; };
|
|
19523
|
-
|
|
19524
|
-
function setStatus(msg){ $('statusbar').textContent=msg; }
|
|
19525
|
-
function nowTs(){ return new Date().toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit',second:'2-digit'}); }
|
|
19526
|
-
|
|
19527
|
-
function titleOf(s){
|
|
19528
|
-
return s.label || s.name || (s.command? s.command.split(/\\s+/)[0].split('/').pop() : null) || s.id.slice(0,8);
|
|
19529
|
-
}
|
|
19530
|
-
|
|
19531
|
-
// ============================================================
|
|
19532
|
-
// Picker screen
|
|
19533
|
-
// ============================================================
|
|
19534
|
-
function renderPicker(){
|
|
19535
|
-
var el=$('pickerList');
|
|
19536
|
-
if(!sessions.length){ el.innerHTML='<div class="pk-empty">Aucune session</div>'; return; }
|
|
19537
|
-
var html='';
|
|
19538
|
-
sessions.forEach(function(s){
|
|
19539
|
-
html+='<div class="pk-item" data-id="'+esc(s.id)+'">'
|
|
19540
|
-
+ '<span class="pk-name">'+esc(titleOf(s))+'</span>'
|
|
19541
|
-
+ '<span class="badge '+esc(s.status)+'">'+esc(s.status)+'</span>'
|
|
19542
|
-
+ '<span class="pk-meta">'+esc(s.kind||'')+'</span>'
|
|
19543
|
-
+ '</div>';
|
|
19544
|
-
});
|
|
19545
|
-
el.innerHTML=html;
|
|
19546
|
-
Array.prototype.forEach.call(el.querySelectorAll('.pk-item'), function(row){
|
|
19547
|
-
row.onclick=function(){ openSession(row.getAttribute('data-id')); };
|
|
19548
|
-
});
|
|
19549
|
-
}
|
|
19550
|
-
|
|
19551
|
-
function showPicker(){
|
|
19552
|
-
activeSessionId=null;
|
|
19553
|
-
$('pickerScreen').classList.remove('hidden');
|
|
19554
|
-
$('storyScreen').classList.add('hidden');
|
|
19555
|
-
renderPicker();
|
|
19556
|
-
}
|
|
19557
|
-
|
|
19558
|
-
function openSession(id){
|
|
19559
|
-
activeSessionId=id;
|
|
19560
|
-
story={chapters:[], steps:[]};
|
|
19561
|
-
open={};
|
|
19562
|
-
selected=-1;
|
|
19563
|
-
lastSeenOutputAt=null;
|
|
19564
|
-
$('pickerScreen').classList.add('hidden');
|
|
19565
|
-
$('storyScreen').classList.remove('hidden');
|
|
19566
|
-
$('fullPanelLink').href='https://cli.agentproto.sh/panel?session='+encodeURIComponent(id);
|
|
19567
|
-
closePanel();
|
|
19568
|
-
loadStory().then(renderAll);
|
|
19569
|
-
}
|
|
19570
|
-
|
|
19571
|
-
// ============================================================
|
|
19572
|
-
// Story loading
|
|
19573
|
-
// ============================================================
|
|
19574
|
-
function activeSession(){ return sessions.filter(function(s){ return s.id===activeSessionId; })[0]; }
|
|
19575
|
-
|
|
19576
|
-
function loadStory(){
|
|
19577
|
-
return callTool('agent_export', {sessionId:activeSessionId, format:'json'}).then(function(data){
|
|
19578
|
-
var messages=(data && data.messages) || [];
|
|
19579
|
-
story=buildStoryJs(messages);
|
|
19580
|
-
// Default open state: only the last (current) chapter is expanded.
|
|
19581
|
-
var last=story.chapters[story.chapters.length-1];
|
|
19582
|
-
if(last && !(last.id in open)) open[last.id]=true;
|
|
19583
|
-
}).catch(function(e){
|
|
19584
|
-
setStatus('Erreur export : '+e.message);
|
|
19585
|
-
});
|
|
19586
|
-
}
|
|
19587
|
-
|
|
19588
|
-
function canSend(){
|
|
19589
|
-
var s=activeSession();
|
|
19590
|
-
return !!s && s.kind==='agent-cli' && s.status==='running';
|
|
19591
|
-
}
|
|
19592
|
-
|
|
19593
|
-
function renderComposer(){
|
|
19594
|
-
var s=activeSession();
|
|
19595
|
-
var box=$('msgBox'), btn=$('sendBtn');
|
|
19596
|
-
var enabled=canSend();
|
|
19597
|
-
box.disabled=!enabled;
|
|
19598
|
-
btn.disabled=!enabled;
|
|
19599
|
-
if(!s){ box.placeholder='Session introuvable.'; }
|
|
19600
|
-
else if(!enabled) box.placeholder='Lecture seule \u2014 session '+esc(s.status)+'.';
|
|
19601
|
-
else box.placeholder="\xC9cris \xE0 l'agent\u2026 (la surcouche classe ton message : suite de la sous-t\xE2che ou nouvelle sous-t\xE2che)";
|
|
19602
|
-
}
|
|
19603
|
-
|
|
19604
|
-
function renderHero(){
|
|
19605
|
-
var s=activeSession();
|
|
19606
|
-
$('heroTitle').textContent=s? titleOf(s) : (activeSessionId||'');
|
|
19607
|
-
var firstUser=story.steps.filter(function(st){ return st.kind==='user'; })[0];
|
|
19608
|
-
var mission=firstUser? firstUser.userText || (firstUser.items[0] && firstUser.items[0].text) : null;
|
|
19609
|
-
$('heroSub').textContent=mission? truncateStr(mission,200) : 'Aucun message pour le moment.';
|
|
19610
|
-
var p=$('pulse');
|
|
19611
|
-
p.classList.toggle('off', !(s && (s.status==='running' || s.status==='starting')));
|
|
19612
|
-
renderComposer();
|
|
19613
|
-
}
|
|
19614
|
-
|
|
19615
|
-
function renderAll(){
|
|
19616
|
-
renderHero();
|
|
19617
|
-
renderPlan();
|
|
19618
|
-
renderRows('bottom');
|
|
19619
|
-
}
|
|
19620
|
-
|
|
19621
|
-
// ============================================================
|
|
19622
|
-
// big picture strip
|
|
19623
|
-
// ============================================================
|
|
19624
|
-
function renderPlan(){
|
|
19625
|
-
var done=story.chapters.filter(function(c){ return c.status==='done'; }).length;
|
|
19626
|
-
$('plan').innerHTML=story.chapters.map(function(c,i){
|
|
19627
|
-
return '<span class="pt '+c.status+'" data-c="'+esc(c.id)+'"><span class="st">'+(c.status==='done'?'\u2713':'\u25CF')+'</span>'+(i+1)+'. '+esc(c.title)+'</span>';
|
|
19628
|
-
}).join('') + '<span class="pt" style="cursor:default"><b>'+done+'/'+story.chapters.length+'</b> faites</span>';
|
|
19629
|
-
Array.prototype.forEach.call($('plan').querySelectorAll('.pt[data-c]'), function(el){
|
|
19630
|
-
el.onclick=function(){ open[el.getAttribute('data-c')]=true; renderRows(); jumpToChap(el.getAttribute('data-c')); };
|
|
19631
|
-
});
|
|
19632
|
-
}
|
|
19633
|
-
function jumpToChap(cid){
|
|
19634
|
-
var el=document.querySelector('.chap[data-c="'+cid+'"]');
|
|
19635
|
-
if(el) el.scrollIntoView({block:'start',behavior:'smooth'});
|
|
19636
|
-
}
|
|
19637
|
-
|
|
19638
|
-
// ============================================================
|
|
19639
|
-
// feed segment\xE9 par chapitres
|
|
19640
|
-
// ============================================================
|
|
19641
|
-
function rowHtml(s,i,isNew){
|
|
19642
|
-
var spec=icoSpec(s.kind), cls=spec[0], ch=spec[1];
|
|
19643
|
-
var route=s.route? '<span class="route '+(s.route==='newt'?'newt':'cont')+'">'+(s.route==='newt'?'\u2605 nouvelle sous-t\xE2che':'\u21B3 suite')+'</span>' : '';
|
|
19644
|
-
return '<div class="row '+(isNew?'new':'')+'" aria-selected="'+(i===selected)+'" data-i="'+i+'">'
|
|
19645
|
-
+ '<span class="ico '+cls+'">'+ch+'</span>'
|
|
19646
|
-
+ '<span class="mid"><span class="sum">'+esc(s.sum)+'</span><span class="raw1">'+esc(s.raw1||'')+'</span>'+route+'</span>'
|
|
19647
|
-
+ (s.count>1? '<span class="cnt">\xD7'+s.count+'</span>':'') + '<span class="ts">'+esc(s.ts||'')+'</span>'
|
|
19648
|
-
+ '</div>';
|
|
19649
|
-
}
|
|
19650
|
-
function renderRows(keepScroll,newIdx){
|
|
19651
|
-
var feed=$('feed');
|
|
19652
|
-
var prevH=feed.scrollHeight, prevTop=feed.scrollTop;
|
|
19653
|
-
var html='';
|
|
19654
|
-
story.chapters.forEach(function(c,ci){
|
|
19655
|
-
var chapSteps=[];
|
|
19656
|
-
story.steps.forEach(function(s,i){ if(s.chap===c.id) chapSteps.push({s:s,i:i}); });
|
|
19657
|
-
if(!chapSteps.length) return;
|
|
19658
|
-
var isOpen=!!open[c.id];
|
|
19659
|
-
html+='<div class="chap '+c.status+' '+(isOpen?'open':'')+'" data-c="'+esc(c.id)+'">'
|
|
19660
|
-
+ '<span class="cst">'+(c.status==='done'?'\u2713':'\u25CF')+'</span><span class="cnum">'+(ci+1)+'.</span>'
|
|
19661
|
-
+ '<span class="csum">'+esc(c.title)+'</span>'
|
|
19662
|
-
+ '<span class="cmeta">'+chapSteps.length+' \xE9tape'+(chapSteps.length>1?'s':'')+'</span><span class="cchev">\u25B8</span>'
|
|
19663
|
-
+ '</div>';
|
|
19664
|
-
if(isOpen) html += chapSteps.map(function(x){ return rowHtml(x.s,x.i,x.i===newIdx); }).join('');
|
|
19665
|
-
});
|
|
19666
|
-
$('rows').innerHTML=html;
|
|
19667
|
-
Array.prototype.forEach.call(document.querySelectorAll('.row'), function(el){
|
|
19668
|
-
el.onclick=function(){ selectStep(Number(el.getAttribute('data-i'))); };
|
|
19669
|
-
});
|
|
19670
|
-
Array.prototype.forEach.call(document.querySelectorAll('.chap'), function(el){
|
|
19671
|
-
el.onclick=function(){ var c=el.getAttribute('data-c'); open[c]=!open[c]; renderRows(); };
|
|
19672
|
-
});
|
|
19673
|
-
if(keepScroll==='bottom') feed.scrollTop=feed.scrollHeight;
|
|
19674
|
-
else if(keepScroll==='preserve') feed.scrollTop=feed.scrollHeight-prevH+prevTop;
|
|
19675
|
-
}
|
|
19676
|
-
|
|
19677
|
-
// ============================================================
|
|
19678
|
-
// panneau ancr\xE9
|
|
19679
|
-
// ============================================================
|
|
19680
|
-
function selectStep(i){
|
|
19681
|
-
selected=i;
|
|
19682
|
-
var s=story.steps[i]; if(!s) return;
|
|
19683
|
-
open[s.chap]=true;
|
|
19684
|
-
$('panel').classList.add('open');
|
|
19685
|
-
var spec=icoSpec(s.kind), cls=spec[0], ch=spec[1];
|
|
19686
|
-
var ico=$('pIco'); ico.className='ico '+cls; ico.textContent=ch;
|
|
19687
|
-
$('pTitle').textContent=s.sum;
|
|
19688
|
-
var c=chapOf(s.chap);
|
|
19689
|
-
$('pSub').textContent=(s.ts? s.ts+' \xB7 ':'')+(c? ('sous-t\xE2che : '+c.title) : '');
|
|
19690
|
-
var facts=(s.facts||[]).map(function(f){
|
|
19691
|
-
return '<span class="fact '+(/\u2713|exit 0|passed|0 match/.test(f)?'ok':'')+'">'+esc(f)+'</span>';
|
|
19692
|
-
}).join('');
|
|
19693
|
-
var tech=(s.items||[]).map(function(it,k){
|
|
19694
|
-
return it.text!==undefined
|
|
19695
|
-
? '<div class="d-text">'+renderMd(it.text)+'</div>'
|
|
19696
|
-
: '<div class="titem"><div class="th">'+esc(it.h)+'<button class="copy" data-k="'+k+'" type="button">\u29C9</button></div><pre>'+esc(it.r)+'</pre></div>';
|
|
19697
|
-
}).join('');
|
|
19698
|
-
$('pBody').innerHTML=''
|
|
19699
|
-
+ '<div class="plain"><div>'+esc(s.sum)+'.</div><div class="why">'+esc(s.why||'')+'</div></div>'
|
|
19700
|
-
+ (facts? '<div class="facts">'+facts+'</div>':'')
|
|
19701
|
-
+ '<details class="techbox" '+(document.body.classList.contains('tech')?'open':'')+'>'
|
|
19702
|
-
+ '<summary>D\xE9tail technique \xB7 '+(s.items||[]).length+'</summary><div class="techlist">'+tech+'</div>'
|
|
19703
|
-
+ '</details>';
|
|
19704
|
-
Array.prototype.forEach.call($('pBody').querySelectorAll('.copy'), function(btn){
|
|
19705
|
-
btn.onclick=function(e){
|
|
19706
|
-
e.stopPropagation();
|
|
19707
|
-
var it=(s.items||[])[Number(btn.getAttribute('data-k'))];
|
|
19708
|
-
var payload=(it.h||'')+'\\n'+(it.r||it.text||'');
|
|
19709
|
-
if(navigator.clipboard) navigator.clipboard.writeText(payload).catch(function(){});
|
|
19710
|
-
btn.textContent='\u2713'; setTimeout(function(){ btn.textContent='\u29C9'; },900);
|
|
19711
|
-
};
|
|
19712
|
-
});
|
|
19713
|
-
$('pPrev').disabled=i<=0; $('pNext').disabled=i>=story.steps.length-1;
|
|
19714
|
-
renderRows();
|
|
19715
|
-
var el=document.querySelector('.row[data-i="'+i+'"]');
|
|
19716
|
-
if(el) el.scrollIntoView({block:'nearest',behavior:'smooth'});
|
|
19717
|
-
}
|
|
19718
|
-
function closePanel(){ selected=-1; var p=$('panel'); if(p) p.classList.remove('open'); renderRows(); }
|
|
19719
|
-
$('pClose').addEventListener('click',closePanel);
|
|
19720
|
-
$('pPrev').addEventListener('click',function(){ if(selected>0) selectStep(selected-1); });
|
|
19721
|
-
$('pNext').addEventListener('click',function(){ if(selected<story.steps.length-1) selectStep(selected+1); });
|
|
19722
|
-
document.addEventListener('keydown',function(e){
|
|
19723
|
-
if(e.key==='Escape'){ closePanel(); return; }
|
|
19724
|
-
if($('storyScreen').classList.contains('hidden')) return;
|
|
19725
|
-
if(selected<0 || e.target.tagName==='TEXTAREA') return;
|
|
19726
|
-
if(e.key==='ArrowUp'){ e.preventDefault(); if(selected>0) selectStep(selected-1); }
|
|
19727
|
-
if(e.key==='ArrowDown'){ e.preventDefault(); if(selected<story.steps.length-1) selectStep(selected+1); }
|
|
19728
|
-
});
|
|
19729
|
-
|
|
19730
|
-
// ============================================================
|
|
19731
|
-
// Simple / Tech modes
|
|
19732
|
-
// ============================================================
|
|
19733
|
-
function setMode(tech){
|
|
19734
|
-
document.body.classList.toggle('tech',tech);
|
|
19735
|
-
$('modeTech').classList.toggle('on',tech);
|
|
19736
|
-
$('modeSimple').classList.toggle('on',!tech);
|
|
19737
|
-
if(selected>=0) selectStep(selected);
|
|
19738
|
-
}
|
|
19739
|
-
$('modeSimple').addEventListener('click',function(){ setMode(false); });
|
|
19740
|
-
$('modeTech').addEventListener('click',function(){ setMode(true); });
|
|
19741
|
-
$('switchBtn').addEventListener('click', function(){ if(pollTimer) clearTimeout(pollTimer); showPicker(); doPoll(); });
|
|
19742
|
-
|
|
19743
|
-
// ============================================================
|
|
19744
|
-
// composer \u2014 local chapter-routing classification + agent_prompt
|
|
19745
|
-
// ============================================================
|
|
19746
|
-
$('sendBtn').addEventListener('click', sendMsg);
|
|
19747
|
-
$('msgBox').addEventListener('keydown', function(e){
|
|
19748
|
-
if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); sendMsg(); }
|
|
19749
|
-
});
|
|
19750
|
-
function sendMsg(){
|
|
19751
|
-
if(!canSend()) return;
|
|
19752
|
-
var box=$('msgBox'), text=box.value.trim();
|
|
19753
|
-
if(!text) return;
|
|
19754
|
-
box.value='';
|
|
19755
|
-
var cur=curChap();
|
|
19756
|
-
var r=cur? classifyRoute(text) : {route:'cont'};
|
|
19757
|
-
var chapId=cur? cur.id : null;
|
|
19758
|
-
if(r.route==='newt' && cur){
|
|
19759
|
-
cur.status='done';
|
|
19760
|
-
var id='c'+(story.chapters.length+1);
|
|
19761
|
-
story.chapters.push({id:id, title:r.title||'Nouvelle sous-t\xE2che', status:'cur'});
|
|
19762
|
-
chapId=id; open[id]=true;
|
|
19763
|
-
$('routing').innerHTML='\u2726 class\xE9 : <span class="r-newt">\u2605 nouvelle sous-t\xE2che \xAB '+esc(r.title||'')+' \xBB</span>';
|
|
19764
|
-
} else {
|
|
19765
|
-
$('routing').innerHTML='\u2726 class\xE9 : <span class="r-cont">\u21B3 suite de \xAB '+esc(cur? cur.title : '')+' \xBB</span>';
|
|
19766
|
-
}
|
|
19767
|
-
setTimeout(function(){ $('routing').textContent=''; },5000);
|
|
19768
|
-
story.steps.push({
|
|
19769
|
-
chap:chapId, kind:'user', ts:nowTs(),
|
|
19770
|
-
sum:'\xAB '+text.slice(0,80)+(text.length>80?'\u2026':'')+' \xBB',
|
|
19771
|
-
raw1:'user \xB7 '+text.split('\\n').length+' ligne(s)',
|
|
19772
|
-
route:r.route, facts:[], items:[{text:text}], userText:text,
|
|
19773
|
-
});
|
|
19774
|
-
renderPlan(); renderRows('bottom', story.steps.length-1);
|
|
19775
|
-
callTool('agent_prompt', {sessionId:activeSessionId, prompt:text}).catch(function(e){
|
|
19776
|
-
setStatus('Envoi \xE9chou\xE9 : '+e.message);
|
|
19777
|
-
});
|
|
19778
|
-
}
|
|
19779
|
-
|
|
19780
|
-
// ============================================================
|
|
19781
|
-
// Poll \u2014 session_list every ~5s; re-fetch agent_export only on turn
|
|
19782
|
-
// boundaries (lastOutputAt changed for the active session).
|
|
19783
|
-
// ============================================================
|
|
19784
|
-
var POLL_MS=5000;
|
|
19785
|
-
function loadSessions(){
|
|
19786
|
-
return callTool('session_list', {kind:'all'}).then(function(data){
|
|
19787
|
-
sessions=data.sessions||[];
|
|
19788
|
-
if($('pickerScreen') && !$('pickerScreen').classList.contains('hidden')) renderPicker();
|
|
19789
|
-
}).catch(function(e){ setStatus('Erreur : '+e.message); });
|
|
19790
|
-
}
|
|
19791
|
-
function doPoll(){
|
|
19792
|
-
if(polling) return;
|
|
19793
|
-
polling=true;
|
|
19794
|
-
loadSessions().then(function(){
|
|
19795
|
-
if(!activeSessionId){ polling=false; pollTimer=setTimeout(doPoll,POLL_MS); return; }
|
|
19796
|
-
var s=activeSession();
|
|
19797
|
-
if(!s){ polling=false; pollTimer=setTimeout(doPoll,POLL_MS); return; }
|
|
19798
|
-
renderHero();
|
|
19799
|
-
var changed=s.lastOutputAt && s.lastOutputAt!==lastSeenOutputAt;
|
|
19800
|
-
if(changed){
|
|
19801
|
-
lastSeenOutputAt=s.lastOutputAt;
|
|
19802
|
-
loadStory().then(function(){ renderPlan(); renderRows('bottom'); polling=false; pollTimer=setTimeout(doPoll,POLL_MS); });
|
|
19803
|
-
} else {
|
|
19804
|
-
polling=false; pollTimer=setTimeout(doPoll,POLL_MS);
|
|
19805
|
-
}
|
|
19806
|
-
}).catch(function(){ polling=false; pollTimer=setTimeout(doPoll,POLL_MS); });
|
|
19807
|
-
}
|
|
19808
|
-
|
|
19809
|
-
// ============================================================
|
|
19810
|
-
// Boot
|
|
19811
|
-
// ============================================================
|
|
19812
|
-
initBridge().then(loadSessions).then(function(){
|
|
19813
|
-
setTimeout(function(){
|
|
19814
|
-
var target=pendingSessionId && sessions.some(function(s){ return s.id===pendingSessionId; })
|
|
19815
|
-
? pendingSessionId
|
|
19816
|
-
: null;
|
|
19817
|
-
if(!target){
|
|
19818
|
-
var agentSessions=sessions.filter(function(s){ return s.kind==='agent-cli'; });
|
|
19819
|
-
if(agentSessions.length===1) target=agentSessions[0].id;
|
|
19820
|
-
}
|
|
19821
|
-
if(target) openSession(target); else showPicker();
|
|
19822
|
-
pollTimer=setTimeout(doPoll,POLL_MS);
|
|
19823
|
-
}, 50);
|
|
19824
|
-
}).catch(function(e){
|
|
19825
|
-
setStatus('Bridge : '+e.message);
|
|
19826
|
-
$('pickerList').innerHTML='<div class="pk-empty">\xC9chec connexion bridge : '+esc(e.message)+'</div>';
|
|
19827
|
-
});
|
|
19828
|
-
</script>
|
|
19829
|
-
</body>
|
|
19830
|
-
</html>`;
|
|
19831
|
-
|
|
19832
|
-
// src/session-story-panel-app.ts
|
|
19833
|
-
var sessionStoryInputSchema = z.object({
|
|
19834
|
-
sessionId: z.string().optional().describe(
|
|
19835
|
-
"Session id to open directly. When omitted, the panel shows a session picker (built from session_list) and lets the user choose."
|
|
19836
|
-
)
|
|
19837
|
-
});
|
|
19838
|
-
function makeSessionStoryPanelApp(ops) {
|
|
19839
|
-
return {
|
|
19840
|
-
id: "agentproto_session_story",
|
|
19841
|
-
title: "Session Story",
|
|
19842
|
-
description: "Open the session story panel \u2014 a readable, per-session timeline for two audiences at once: a plain-language summary of every step for beginners, expandable to raw tool-call detail for technical users. Shows a one-sentence mission, a plan strip of inferred sub-task chapters, a chapter-segmented feed, and a composer to keep driving the session. Polls live data and lets you jump to any step.",
|
|
19843
|
-
inputSchema: sessionStoryInputSchema,
|
|
19844
|
-
execute: async (input) => input.sessionId ? { sessionId: input.sessionId } : { sessions: ops.listSessions("all") },
|
|
19845
|
-
html: SESSION_STORY_PANEL_HTML
|
|
19846
|
-
};
|
|
19847
|
-
}
|
|
19848
18283
|
var terminalPanelInputSchema = z.object({
|
|
19849
18284
|
sessionId: z.string().optional().describe(
|
|
19850
18285
|
"Attach to an existing PTY session (id or name from terminal_start). Omit to spawn a new one from `argv`."
|
|
@@ -20177,819 +18612,6 @@ initBridge().then(function() {
|
|
|
20177
18612
|
</body>
|
|
20178
18613
|
</html>`;
|
|
20179
18614
|
}
|
|
20180
|
-
var liveSessionInputSchema = z.object({
|
|
20181
|
-
sessionId: z.string().optional().describe(
|
|
20182
|
-
"Attach the widget to an existing session by id or name. Omit to self-discover the newest running session from the tree."
|
|
20183
|
-
)
|
|
20184
|
-
});
|
|
20185
|
-
function makeLiveSessionApp(ops) {
|
|
20186
|
-
const httpBaseUrl = ops?.httpBaseUrl ?? "http://127.0.0.1:18790";
|
|
20187
|
-
const httpOrigin = new URL(httpBaseUrl).origin;
|
|
20188
|
-
return {
|
|
20189
|
-
id: "live_session",
|
|
20190
|
-
title: "Live Session",
|
|
20191
|
-
description: "Open the live session widget \u2014 a two-pane view of a running agent session: a live tree on the left, a streaming timeline (text, tool calls/results, turn-end) on the right. Omit `sessionId` to attach to the newest running session; pass one to attach directly.",
|
|
20192
|
-
inputSchema: liveSessionInputSchema,
|
|
20193
|
-
execute: async (input) => ({
|
|
20194
|
-
sessionId: input.sessionId,
|
|
20195
|
-
httpBaseUrl
|
|
20196
|
-
}),
|
|
20197
|
-
html: (initData) => LIVE_SESSION_HTML(initData),
|
|
20198
|
-
csp: { connectDomains: [httpOrigin] }
|
|
20199
|
-
};
|
|
20200
|
-
}
|
|
20201
|
-
function LIVE_SESSION_HTML(initData) {
|
|
20202
|
-
return `<!DOCTYPE html>
|
|
20203
|
-
<html lang="en">
|
|
20204
|
-
<head>
|
|
20205
|
-
<meta charset="UTF-8">
|
|
20206
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
20207
|
-
<title>agentproto live session</title>
|
|
20208
|
-
<style>
|
|
20209
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
20210
|
-
:root{
|
|
20211
|
-
--bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
|
|
20212
|
-
--text:#e6edf3;--text2:#8b949e;--text3:#6e7681;
|
|
20213
|
-
--green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
|
|
20214
|
-
}
|
|
20215
|
-
html,body{height:100%;font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;background:var(--bg);color:var(--text);overflow:hidden}
|
|
20216
|
-
#app{display:flex;height:100%}
|
|
20217
|
-
#tree-pane{width:280px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border);background:var(--bg2)}
|
|
20218
|
-
#tree-head{padding:10px 12px;border-bottom:1px solid var(--border);font-weight:600;font-size:12px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}
|
|
20219
|
-
#tree-body{flex:1;overflow-y:auto;padding:6px}
|
|
20220
|
-
#timeline-pane{flex:1;display:flex;flex-direction:column;min-width:0;position:relative}
|
|
20221
|
-
#timeline-head{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;flex-shrink:0}
|
|
20222
|
-
#timeline-head .focus-id{font-weight:600;font-size:13px;font-family:Menlo,Monaco,monospace}
|
|
20223
|
-
#head-summary{margin-left:auto;font-size:11px;color:var(--text2);display:flex;align-items:center;gap:4px;white-space:nowrap;overflow:hidden;min-width:0}
|
|
20224
|
-
#head-summary .sdot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:3px;vertical-align:1px}
|
|
20225
|
-
#head-summary .sdot.running{background:var(--green)}
|
|
20226
|
-
#head-summary .sdot.grey{background:var(--text3)}
|
|
20227
|
-
#head-summary .sdot.error{background:var(--red)}
|
|
20228
|
-
#usage-chip{font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:1px 6px;color:var(--text)}
|
|
20229
|
-
#status-line{margin-left:8px;font-size:11px;color:var(--text2);flex-shrink:0}
|
|
20230
|
-
#timeline-body{flex:1;overflow-y:auto;padding:10px 14px;display:flex;flex-direction:column;gap:8px}
|
|
20231
|
-
#head-selector{display:none;max-width:42%;font:inherit;font-size:12px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:5px;padding:2px 4px}
|
|
20232
|
-
#new-pill{display:none;position:absolute;right:14px;bottom:12px;z-index:6;background:var(--blue);color:#0d1117;border:none;font-size:11px;font-weight:700;padding:5px 11px;border-radius:12px;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.4)}
|
|
20233
|
-
#new-pill.show{display:block}
|
|
20234
|
-
|
|
20235
|
-
/* WP3 compact mode \u2014 colour rail + tighter rows; only under body.compact-mode
|
|
20236
|
-
(displayMode 'inline' or narrow viewport). fullscreen/pip keep the cards. */
|
|
20237
|
-
body.compact-mode #tree-pane{display:none}
|
|
20238
|
-
body.compact-mode #head-selector{display:inline-block}
|
|
20239
|
-
body.compact-mode #focus-id-label{display:none}
|
|
20240
|
-
body.compact-mode .row{border-radius:6px;padding:5px 8px;border-left-width:3px}
|
|
20241
|
-
body.compact-mode .row.text{border-left-color:var(--blue)}
|
|
20242
|
-
body.compact-mode .row.tool-call{border-left-color:var(--yellow)}
|
|
20243
|
-
body.compact-mode .row.turn-end{border-left-color:var(--purple)}
|
|
20244
|
-
body.compact-mode .row .body{font-size:12px;margin-top:3px}
|
|
20245
|
-
body.compact-mode .row .rhead{font-size:10px}
|
|
20246
|
-
details.tool-group{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:5px 8px;border-left:3px solid var(--yellow)}
|
|
20247
|
-
details.tool-group>summary{cursor:pointer;font-size:11px;color:var(--text2);font-weight:600;list-style:none}
|
|
20248
|
-
details.tool-group[open]>summary{margin-bottom:6px}
|
|
20249
|
-
details.tool-group .row{margin-top:5px}
|
|
20250
|
-
|
|
20251
|
-
.tnode{border-radius:6px;cursor:pointer;padding:5px 8px;margin:1px 0;display:flex;align-items:center;gap:7px;font-size:12px}
|
|
20252
|
-
.tnode:hover{background:var(--bg3)}
|
|
20253
|
-
.tnode.focus{background:var(--bg3);outline:1px solid var(--blue)}
|
|
20254
|
-
.tnode .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
20255
|
-
.dot.running{background:var(--green)}
|
|
20256
|
-
.dot.grey{background:var(--text3)}
|
|
20257
|
-
.dot.error{background:var(--red)}
|
|
20258
|
-
.tnode .label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:Menlo,Monaco,monospace}
|
|
20259
|
-
.tnode .badge{font-size:9px;font-weight:700;color:var(--purple);border:1px solid var(--purple);border-radius:3px;padding:0 4px;flex-shrink:0}
|
|
20260
|
-
.tchildren{margin-left:14px;border-left:1px solid var(--border);padding-left:4px}
|
|
20261
|
-
#tree-empty{padding:12px;color:var(--text3);font-size:12px}
|
|
20262
|
-
|
|
20263
|
-
.row{border:1px solid var(--border);border-radius:8px;background:var(--bg2);padding:8px 10px}
|
|
20264
|
-
.row .rhead{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text2)}
|
|
20265
|
-
.row .kind{font-weight:700;text-transform:uppercase;letter-spacing:.03em;font-size:10px}
|
|
20266
|
-
.row.text .kind{color:var(--blue)}
|
|
20267
|
-
.row.tool-call .kind{color:var(--yellow)}
|
|
20268
|
-
.row.turn-end .kind{color:var(--purple)}
|
|
20269
|
-
.row .body{margin-top:5px;font-size:12.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word}
|
|
20270
|
-
.row.text .body{font-family:inherit}
|
|
20271
|
-
.row.tool-call .toolname{font-family:Menlo,Monaco,monospace;font-weight:600}
|
|
20272
|
-
.status-badge{font-size:9px;font-weight:700;border-radius:3px;padding:1px 5px;margin-left:6px}
|
|
20273
|
-
.status-badge.pending{background:var(--bg3);color:var(--text2)}
|
|
20274
|
-
.status-badge.ok{background:#0f3d20;color:var(--green)}
|
|
20275
|
-
.status-badge.error{background:#3d1418;color:var(--red)}
|
|
20276
|
-
.row details{margin-top:6px}
|
|
20277
|
-
.row summary{cursor:pointer;font-size:11px;color:var(--text2)}
|
|
20278
|
-
.row pre{margin-top:4px;font-family:Menlo,Monaco,monospace;font-size:11px;white-space:pre-wrap;word-break:break-word;color:var(--text2);background:var(--bg);border-radius:5px;padding:6px 8px;max-height:200px;overflow:auto}
|
|
20279
|
-
.chip{display:inline-block;font-size:10.5px;font-family:Menlo,Monaco,monospace;background:var(--bg3);border-radius:4px;padding:2px 6px}
|
|
20280
|
-
#timeline-empty{color:var(--text3);font-size:12px;padding:8px 2px}
|
|
20281
|
-
</style>
|
|
20282
|
-
</head>
|
|
20283
|
-
<body>
|
|
20284
|
-
<div id="app">
|
|
20285
|
-
<div id="tree-pane">
|
|
20286
|
-
<div id="tree-head">Sessions</div>
|
|
20287
|
-
<div id="tree-body"><div id="tree-empty">Loading\u2026</div></div>
|
|
20288
|
-
</div>
|
|
20289
|
-
<div id="timeline-pane">
|
|
20290
|
-
<div id="timeline-head">
|
|
20291
|
-
<span class="focus-id" id="focus-id-label">\u2014</span>
|
|
20292
|
-
<select id="head-selector" title="Sessions"></select>
|
|
20293
|
-
<span id="head-summary"></span>
|
|
20294
|
-
<span id="status-line">connecting\u2026</span>
|
|
20295
|
-
</div>
|
|
20296
|
-
<div id="timeline-body"><div id="timeline-empty">No session focused yet.</div></div>
|
|
20297
|
-
<button id="new-pill" type="button">\u2193 New messages</button>
|
|
20298
|
-
</div>
|
|
20299
|
-
</div>
|
|
20300
|
-
<script>
|
|
20301
|
-
window.__APP_INIT__ = ${JSON.stringify(initData)};
|
|
20302
|
-
|
|
20303
|
-
${panelBridgeScript("agentproto-live-session")}
|
|
20304
|
-
|
|
20305
|
-
// ============================================================
|
|
20306
|
-
// INLINED REDUCER COPY \u2014 hand-kept mirror of live-session-app.logic.ts.
|
|
20307
|
-
// Plain JS, same semantics: coalesce consecutive text-delta of the same
|
|
20308
|
-
// session (and rejoin an unterminated mid-line fragment split by an
|
|
20309
|
-
// interleaved record \u2014 see the TS module's text-delta arm), pair
|
|
20310
|
-
// tool-call/tool-result by toolCallId, pass through turn-end, keep usage
|
|
20311
|
-
// as STATE (SPEC \xA71: usage leaves the timeline \u2014 a usage_update record
|
|
20312
|
-
// never produces a row), ignore unknown kinds. Keep in sync with the TS
|
|
20313
|
-
// module; the TS module is the one the test suite imports.
|
|
20314
|
-
// ============================================================
|
|
20315
|
-
|
|
20316
|
-
function initialTimelineState() {
|
|
20317
|
-
return { rows: [], usage: null };
|
|
20318
|
-
}
|
|
20319
|
-
|
|
20320
|
-
function rowId(record, rows) {
|
|
20321
|
-
return record.seq != null ? (record.kind + '-' + record.seq) : (record.kind + '-' + rows.length);
|
|
20322
|
-
}
|
|
20323
|
-
|
|
20324
|
-
// Fold a text-delta record into an existing row (fresh object), keeping the
|
|
20325
|
-
// row's "partial" hint in step with the latest record \u2014 see mergeTextDelta in
|
|
20326
|
-
// the TS module.
|
|
20327
|
-
function mergeTextDelta(row, record) {
|
|
20328
|
-
var merged = Object.assign({}, row, {
|
|
20329
|
-
text: row.text + (record.text || ''),
|
|
20330
|
-
seq: record.seq,
|
|
20331
|
-
ts: record.ts,
|
|
20332
|
-
});
|
|
20333
|
-
if (record.partial === true) merged.partial = true;
|
|
20334
|
-
else delete merged.partial;
|
|
20335
|
-
return merged;
|
|
20336
|
-
}
|
|
20337
|
-
|
|
20338
|
-
function reduceEvent(state, record) {
|
|
20339
|
-
switch (record.kind) {
|
|
20340
|
-
case 'text-delta': {
|
|
20341
|
-
var last = state.rows[state.rows.length - 1];
|
|
20342
|
-
if (last && last.kind === 'text' && last.sessionId === record.sessionId) {
|
|
20343
|
-
return { rows: state.rows.slice(0, -1).concat([mergeTextDelta(last, record)]), usage: state.usage };
|
|
20344
|
-
}
|
|
20345
|
-
// Debounce can flush an unterminated mid-word fragment (flagged
|
|
20346
|
-
// partial), let a tool-call land, then flush the continuation \u2014 look
|
|
20347
|
-
// back within the same turn (bounded by this session's last turn-end)
|
|
20348
|
-
// for that session's most recent text row and continue it in place.
|
|
20349
|
-
// Only the explicit partial flag glues: a non-partial record with no
|
|
20350
|
-
// trailing newline is the writer's normal end-of-text-block shape.
|
|
20351
|
-
for (var i = state.rows.length - 1; i >= 0; i--) {
|
|
20352
|
-
var prior = state.rows[i];
|
|
20353
|
-
if (prior.sessionId !== record.sessionId) continue;
|
|
20354
|
-
if (prior.kind === 'turn-end') break;
|
|
20355
|
-
if (prior.kind !== 'text') continue;
|
|
20356
|
-
if (prior.partial === true) {
|
|
20357
|
-
var patched = state.rows.slice();
|
|
20358
|
-
patched[i] = mergeTextDelta(prior, record);
|
|
20359
|
-
return { rows: patched, usage: state.usage };
|
|
20360
|
-
}
|
|
20361
|
-
break;
|
|
20362
|
-
}
|
|
20363
|
-
var row = {
|
|
20364
|
-
kind: 'text', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,
|
|
20365
|
-
sessionId: record.sessionId, text: record.text || '',
|
|
20366
|
-
};
|
|
20367
|
-
if (record.partial === true) row.partial = true;
|
|
20368
|
-
return { rows: state.rows.concat([row]), usage: state.usage };
|
|
20369
|
-
}
|
|
20370
|
-
case 'tool-call': {
|
|
20371
|
-
var row = {
|
|
20372
|
-
kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,
|
|
20373
|
-
sessionId: record.sessionId, toolCallId: record.toolCallId || '',
|
|
20374
|
-
toolName: record.toolName || 'unknown', arguments: record.arguments, status: 'pending',
|
|
20375
|
-
};
|
|
20376
|
-
return { rows: state.rows.concat([row]), usage: state.usage };
|
|
20377
|
-
}
|
|
20378
|
-
case 'tool-result': {
|
|
20379
|
-
var idx = -1;
|
|
20380
|
-
for (var i = 0; i < state.rows.length; i++) {
|
|
20381
|
-
if (state.rows[i].kind === 'tool-call' && state.rows[i].toolCallId === record.toolCallId) idx = i;
|
|
20382
|
-
}
|
|
20383
|
-
if (idx === -1) {
|
|
20384
|
-
var row = {
|
|
20385
|
-
kind: 'tool-call', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,
|
|
20386
|
-
sessionId: record.sessionId, toolCallId: record.toolCallId || '', toolName: 'unknown',
|
|
20387
|
-
status: record.isError ? 'error' : 'ok', result: record.result,
|
|
20388
|
-
};
|
|
20389
|
-
return { rows: state.rows.concat([row]), usage: state.usage };
|
|
20390
|
-
}
|
|
20391
|
-
var updated = Object.assign({}, state.rows[idx], {
|
|
20392
|
-
status: record.isError ? 'error' : 'ok', result: record.result,
|
|
20393
|
-
});
|
|
20394
|
-
var rows = state.rows.slice();
|
|
20395
|
-
rows[idx] = updated;
|
|
20396
|
-
return { rows: rows, usage: state.usage };
|
|
20397
|
-
}
|
|
20398
|
-
case 'turn-end': {
|
|
20399
|
-
var row = {
|
|
20400
|
-
kind: 'turn-end', id: rowId(record, state.rows), seq: record.seq, ts: record.ts,
|
|
20401
|
-
sessionId: record.sessionId, reason: record.reason,
|
|
20402
|
-
};
|
|
20403
|
-
return { rows: state.rows.concat([row]), usage: state.usage };
|
|
20404
|
-
}
|
|
20405
|
-
case 'usage_update': {
|
|
20406
|
-
// Usage is state, not a row (SPEC \xA71) \u2014 last-write-wins, no merge with
|
|
20407
|
-
// the prior snapshot. state.rows is reused as-is (it didn't change).
|
|
20408
|
-
return {
|
|
20409
|
-
rows: state.rows,
|
|
20410
|
-
usage: {
|
|
20411
|
-
size: record.size, used: record.used, cost: record.cost,
|
|
20412
|
-
tokensIn: record.tokensIn, tokensOut: record.tokensOut,
|
|
20413
|
-
seq: record.seq, ts: record.ts,
|
|
20414
|
-
},
|
|
20415
|
-
};
|
|
20416
|
-
}
|
|
20417
|
-
default:
|
|
20418
|
-
return state;
|
|
20419
|
-
}
|
|
20420
|
-
}
|
|
20421
|
-
|
|
20422
|
-
// ============================================================
|
|
20423
|
-
// INLINED PURE HELPERS \u2014 exact copies of live-session-app.logic.ts's
|
|
20424
|
-
// isNearBottom (SPEC \xA72) and groupAdjacentToolCalls (SPEC \xA73). Same names,
|
|
20425
|
-
// same signatures, same default thresholds; plain JS because the widget
|
|
20426
|
-
// has no import step.
|
|
20427
|
-
// ============================================================
|
|
20428
|
-
|
|
20429
|
-
var SCROLL_STICK_THRESHOLD_PX = 24;
|
|
20430
|
-
|
|
20431
|
-
function isNearBottom(scrollHeight, scrollTop, clientHeight, threshold) {
|
|
20432
|
-
if (threshold == null) threshold = SCROLL_STICK_THRESHOLD_PX;
|
|
20433
|
-
return scrollHeight - scrollTop - clientHeight <= threshold;
|
|
20434
|
-
}
|
|
20435
|
-
|
|
20436
|
-
var TOOL_CALL_GROUP_THRESHOLD = 2;
|
|
20437
|
-
|
|
20438
|
-
// Collapse runs of \`threshold\`+ adjacent tool-call rows into one
|
|
20439
|
-
// {kind:'tool-group', rows:[...]} entry; everything else passes through as
|
|
20440
|
-
// individual {kind:'row', row} entries, same order as the input.
|
|
20441
|
-
function groupAdjacentToolCalls(rows, threshold) {
|
|
20442
|
-
if (threshold == null) threshold = TOOL_CALL_GROUP_THRESHOLD;
|
|
20443
|
-
var out = [];
|
|
20444
|
-
var run = [];
|
|
20445
|
-
function flushRun() {
|
|
20446
|
-
if (!run.length) return;
|
|
20447
|
-
if (run.length >= threshold) out.push({ kind: 'tool-group', rows: run });
|
|
20448
|
-
else for (var j = 0; j < run.length; j++) out.push({ kind: 'row', row: run[j] });
|
|
20449
|
-
run = [];
|
|
20450
|
-
}
|
|
20451
|
-
for (var i = 0; i < rows.length; i++) {
|
|
20452
|
-
if (rows[i].kind === 'tool-call') { run.push(rows[i]); continue; }
|
|
20453
|
-
flushRun();
|
|
20454
|
-
out.push({ kind: 'row', row: rows[i] });
|
|
20455
|
-
}
|
|
20456
|
-
flushRun();
|
|
20457
|
-
return out;
|
|
20458
|
-
}
|
|
20459
|
-
|
|
20460
|
-
// ============================================================
|
|
20461
|
-
// Rendering helpers
|
|
20462
|
-
// ============================================================
|
|
20463
|
-
|
|
20464
|
-
function escHtml(s) {
|
|
20465
|
-
return String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
20466
|
-
}
|
|
20467
|
-
|
|
20468
|
-
function safeJson(v) {
|
|
20469
|
-
try { return JSON.stringify(v, null, 2); } catch (e) { return String(v); }
|
|
20470
|
-
}
|
|
20471
|
-
|
|
20472
|
-
function statusDotClass(status) {
|
|
20473
|
-
if (status === 'running' || status === 'starting') return 'running';
|
|
20474
|
-
if (status === 'error' || status === 'killed') return 'error';
|
|
20475
|
-
return 'grey';
|
|
20476
|
-
}
|
|
20477
|
-
|
|
20478
|
-
// SPEC \xA74 (non-contract guidance): 52524 \u2192 "52.5k", >=1e6 \u2192 "M" suffix,
|
|
20479
|
-
// one decimal, trailing .0 vanishes because the value stays a number.
|
|
20480
|
-
// cost \u2192 "$" + toFixed(2). Read from timelineState.usage.
|
|
20481
|
-
function fmtCompactNum(n) {
|
|
20482
|
-
if (typeof n !== 'number' || !isFinite(n)) return null;
|
|
20483
|
-
if (n >= 1000000) return (Math.round(n / 100000) / 10) + 'M';
|
|
20484
|
-
if (n >= 1000) return (Math.round(n / 100) / 10) + 'k';
|
|
20485
|
-
return String(n);
|
|
20486
|
-
}
|
|
20487
|
-
|
|
20488
|
-
function usageChipText(usage) {
|
|
20489
|
-
if (!usage) return '';
|
|
20490
|
-
var bits = [];
|
|
20491
|
-
if (usage.used != null && usage.size != null) {
|
|
20492
|
-
bits.push(fmtCompactNum(usage.used) + '/' + fmtCompactNum(usage.size));
|
|
20493
|
-
} else if (usage.used != null) {
|
|
20494
|
-
bits.push(fmtCompactNum(usage.used));
|
|
20495
|
-
}
|
|
20496
|
-
if (typeof usage.cost === 'number') bits.push('$' + usage.cost.toFixed(2));
|
|
20497
|
-
return bits.join(' \xB7 ');
|
|
20498
|
-
}
|
|
20499
|
-
|
|
20500
|
-
// WP5: elapsed seconds since the timeline's first row (running only).
|
|
20501
|
-
function elapsedText(rows, status) {
|
|
20502
|
-
if (status !== 'running' && status !== 'starting') return null;
|
|
20503
|
-
if (!rows.length || rows[0].ts == null) return null;
|
|
20504
|
-
var ms = new Date(rows[0].ts).getTime();
|
|
20505
|
-
if (!isFinite(ms)) return null;
|
|
20506
|
-
return Math.max(0, Math.round((Date.now() - ms) / 1000)) + 's';
|
|
20507
|
-
}
|
|
20508
|
-
|
|
20509
|
-
// ============================================================
|
|
20510
|
-
// LEFT pane \u2014 live tree
|
|
20511
|
-
// ============================================================
|
|
20512
|
-
|
|
20513
|
-
var currentTree = [];
|
|
20514
|
-
var focusId = (window.__APP_INIT__ && window.__APP_INIT__.sessionId) || null;
|
|
20515
|
-
var treeTimer = null;
|
|
20516
|
-
|
|
20517
|
-
function findNode(nodes, id) {
|
|
20518
|
-
for (var i = 0; i < nodes.length; i++) {
|
|
20519
|
-
if (nodes[i].id === id) return nodes[i];
|
|
20520
|
-
var found = findNode(nodes[i].children || [], id);
|
|
20521
|
-
if (found) return found;
|
|
20522
|
-
}
|
|
20523
|
-
return null;
|
|
20524
|
-
}
|
|
20525
|
-
|
|
20526
|
-
function flattenDfs(nodes, out) {
|
|
20527
|
-
out = out || [];
|
|
20528
|
-
for (var i = 0; i < nodes.length; i++) {
|
|
20529
|
-
out.push(nodes[i]);
|
|
20530
|
-
flattenDfs(nodes[i].children || [], out);
|
|
20531
|
-
}
|
|
20532
|
-
return out;
|
|
20533
|
-
}
|
|
20534
|
-
|
|
20535
|
-
function pickInitialFocus(tree) {
|
|
20536
|
-
var flat = flattenDfs(tree);
|
|
20537
|
-
var alive = flat.filter(function(n) { return n.status === 'running' || n.status === 'starting'; });
|
|
20538
|
-
if (alive.length) return alive[alive.length - 1].id;
|
|
20539
|
-
return tree.length ? tree[0].id : null;
|
|
20540
|
-
}
|
|
20541
|
-
|
|
20542
|
-
function renderTreeNode(node, depth) {
|
|
20543
|
-
var childrenHtml = (node.children || []).map(function(c) { return renderTreeNode(c, depth + 1); }).join('');
|
|
20544
|
-
var badge = node.isOrchestrator ? '<span class="badge">orch</span>' : '';
|
|
20545
|
-
var cls = 'tnode' + (node.id === focusId ? ' focus' : '');
|
|
20546
|
-
return '<div class="' + cls + '" data-id="' + escHtml(node.id) + '">' +
|
|
20547
|
-
'<span class="dot ' + statusDotClass(node.status) + '"></span>' +
|
|
20548
|
-
'<span class="label">' + escHtml(node.label || node.id) + '</span>' + badge +
|
|
20549
|
-
'</div>' +
|
|
20550
|
-
(childrenHtml ? '<div class="tchildren">' + childrenHtml + '</div>' : '');
|
|
20551
|
-
}
|
|
20552
|
-
|
|
20553
|
-
// WP4: compact header <select> mirrors the tree; still routes through
|
|
20554
|
-
// setFocus() \u2014 the only session-switch entry point.
|
|
20555
|
-
function renderHeadSelector() {
|
|
20556
|
-
var sel = document.getElementById('head-selector');
|
|
20557
|
-
var flat = flattenDfs(currentTree);
|
|
20558
|
-
while (sel.firstChild) sel.removeChild(sel.firstChild);
|
|
20559
|
-
if (!flat.length) return;
|
|
20560
|
-
for (var i = 0; i < flat.length; i++) {
|
|
20561
|
-
var n = flat[i];
|
|
20562
|
-
var o = document.createElement('option');
|
|
20563
|
-
o.value = n.id;
|
|
20564
|
-
o.textContent = (n.id === focusId ? '\u25CF ' : '') + (n.label || n.id);
|
|
20565
|
-
sel.appendChild(o);
|
|
20566
|
-
}
|
|
20567
|
-
sel.value = focusId || '';
|
|
20568
|
-
}
|
|
20569
|
-
|
|
20570
|
-
function renderTree() {
|
|
20571
|
-
var body = document.getElementById('tree-body');
|
|
20572
|
-
var root = focusId ? findNode(currentTree, focusId) : null;
|
|
20573
|
-
var renderNodes = root ? [root] : currentTree;
|
|
20574
|
-
if (!renderNodes.length) {
|
|
20575
|
-
body.innerHTML = '<div id="tree-empty">No sessions.</div>';
|
|
20576
|
-
renderHeadSelector();
|
|
20577
|
-
return;
|
|
20578
|
-
}
|
|
20579
|
-
body.innerHTML = renderNodes.map(function(n) { return renderTreeNode(n, 0); }).join('');
|
|
20580
|
-
var els = body.querySelectorAll('.tnode');
|
|
20581
|
-
els.forEach(function(el) {
|
|
20582
|
-
el.addEventListener('click', function() {
|
|
20583
|
-
var id = el.getAttribute('data-id');
|
|
20584
|
-
if (id === focusId) return;
|
|
20585
|
-
setFocus(id);
|
|
20586
|
-
});
|
|
20587
|
-
});
|
|
20588
|
-
renderHeadSelector();
|
|
20589
|
-
}
|
|
20590
|
-
|
|
20591
|
-
function pollTree() {
|
|
20592
|
-
callTool('app_session_tree', {}).then(function(res) {
|
|
20593
|
-
currentTree = res.tree || [];
|
|
20594
|
-
if (!focusId) {
|
|
20595
|
-
focusId = pickInitialFocus(currentTree);
|
|
20596
|
-
if (focusId) startTimeline(focusId);
|
|
20597
|
-
}
|
|
20598
|
-
renderTree();
|
|
20599
|
-
updateHeader();
|
|
20600
|
-
}).catch(function() {
|
|
20601
|
-
// Leave the last-known tree rendered; the next poll may recover.
|
|
20602
|
-
});
|
|
20603
|
-
}
|
|
20604
|
-
|
|
20605
|
-
// ============================================================
|
|
20606
|
-
// RIGHT pane \u2014 timeline for focusId
|
|
20607
|
-
// ============================================================
|
|
20608
|
-
|
|
20609
|
-
var timelineState = initialTimelineState();
|
|
20610
|
-
var sincePtr = 0;
|
|
20611
|
-
var activeSource = null; // {type:'sse', es} | {type:'poll', timer}
|
|
20612
|
-
var compactMode = false; // WP3: hostContext.displayMode === 'inline' (or narrow)
|
|
20613
|
-
|
|
20614
|
-
function setStatus(msg) {
|
|
20615
|
-
document.getElementById('status-line').textContent = msg;
|
|
20616
|
-
}
|
|
20617
|
-
|
|
20618
|
-
function isCompact() {
|
|
20619
|
-
return (getHostContext() && getHostContext().displayMode === 'inline') ||
|
|
20620
|
-
(typeof window !== 'undefined' && window.innerWidth < 640);
|
|
20621
|
-
}
|
|
20622
|
-
|
|
20623
|
-
// WP3/WP4: apply the compact/expanded split. Only re-renders the timeline
|
|
20624
|
-
// when the mode actually flips (scroll + <details> state survive otherwise).
|
|
20625
|
-
function applyDisplayMode() {
|
|
20626
|
-
var c = isCompact();
|
|
20627
|
-
document.body.classList.toggle('compact-mode', c);
|
|
20628
|
-
if (c !== compactMode) {
|
|
20629
|
-
compactMode = c;
|
|
20630
|
-
renderTimelineFull();
|
|
20631
|
-
renderTree();
|
|
20632
|
-
}
|
|
20633
|
-
}
|
|
20634
|
-
|
|
20635
|
-
// WP5 + WP2: one header line \u2014 \`\u25CF running \xB7 3 tools \xB7 52.5k/200k \xB7 $0.04 \xB7 12s\`
|
|
20636
|
-
// (status dot from the focus node, tool count from the rows, usage from
|
|
20637
|
-
// timelineState.usage \u2014 no usage_update row anymore) + transport status.
|
|
20638
|
-
function updateHeader() {
|
|
20639
|
-
document.getElementById('focus-id-label').textContent = focusId || '\u2014';
|
|
20640
|
-
var rows = timelineState.rows || [];
|
|
20641
|
-
var usage = timelineState.usage;
|
|
20642
|
-
var tools = 0;
|
|
20643
|
-
for (var i = 0; i < rows.length; i++) if (rows[i].kind === 'tool-call') tools++;
|
|
20644
|
-
var node = focusId ? findNode(currentTree, focusId) : null;
|
|
20645
|
-
var st = node ? node.status : null;
|
|
20646
|
-
var parts = [];
|
|
20647
|
-
if (st) parts.push('<span class="sdot ' + statusDotClass(st) + '"></span>' + escHtml(st));
|
|
20648
|
-
if (tools) parts.push(tools + (tools === 1 ? ' tool' : ' tools'));
|
|
20649
|
-
var chip = usageChipText(usage);
|
|
20650
|
-
if (chip) parts.push('<span id="usage-chip" class="chip">' + escHtml(chip) + '</span>');
|
|
20651
|
-
var el = elapsedText(rows, st);
|
|
20652
|
-
if (el) parts.push(escHtml(el));
|
|
20653
|
-
document.getElementById('head-summary').innerHTML = parts.length ? parts.join(' \xB7 ') : '';
|
|
20654
|
-
}
|
|
20655
|
-
|
|
20656
|
-
function captureDetailsOpenFlags(body) {
|
|
20657
|
-
var dets = body.querySelectorAll('details');
|
|
20658
|
-
var flags = [];
|
|
20659
|
-
for (var i = 0; i < dets.length; i++) flags.push(dets[i].open);
|
|
20660
|
-
return flags;
|
|
20661
|
-
}
|
|
20662
|
-
|
|
20663
|
-
function restoreDetailsOpenFlags(body, flags) {
|
|
20664
|
-
var dets = body.querySelectorAll('details');
|
|
20665
|
-
for (var i = 0; i < dets.length && i < flags.length; i++) {
|
|
20666
|
-
if (flags[i]) dets[i].open = true;
|
|
20667
|
-
}
|
|
20668
|
-
}
|
|
20669
|
-
|
|
20670
|
-
// Full rebuild \u2014 used ONLY for the first paint of a session, a setFocus()
|
|
20671
|
-
// session switch (SPEC \xA72: full reset is correct there), a display-mode
|
|
20672
|
-
// flip, and as the fallback for shapes the incremental patcher can't patch.
|
|
20673
|
-
// A display-mode flip re-renders the SAME content (WP3: "r\xE9actif... sans
|
|
20674
|
-
// perdre le scroll ni l'\xE9tat des <details>"), so open <details> and the
|
|
20675
|
-
// exact scroll offset (not just the at-bottom decision) survive the rebuild.
|
|
20676
|
-
function renderTimelineFull() {
|
|
20677
|
-
var body = document.getElementById('timeline-body');
|
|
20678
|
-
var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);
|
|
20679
|
-
var savedScrollTop = body.scrollTop;
|
|
20680
|
-
var flags = captureDetailsOpenFlags(body);
|
|
20681
|
-
body.innerHTML = buildTimelineHtml();
|
|
20682
|
-
restoreDetailsOpenFlags(body, flags);
|
|
20683
|
-
body.scrollTop = wasAtBottom ? body.scrollHeight : savedScrollTop;
|
|
20684
|
-
updateHeader();
|
|
20685
|
-
}
|
|
20686
|
-
|
|
20687
|
-
function buildTimelineHtml() {
|
|
20688
|
-
var rows = timelineState.rows;
|
|
20689
|
-
if (!rows.length) return '<div id="timeline-empty">No events yet.</div>';
|
|
20690
|
-
if (compactMode) {
|
|
20691
|
-
var entries = groupAdjacentToolCalls(rows);
|
|
20692
|
-
var html = '';
|
|
20693
|
-
for (var i = 0; i < entries.length; i++) {
|
|
20694
|
-
var e = entries[i];
|
|
20695
|
-
if (e.kind === 'tool-group') {
|
|
20696
|
-
html += '<details class="tool-group"><summary>\u25B8 ' + e.rows.length + ' tool calls</summary>' +
|
|
20697
|
-
e.rows.map(renderRow).join('') + '</details>';
|
|
20698
|
-
} else {
|
|
20699
|
-
html += renderRow(e.row);
|
|
20700
|
-
}
|
|
20701
|
-
}
|
|
20702
|
-
return html;
|
|
20703
|
-
}
|
|
20704
|
-
return rows.map(renderRow).join('');
|
|
20705
|
-
}
|
|
20706
|
-
|
|
20707
|
-
// \u2500\u2500 WP1 incremental patch (dense mode) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
20708
|
-
// One record, one minimal DOM mutation. The common case (text-delta merged
|
|
20709
|
-
// into the LAST rendered row) patches just that row's text node. Anything
|
|
20710
|
-
// else appends via insertAdjacentHTML. Only rare in-place replacements
|
|
20711
|
-
// (tool-result merge, partial-lookback re-merge) swap ONE element's HTML.
|
|
20712
|
-
function applyRecordToDom(prevRows) {
|
|
20713
|
-
var body = document.getElementById('timeline-body');
|
|
20714
|
-
// Capture "stuck to bottom?" BEFORE any mutation (SPEC \xA72).
|
|
20715
|
-
var wasAtBottom = isNearBottom(body.scrollHeight, body.scrollTop, body.clientHeight);
|
|
20716
|
-
var rows = timelineState.rows;
|
|
20717
|
-
|
|
20718
|
-
if (compactMode) {
|
|
20719
|
-
// Inline mode: smaller rows \u2014 full grouped rebuild, preserving
|
|
20720
|
-
// wasAtBottom/scrollTop and <details> open-state. Only touch the DOM
|
|
20721
|
-
// (and the scroll/pill decision) when the rows actually changed \u2014 a
|
|
20722
|
-
// usage_update leaves rows === prevRows, so it must fall through to the
|
|
20723
|
-
// header-only path below, same as the dense-mode branch.
|
|
20724
|
-
if (rows !== prevRows) {
|
|
20725
|
-
var flags = captureDetailsOpenFlags(body);
|
|
20726
|
-
body.innerHTML = buildTimelineHtml();
|
|
20727
|
-
restoreDetailsOpenFlags(body, flags);
|
|
20728
|
-
if (wasAtBottom) body.scrollTop = body.scrollHeight;
|
|
20729
|
-
else showNewPill();
|
|
20730
|
-
}
|
|
20731
|
-
updateHeader();
|
|
20732
|
-
return;
|
|
20733
|
-
}
|
|
20734
|
-
|
|
20735
|
-
var appended = rows.length > prevRows.length;
|
|
20736
|
-
if (prevRows.length === 0 || (appended && !samePrefix(prevRows, rows))) {
|
|
20737
|
-
// First real paint of this session (empty placeholder present) or an
|
|
20738
|
-
// unexpected shape \u2014 full reset is correct/cheapest.
|
|
20739
|
-
body.innerHTML = buildTimelineHtml();
|
|
20740
|
-
body.scrollTop = body.scrollHeight;
|
|
20741
|
-
updateHeader();
|
|
20742
|
-
return;
|
|
20743
|
-
}
|
|
20744
|
-
|
|
20745
|
-
if (appended) {
|
|
20746
|
-
body.insertAdjacentHTML('beforeend', renderRow(rows[rows.length - 1]));
|
|
20747
|
-
} else if (rows.length === prevRows.length && rows.length > 0) {
|
|
20748
|
-
// Exactly one row object replaced (tool-result merge, partial-text
|
|
20749
|
-
// re-merge via the reducer's lookback). Patch that one element only.
|
|
20750
|
-
// Scan from the tail: the hot path (a text-delta merged into the LAST
|
|
20751
|
-
// row, the dominant streaming case) is found in O(1) this way instead
|
|
20752
|
-
// of walking the whole array \u2014 the rare mid-array lookback re-merge
|
|
20753
|
-
// still resolves correctly, just slower.
|
|
20754
|
-
var idx = -1;
|
|
20755
|
-
for (var i = rows.length - 1; i >= 0; i--) {
|
|
20756
|
-
if (rows[i] !== prevRows[i]) { idx = i; break; }
|
|
20757
|
-
}
|
|
20758
|
-
var patched = false;
|
|
20759
|
-
if (idx >= 0) {
|
|
20760
|
-
var row = rows[idx];
|
|
20761
|
-
var el = body.querySelector('[data-row-id="' + escHtml(row.id) + '"]');
|
|
20762
|
-
if (el) {
|
|
20763
|
-
if (row.kind === 'text' && idx === rows.length - 1) {
|
|
20764
|
-
// High-frequency case: patch the existing text node in place.
|
|
20765
|
-
var b = el.querySelector('.body');
|
|
20766
|
-
if (b) { b.textContent = row.text; patched = true; }
|
|
20767
|
-
}
|
|
20768
|
-
if (!patched) { el.outerHTML = renderRow(row); patched = true; }
|
|
20769
|
-
}
|
|
20770
|
-
}
|
|
20771
|
-
if (!patched) {
|
|
20772
|
-
// Multiple rows changed at once (shouldn't happen per-record) \u2014 rebuild.
|
|
20773
|
-
body.innerHTML = buildTimelineHtml();
|
|
20774
|
-
if (wasAtBottom) body.scrollTop = body.scrollHeight;
|
|
20775
|
-
updateHeader();
|
|
20776
|
-
return;
|
|
20777
|
-
}
|
|
20778
|
-
} else {
|
|
20779
|
-
// rows unchanged (usage_update) \u2014 header only.
|
|
20780
|
-
updateHeader();
|
|
20781
|
-
return;
|
|
20782
|
-
}
|
|
20783
|
-
|
|
20784
|
-
// Scroll decision AFTER the mutation: stick or leave the position alone.
|
|
20785
|
-
if (wasAtBottom) body.scrollTop = body.scrollHeight;
|
|
20786
|
-
else showNewPill();
|
|
20787
|
-
updateHeader();
|
|
20788
|
-
}
|
|
20789
|
-
|
|
20790
|
-
function samePrefix(prevRows, rows) {
|
|
20791
|
-
for (var i = 0; i < prevRows.length; i++) {
|
|
20792
|
-
if (prevRows[i] !== rows[i]) return false;
|
|
20793
|
-
}
|
|
20794
|
-
return true;
|
|
20795
|
-
}
|
|
20796
|
-
|
|
20797
|
-
function showNewPill() {
|
|
20798
|
-
document.getElementById('new-pill').classList.add('show');
|
|
20799
|
-
}
|
|
20800
|
-
|
|
20801
|
-
function hideNewPill() {
|
|
20802
|
-
document.getElementById('new-pill').classList.remove('show');
|
|
20803
|
-
}
|
|
20804
|
-
|
|
20805
|
-
function renderRow(row) {
|
|
20806
|
-
if (row.kind === 'text') {
|
|
20807
|
-
return '<div class="row text" data-row-id="' + escHtml(row.id) + '"><div class="rhead"><span class="kind">text</span></div>' +
|
|
20808
|
-
'<div class="body">' + escHtml(row.text) + '</div></div>';
|
|
20809
|
-
}
|
|
20810
|
-
if (row.kind === 'tool-call') {
|
|
20811
|
-
var badgeCls = row.status === 'pending' ? 'pending' : row.status;
|
|
20812
|
-
var badgeText = row.status === 'pending' ? 'pending' : (row.status === 'ok' ? 'ok' : 'error');
|
|
20813
|
-
var resultHtml = row.status !== 'pending'
|
|
20814
|
-
? '<details><summary>result</summary><pre>' + escHtml(safeJson(row.result)) + '</pre></details>'
|
|
20815
|
-
: '';
|
|
20816
|
-
return '<div class="row tool-call" data-row-id="' + escHtml(row.id) + '"><div class="rhead"><span class="kind">tool</span>' +
|
|
20817
|
-
'<span class="toolname">' + escHtml(row.toolName) + '</span>' +
|
|
20818
|
-
'<span class="status-badge ' + badgeCls + '">' + badgeText + '</span></div>' +
|
|
20819
|
-
'<details><summary>arguments</summary><pre>' + escHtml(safeJson(row.arguments)) + '</pre></details>' +
|
|
20820
|
-
resultHtml + '</div>';
|
|
20821
|
-
}
|
|
20822
|
-
if (row.kind === 'turn-end') {
|
|
20823
|
-
return '<div class="row turn-end" data-row-id="' + escHtml(row.id) + '"><div class="rhead"><span class="kind">turn end</span>' +
|
|
20824
|
-
'<span class="chip">' + escHtml(row.reason || '\u2014') + '</span></div></div>';
|
|
20825
|
-
}
|
|
20826
|
-
return '';
|
|
20827
|
-
}
|
|
20828
|
-
|
|
20829
|
-
function teardownTimeline() {
|
|
20830
|
-
if (activeSource) {
|
|
20831
|
-
if (activeSource.type === 'sse' && activeSource.es) {
|
|
20832
|
-
try { activeSource.es.close(); } catch (e) {}
|
|
20833
|
-
}
|
|
20834
|
-
// Both the poll re-arm timer AND the SSE open/first-message fallback
|
|
20835
|
-
// timer must be cleared, or a focus switch within the 2.5s window leaves
|
|
20836
|
-
// a stale timer that fires startPolling for the abandoned session.
|
|
20837
|
-
if (activeSource.timer) clearTimeout(activeSource.timer);
|
|
20838
|
-
if (activeSource.fallbackTimer) clearTimeout(activeSource.fallbackTimer);
|
|
20839
|
-
}
|
|
20840
|
-
activeSource = null;
|
|
20841
|
-
}
|
|
20842
|
-
|
|
20843
|
-
function attemptSSE(id) {
|
|
20844
|
-
var settled = false;
|
|
20845
|
-
var es;
|
|
20846
|
-
try {
|
|
20847
|
-
es = new EventSource(window.__APP_INIT__.httpBaseUrl + '/sessions/' + encodeURIComponent(id) + '/events/stream');
|
|
20848
|
-
} catch (e) {
|
|
20849
|
-
startPolling(id);
|
|
20850
|
-
return;
|
|
20851
|
-
}
|
|
20852
|
-
var src = { type: 'sse', es: es, fallbackTimer: null };
|
|
20853
|
-
activeSource = src;
|
|
20854
|
-
// Every callback below re-checks focusId===id AND that src is still the
|
|
20855
|
-
// live source: a focus switch tears down src and starts a new one, so a
|
|
20856
|
-
// late open/message/error from this abandoned EventSource must be a no-op
|
|
20857
|
-
// rather than mutating the new focus's timeline or clobbering activeSource.
|
|
20858
|
-
function stale() { return activeSource !== src || focusId !== id; }
|
|
20859
|
-
src.fallbackTimer = setTimeout(function() {
|
|
20860
|
-
if (settled || stale()) return;
|
|
20861
|
-
settled = true;
|
|
20862
|
-
try { es.close(); } catch (e) {}
|
|
20863
|
-
startPolling(id);
|
|
20864
|
-
}, 2500);
|
|
20865
|
-
es.addEventListener('open', function() {
|
|
20866
|
-
if (settled || stale()) return;
|
|
20867
|
-
settled = true;
|
|
20868
|
-
clearTimeout(src.fallbackTimer);
|
|
20869
|
-
setStatus('streaming via SSE');
|
|
20870
|
-
});
|
|
20871
|
-
es.onmessage = function(evt) {
|
|
20872
|
-
if (stale()) return;
|
|
20873
|
-
if (!settled) {
|
|
20874
|
-
settled = true;
|
|
20875
|
-
clearTimeout(src.fallbackTimer);
|
|
20876
|
-
setStatus('streaming via SSE');
|
|
20877
|
-
}
|
|
20878
|
-
var rec;
|
|
20879
|
-
try { rec = JSON.parse(evt.data); } catch (e) { return; }
|
|
20880
|
-
var prev = timelineState;
|
|
20881
|
-
timelineState = reduceEvent(timelineState, rec);
|
|
20882
|
-
if (typeof rec.seq === 'number' && rec.seq > sincePtr) sincePtr = rec.seq;
|
|
20883
|
-
if (timelineState !== prev) applyRecordToDom(prev.rows);
|
|
20884
|
-
};
|
|
20885
|
-
es.onerror = function() {
|
|
20886
|
-
if (stale()) return;
|
|
20887
|
-
if (!settled) {
|
|
20888
|
-
settled = true;
|
|
20889
|
-
clearTimeout(src.fallbackTimer);
|
|
20890
|
-
try { es.close(); } catch (e) {}
|
|
20891
|
-
startPolling(id);
|
|
20892
|
-
} else {
|
|
20893
|
-
// Do not leave EventSource to perform its native reconnect: this
|
|
20894
|
-
// endpoint replays from since=0 when no cursor is provided, so a
|
|
20895
|
-
// transparent reconnect would duplicate every row already reduced.
|
|
20896
|
-
// The bridge poll resumes from sincePtr instead and preserves the
|
|
20897
|
-
// exactly-once cursor contract.
|
|
20898
|
-
try { es.close(); } catch (e) {}
|
|
20899
|
-
startPolling(id);
|
|
20900
|
-
}
|
|
20901
|
-
};
|
|
20902
|
-
}
|
|
20903
|
-
|
|
20904
|
-
function startPolling(id) {
|
|
20905
|
-
activeSource = { type: 'poll', timer: null };
|
|
20906
|
-
setStatus('polling');
|
|
20907
|
-
function tick() {
|
|
20908
|
-
if (!activeSource || activeSource.type !== 'poll' || focusId !== id) return;
|
|
20909
|
-
callTool('app_session_events', { sessionId: id, since: sincePtr }).then(function(res) {
|
|
20910
|
-
var events = res.events || [];
|
|
20911
|
-
for (var i = 0; i < events.length; i++) {
|
|
20912
|
-
var prev = timelineState;
|
|
20913
|
-
timelineState = reduceEvent(timelineState, events[i]);
|
|
20914
|
-
if (timelineState !== prev) applyRecordToDom(prev.rows);
|
|
20915
|
-
}
|
|
20916
|
-
if (typeof res.nextSeq === 'number') sincePtr = res.nextSeq;
|
|
20917
|
-
setStatus('polling');
|
|
20918
|
-
if (activeSource) activeSource.timer = setTimeout(tick, 1500);
|
|
20919
|
-
}).catch(function() {
|
|
20920
|
-
setStatus('disconnected');
|
|
20921
|
-
if (activeSource) activeSource.timer = setTimeout(tick, 1500);
|
|
20922
|
-
});
|
|
20923
|
-
}
|
|
20924
|
-
tick();
|
|
20925
|
-
}
|
|
20926
|
-
|
|
20927
|
-
function startTimeline(id) {
|
|
20928
|
-
teardownTimeline();
|
|
20929
|
-
timelineState = initialTimelineState();
|
|
20930
|
-
sincePtr = 0;
|
|
20931
|
-
hideNewPill();
|
|
20932
|
-
renderTimelineFull();
|
|
20933
|
-
setStatus('connecting\u2026');
|
|
20934
|
-
attemptSSE(id);
|
|
20935
|
-
}
|
|
20936
|
-
|
|
20937
|
-
function setFocus(id) {
|
|
20938
|
-
focusId = id;
|
|
20939
|
-
renderTree();
|
|
20940
|
-
startTimeline(id);
|
|
20941
|
-
}
|
|
20942
|
-
|
|
20943
|
-
// ============================================================
|
|
20944
|
-
// Boot
|
|
20945
|
-
// ============================================================
|
|
20946
|
-
|
|
20947
|
-
initBridge().then(function() {
|
|
20948
|
-
var init = window.__APP_INIT__ || {};
|
|
20949
|
-
if (init.httpBaseUrl) return init;
|
|
20950
|
-
// Static ui:// resources render once at server-registration time with
|
|
20951
|
-
// EMPTY initData (mcp-apps-adapter.ts registerMcpApps) \u2014 fall back to
|
|
20952
|
-
// calling the tool ourselves over the bridge, same as the other panels.
|
|
20953
|
-
return callTool('live_session', {}).then(function(result) {
|
|
20954
|
-
window.__APP_INIT__ = result;
|
|
20955
|
-
return result;
|
|
20956
|
-
});
|
|
20957
|
-
}).then(function(init) {
|
|
20958
|
-
focusId = init.sessionId || null;
|
|
20959
|
-
onHostContext(function() {
|
|
20960
|
-
applyDisplayMode();
|
|
20961
|
-
updateHeader();
|
|
20962
|
-
});
|
|
20963
|
-
document.getElementById('head-selector').addEventListener('change', function(evt) {
|
|
20964
|
-
var id = evt.target.value;
|
|
20965
|
-
if (id && id !== focusId) setFocus(id);
|
|
20966
|
-
});
|
|
20967
|
-
document.getElementById('new-pill').addEventListener('click', function() {
|
|
20968
|
-
var body = document.getElementById('timeline-body');
|
|
20969
|
-
body.scrollTop = body.scrollHeight;
|
|
20970
|
-
hideNewPill();
|
|
20971
|
-
});
|
|
20972
|
-
document.getElementById('timeline-body').addEventListener('scroll', function() {
|
|
20973
|
-
// User scrolled back near the bottom \u2014 the pill is stale, hide it.
|
|
20974
|
-
var el = document.getElementById('timeline-body');
|
|
20975
|
-
if (isNearBottom(el.scrollHeight, el.scrollTop, el.clientHeight)) hideNewPill();
|
|
20976
|
-
});
|
|
20977
|
-
window.addEventListener('resize', function() {
|
|
20978
|
-
var c = isCompact();
|
|
20979
|
-
if (c !== compactMode) { applyDisplayMode(); }
|
|
20980
|
-
});
|
|
20981
|
-
applyDisplayMode();
|
|
20982
|
-
pollTree();
|
|
20983
|
-
treeTimer = setInterval(pollTree, 2000);
|
|
20984
|
-
setInterval(updateHeader, 1000); // keep the WP5 elapsed clock fresh
|
|
20985
|
-
if (focusId) startTimeline(focusId);
|
|
20986
|
-
}).catch(function(e) {
|
|
20987
|
-
setStatus('Bridge error: ' + e.message);
|
|
20988
|
-
});
|
|
20989
|
-
</script>
|
|
20990
|
-
</body>
|
|
20991
|
-
</html>`;
|
|
20992
|
-
}
|
|
20993
18615
|
init_transcript_writer();
|
|
20994
18616
|
function readSessionEventsSince(filePath, since, limit) {
|
|
20995
18617
|
let raw;
|
|
@@ -21475,6 +19097,350 @@ function createAppRegistry(opts) {
|
|
|
21475
19097
|
}
|
|
21476
19098
|
};
|
|
21477
19099
|
}
|
|
19100
|
+
var AppPathTraversalError = class extends Error {
|
|
19101
|
+
code = "APP_PATH_TRAVERSAL";
|
|
19102
|
+
constructor(relPath) {
|
|
19103
|
+
super(`app-data: path traversal rejected for "${relPath}" \u2014 must resolve inside the app dir.`);
|
|
19104
|
+
this.name = "AppPathTraversalError";
|
|
19105
|
+
}
|
|
19106
|
+
};
|
|
19107
|
+
var DEFAULT_APP_DATA_SUBDIR = "data";
|
|
19108
|
+
function appDataDir(app) {
|
|
19109
|
+
return app.dataDir ?? join(app.dir, DEFAULT_APP_DATA_SUBDIR);
|
|
19110
|
+
}
|
|
19111
|
+
function isDefaultAppDataLayout(app) {
|
|
19112
|
+
return resolve(appDataDir(app)) === resolve(app.dir, DEFAULT_APP_DATA_SUBDIR);
|
|
19113
|
+
}
|
|
19114
|
+
function collapseLegacyDataPrefix(relPath) {
|
|
19115
|
+
const n = normalize(relPath);
|
|
19116
|
+
if (n === DEFAULT_APP_DATA_SUBDIR) return ".";
|
|
19117
|
+
const prefix = DEFAULT_APP_DATA_SUBDIR + sep;
|
|
19118
|
+
if (n.startsWith(prefix)) {
|
|
19119
|
+
const rest = n.slice(prefix.length);
|
|
19120
|
+
return rest === "" ? "." : rest;
|
|
19121
|
+
}
|
|
19122
|
+
return relPath;
|
|
19123
|
+
}
|
|
19124
|
+
function resolveAppDataPath(appDir, relPath) {
|
|
19125
|
+
if (isAbsolute(relPath)) throw new AppPathTraversalError(relPath);
|
|
19126
|
+
if (/^[A-Za-z]:/.test(relPath)) throw new AppPathTraversalError(relPath);
|
|
19127
|
+
const root = resolve(appDir);
|
|
19128
|
+
const target = resolve(appDir, relPath);
|
|
19129
|
+
const rootWithSep = root.endsWith(sep) ? root : root + sep;
|
|
19130
|
+
if (target !== root && !target.startsWith(rootWithSep)) {
|
|
19131
|
+
throw new AppPathTraversalError(relPath);
|
|
19132
|
+
}
|
|
19133
|
+
return target;
|
|
19134
|
+
}
|
|
19135
|
+
async function pathExists(p) {
|
|
19136
|
+
try {
|
|
19137
|
+
await stat(p);
|
|
19138
|
+
return true;
|
|
19139
|
+
} catch {
|
|
19140
|
+
return false;
|
|
19141
|
+
}
|
|
19142
|
+
}
|
|
19143
|
+
async function realpathMaybe(p) {
|
|
19144
|
+
try {
|
|
19145
|
+
return await realpath(p);
|
|
19146
|
+
} catch {
|
|
19147
|
+
return void 0;
|
|
19148
|
+
}
|
|
19149
|
+
}
|
|
19150
|
+
async function resolveAppDataRoots(app, opts) {
|
|
19151
|
+
const dataDir = resolve(appDataDir(app));
|
|
19152
|
+
if (opts?.ensureDataDir) await mkdir(dataDir, { recursive: true });
|
|
19153
|
+
const realData = await realpathMaybe(dataDir);
|
|
19154
|
+
const legacyRoot = await realpathMaybe(app.dir);
|
|
19155
|
+
return {
|
|
19156
|
+
dataRoot: realData ?? dataDir,
|
|
19157
|
+
dataRootExists: realData !== void 0,
|
|
19158
|
+
legacyRoot,
|
|
19159
|
+
defaultLayout: isDefaultAppDataLayout(app)
|
|
19160
|
+
};
|
|
19161
|
+
}
|
|
19162
|
+
async function assertRealInside(root, target) {
|
|
19163
|
+
let real;
|
|
19164
|
+
try {
|
|
19165
|
+
real = await realpath(target);
|
|
19166
|
+
} catch {
|
|
19167
|
+
return;
|
|
19168
|
+
}
|
|
19169
|
+
const rootWithSep = root.endsWith(sep) ? root : root + sep;
|
|
19170
|
+
if (real !== root && !real.startsWith(rootWithSep)) {
|
|
19171
|
+
throw new AppPathTraversalError(target);
|
|
19172
|
+
}
|
|
19173
|
+
}
|
|
19174
|
+
function firstSegment(rel) {
|
|
19175
|
+
const n = normalize(rel);
|
|
19176
|
+
const seg = n.split(sep).find((s) => s !== "" && s !== ".");
|
|
19177
|
+
return seg === void 0 || seg === ".." ? void 0 : seg;
|
|
19178
|
+
}
|
|
19179
|
+
async function locateAppDataPath(roots, relPath) {
|
|
19180
|
+
const rel = roots.defaultLayout ? collapseLegacyDataPrefix(relPath) : relPath;
|
|
19181
|
+
const primary = resolveAppDataPath(roots.dataRoot, rel);
|
|
19182
|
+
await assertRealInside(roots.dataRoot, primary);
|
|
19183
|
+
const primaryExists = await pathExists(primary);
|
|
19184
|
+
let legacyTarget;
|
|
19185
|
+
if (roots.legacyRoot !== void 0) {
|
|
19186
|
+
try {
|
|
19187
|
+
legacyTarget = resolveAppDataPath(roots.legacyRoot, relPath);
|
|
19188
|
+
} catch {
|
|
19189
|
+
legacyTarget = void 0;
|
|
19190
|
+
}
|
|
19191
|
+
if (legacyTarget === primary) legacyTarget = void 0;
|
|
19192
|
+
}
|
|
19193
|
+
let legacyExists = false;
|
|
19194
|
+
if (legacyTarget !== void 0) {
|
|
19195
|
+
await assertRealInside(roots.legacyRoot, legacyTarget);
|
|
19196
|
+
legacyExists = await pathExists(legacyTarget);
|
|
19197
|
+
}
|
|
19198
|
+
if (primaryExists) {
|
|
19199
|
+
return {
|
|
19200
|
+
target: primary,
|
|
19201
|
+
root: roots.dataRoot,
|
|
19202
|
+
legacy: false,
|
|
19203
|
+
...legacyExists && legacyTarget !== void 0 ? { sibling: legacyTarget } : {}
|
|
19204
|
+
};
|
|
19205
|
+
}
|
|
19206
|
+
if (legacyExists && legacyTarget !== void 0) {
|
|
19207
|
+
return { target: legacyTarget, root: roots.legacyRoot, legacy: true };
|
|
19208
|
+
}
|
|
19209
|
+
const top = firstSegment(relPath);
|
|
19210
|
+
if (top !== void 0 && roots.legacyRoot !== void 0 && legacyTarget !== void 0) {
|
|
19211
|
+
const primaryTop = resolveAppDataPath(roots.dataRoot, roots.defaultLayout ? collapseLegacyDataPrefix(top) : top);
|
|
19212
|
+
const legacyTop = resolveAppDataPath(roots.legacyRoot, top);
|
|
19213
|
+
if (legacyTop !== primaryTop && !await pathExists(primaryTop) && await pathExists(legacyTop)) {
|
|
19214
|
+
return { target: legacyTarget, root: roots.legacyRoot, legacy: true };
|
|
19215
|
+
}
|
|
19216
|
+
}
|
|
19217
|
+
return { target: primary, root: roots.dataRoot, legacy: false };
|
|
19218
|
+
}
|
|
19219
|
+
function textResult(body) {
|
|
19220
|
+
return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
|
|
19221
|
+
}
|
|
19222
|
+
function errorResult(text10) {
|
|
19223
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: text10 }) }], isError: true };
|
|
19224
|
+
}
|
|
19225
|
+
async function atomicWrite(filePath, data) {
|
|
19226
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
19227
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
19228
|
+
await writeFile(tmp, data, "utf8");
|
|
19229
|
+
await rename(tmp, filePath);
|
|
19230
|
+
}
|
|
19231
|
+
async function writeRaw(roots, rel, data) {
|
|
19232
|
+
await atomicWrite((await locateAppDataPath(roots, rel)).target, data);
|
|
19233
|
+
}
|
|
19234
|
+
async function writeJson(roots, rel, value) {
|
|
19235
|
+
await writeRaw(roots, rel, JSON.stringify(value, null, 2) + "\n");
|
|
19236
|
+
}
|
|
19237
|
+
async function readTextMaybe(path) {
|
|
19238
|
+
try {
|
|
19239
|
+
return await readFile(path, "utf8");
|
|
19240
|
+
} catch {
|
|
19241
|
+
return void 0;
|
|
19242
|
+
}
|
|
19243
|
+
}
|
|
19244
|
+
async function readJsonMaybe(path) {
|
|
19245
|
+
const raw = await readTextMaybe(path);
|
|
19246
|
+
if (raw === void 0) return void 0;
|
|
19247
|
+
try {
|
|
19248
|
+
return JSON.parse(raw);
|
|
19249
|
+
} catch {
|
|
19250
|
+
return void 0;
|
|
19251
|
+
}
|
|
19252
|
+
}
|
|
19253
|
+
function normalizeJob(raw) {
|
|
19254
|
+
const jobId = raw.jobId ?? raw.id;
|
|
19255
|
+
const out = { ...raw, id: jobId, jobId };
|
|
19256
|
+
if (out.applyUrl === void 0 || out.applyUrl === null) out.applyUrl = raw.url;
|
|
19257
|
+
return out;
|
|
19258
|
+
}
|
|
19259
|
+
async function readDossierJobId(dossierDir) {
|
|
19260
|
+
const parsed = await readJsonMaybe(join(dossierDir, "job.json"));
|
|
19261
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
19262
|
+
const jobId = parsed.jobId ?? parsed.id;
|
|
19263
|
+
return typeof jobId === "string" && jobId.length > 0 ? jobId : void 0;
|
|
19264
|
+
}
|
|
19265
|
+
function registerAppDataTools(server, opts) {
|
|
19266
|
+
const { appRegistry } = opts;
|
|
19267
|
+
server.tool(
|
|
19268
|
+
"app_data_read",
|
|
19269
|
+
"Read an app-scoped data file (app-relative path). JSON paths return the parsed value in `content`; everything else returns the raw text. Paths resolve under the app's data dir (`dataDir`, default `<dir>/data`); under the default layout a leading `data/` is accepted as the legacy spelling, and a file that only exists under the app's source dir (a pre-dataDir install) is still found there. Path traversal outside either root is rejected.",
|
|
19270
|
+
{ appId: z.string(), path: z.string().describe("App-relative path under the app's data dir.") },
|
|
19271
|
+
async (input) => {
|
|
19272
|
+
const installed = appRegistry.getApp(input.appId);
|
|
19273
|
+
if (!installed) return errorResult(`app_data_read: no installed app "${input.appId}".`);
|
|
19274
|
+
let target;
|
|
19275
|
+
try {
|
|
19276
|
+
const roots = await resolveAppDataRoots(installed);
|
|
19277
|
+
target = (await locateAppDataPath(roots, input.path)).target;
|
|
19278
|
+
} catch (err) {
|
|
19279
|
+
return errorResult(`app_data_read: ${err instanceof Error ? err.message : String(err)}`);
|
|
19280
|
+
}
|
|
19281
|
+
const raw = await readTextMaybe(target);
|
|
19282
|
+
if (raw === void 0) return textResult({ appId: input.appId, path: input.path, exists: false });
|
|
19283
|
+
if (input.path.endsWith(".json")) {
|
|
19284
|
+
try {
|
|
19285
|
+
return textResult({ appId: input.appId, path: input.path, exists: true, content: JSON.parse(raw) });
|
|
19286
|
+
} catch {
|
|
19287
|
+
return textResult({ appId: input.appId, path: input.path, exists: true, content: raw });
|
|
19288
|
+
}
|
|
19289
|
+
}
|
|
19290
|
+
return textResult({ appId: input.appId, path: input.path, exists: true, content: raw });
|
|
19291
|
+
}
|
|
19292
|
+
);
|
|
19293
|
+
server.tool(
|
|
19294
|
+
"app_data_write",
|
|
19295
|
+
"Write an app-scoped data file (app-relative path), creating parent directories as needed. `.json` paths are JSON-stringified (pretty); other paths write the raw string passed as `content.text` (or a plain string `content`). Atomic write (tmp + rename). New files land under the app's data dir (`dataDir`, default `<dir>/data`); a file (or top-level folder) that already exists under the app's source dir from a pre-dataDir install is updated in place. Path traversal outside either root is rejected.",
|
|
19296
|
+
{
|
|
19297
|
+
appId: z.string(),
|
|
19298
|
+
path: z.string().describe("App-relative path under the app's data dir."),
|
|
19299
|
+
content: z.unknown().describe("JSON value for `.json` paths, or `{ text }` / string for others.")
|
|
19300
|
+
},
|
|
19301
|
+
async (input) => {
|
|
19302
|
+
const installed = appRegistry.getApp(input.appId);
|
|
19303
|
+
if (!installed) return errorResult(`app_data_write: no installed app "${input.appId}".`);
|
|
19304
|
+
let target;
|
|
19305
|
+
try {
|
|
19306
|
+
const roots = await resolveAppDataRoots(installed, { ensureDataDir: true });
|
|
19307
|
+
target = (await locateAppDataPath(roots, input.path)).target;
|
|
19308
|
+
} catch (err) {
|
|
19309
|
+
return errorResult(`app_data_write: ${err instanceof Error ? err.message : String(err)}`);
|
|
19310
|
+
}
|
|
19311
|
+
let payload;
|
|
19312
|
+
if (input.path.endsWith(".json")) {
|
|
19313
|
+
payload = JSON.stringify(input.content, null, 2);
|
|
19314
|
+
} else {
|
|
19315
|
+
const raw = input.content;
|
|
19316
|
+
if (typeof raw === "string") payload = raw;
|
|
19317
|
+
else if (raw !== null && typeof raw === "object" && typeof raw.text === "string") {
|
|
19318
|
+
payload = raw.text;
|
|
19319
|
+
} else {
|
|
19320
|
+
payload = JSON.stringify(raw);
|
|
19321
|
+
}
|
|
19322
|
+
}
|
|
19323
|
+
try {
|
|
19324
|
+
await atomicWrite(target, payload);
|
|
19325
|
+
return textResult({ appId: input.appId, path: input.path, size: Buffer.byteLength(payload, "utf8") });
|
|
19326
|
+
} catch (err) {
|
|
19327
|
+
return errorResult(`app_data_write: ${err instanceof Error ? err.message : String(err)}`);
|
|
19328
|
+
}
|
|
19329
|
+
}
|
|
19330
|
+
);
|
|
19331
|
+
server.tool(
|
|
19332
|
+
"app_data_list",
|
|
19333
|
+
"List entries (name + type + size) under an app-relative directory (default `.`, the app's data dir). A missing directory returns empty entries, not an error. When the same directory also exists under the app's source dir (a pre-dataDir install) both views are merged, data dir entries winning on name clashes; `.` lists the data dir only (or the source dir while no data dir exists yet). Path traversal outside either root is rejected.",
|
|
19334
|
+
{
|
|
19335
|
+
appId: z.string(),
|
|
19336
|
+
dir: z.string().optional().describe("App-relative directory to list. Defaults to `.`.")
|
|
19337
|
+
},
|
|
19338
|
+
async (input) => {
|
|
19339
|
+
const installed = appRegistry.getApp(input.appId);
|
|
19340
|
+
if (!installed) return errorResult(`app_data_list: no installed app "${input.appId}".`);
|
|
19341
|
+
const relDir = input.dir ?? ".";
|
|
19342
|
+
const dirs = [];
|
|
19343
|
+
try {
|
|
19344
|
+
const roots = await resolveAppDataRoots(installed);
|
|
19345
|
+
const located = await locateAppDataPath(roots, relDir);
|
|
19346
|
+
const isRoot = located.target === roots.dataRoot || located.target === roots.legacyRoot;
|
|
19347
|
+
dirs.push(located.target);
|
|
19348
|
+
if (!isRoot && located.sibling !== void 0) dirs.push(located.sibling);
|
|
19349
|
+
} catch (err) {
|
|
19350
|
+
return errorResult(`app_data_list: ${err instanceof Error ? err.message : String(err)}`);
|
|
19351
|
+
}
|
|
19352
|
+
const seen = /* @__PURE__ */ new Map();
|
|
19353
|
+
for (const target of dirs) {
|
|
19354
|
+
let dirents;
|
|
19355
|
+
try {
|
|
19356
|
+
dirents = await readdir(target, { withFileTypes: true });
|
|
19357
|
+
} catch {
|
|
19358
|
+
continue;
|
|
19359
|
+
}
|
|
19360
|
+
for (const d of dirents) {
|
|
19361
|
+
if (seen.has(d.name)) continue;
|
|
19362
|
+
const isDirectory = d.isDirectory();
|
|
19363
|
+
let size = 0;
|
|
19364
|
+
if (!isDirectory) {
|
|
19365
|
+
try {
|
|
19366
|
+
size = (await stat(join(target, d.name))).size;
|
|
19367
|
+
} catch {
|
|
19368
|
+
size = 0;
|
|
19369
|
+
}
|
|
19370
|
+
}
|
|
19371
|
+
seen.set(d.name, { name: d.name, type: isDirectory ? "directory" : "file", size });
|
|
19372
|
+
}
|
|
19373
|
+
}
|
|
19374
|
+
const entries = [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
19375
|
+
return textResult({ appId: input.appId, dir: relDir, entries });
|
|
19376
|
+
}
|
|
19377
|
+
);
|
|
19378
|
+
server.tool(
|
|
19379
|
+
"app_data_migrate",
|
|
19380
|
+
"One-time import of legacy job-app data into the durable shape under the app's data dir: `jobs/<jobId>.json` (normalized id/jobId/applyUrl), `rankings/latest.json` (full ranked list) + per-job ranking artifacts, `applications/<jobId>/{job.json,cv.json,cover.md}` from matching `dossiers/*` folders, and `state.json`. The legacy inputs (`ranked-jobs.json`, `dossiers/`) are read from wherever they resolve \u2014 the app's source dir for pre-dataDir installs. Idempotent \u2014 re-running after migration returns `alreadyMigrated` unless `force`.",
|
|
19381
|
+
{
|
|
19382
|
+
appId: z.string(),
|
|
19383
|
+
force: z.boolean().optional().describe("Re-run even if already migrated.")
|
|
19384
|
+
},
|
|
19385
|
+
async (input) => {
|
|
19386
|
+
const installed = appRegistry.getApp(input.appId);
|
|
19387
|
+
if (!installed) return errorResult(`app_data_migrate: no installed app "${input.appId}".`);
|
|
19388
|
+
let roots;
|
|
19389
|
+
try {
|
|
19390
|
+
roots = await resolveAppDataRoots(installed, { ensureDataDir: true });
|
|
19391
|
+
} catch (err) {
|
|
19392
|
+
return errorResult(`app_data_migrate: ${err instanceof Error ? err.message : String(err)}`);
|
|
19393
|
+
}
|
|
19394
|
+
const at = async (rel) => (await locateAppDataPath(roots, rel)).target;
|
|
19395
|
+
const stateRel = "state.json";
|
|
19396
|
+
if (!input.force && await readJsonMaybe(await at(stateRel)) !== void 0) {
|
|
19397
|
+
return textResult({ appId: input.appId, migrated: false, alreadyMigrated: true });
|
|
19398
|
+
}
|
|
19399
|
+
let jobs = [];
|
|
19400
|
+
const rankedRaw = await readJsonMaybe(await at("ranked-jobs.json"));
|
|
19401
|
+
if (Array.isArray(rankedRaw)) jobs = rankedRaw;
|
|
19402
|
+
const normalized = jobs.map(normalizeJob).filter((j) => typeof j.jobId === "string" && j.jobId.length > 0);
|
|
19403
|
+
for (const job of normalized) {
|
|
19404
|
+
const id = job.jobId;
|
|
19405
|
+
await writeJson(roots, `jobs/${id}.json`, job);
|
|
19406
|
+
await writeJson(roots, `rankings/${id}.json`, job);
|
|
19407
|
+
}
|
|
19408
|
+
await writeJson(roots, "rankings/latest.json", normalized);
|
|
19409
|
+
let folderNames = [];
|
|
19410
|
+
try {
|
|
19411
|
+
folderNames = (await readdir(await at("dossiers"), { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
19412
|
+
} catch {
|
|
19413
|
+
folderNames = [];
|
|
19414
|
+
}
|
|
19415
|
+
const matched = /* @__PURE__ */ new Set();
|
|
19416
|
+
const skippedFolders = [];
|
|
19417
|
+
for (const name of folderNames) {
|
|
19418
|
+
const dossierDir = await at(join("dossiers", name));
|
|
19419
|
+
const jobId = await readDossierJobId(dossierDir);
|
|
19420
|
+
const targetJob = normalized.find((j) => j.jobId === jobId);
|
|
19421
|
+
if (!jobId || !targetJob) {
|
|
19422
|
+
skippedFolders.push(name);
|
|
19423
|
+
continue;
|
|
19424
|
+
}
|
|
19425
|
+
matched.add(jobId);
|
|
19426
|
+
await writeJson(roots, `applications/${jobId}/job.json`, targetJob);
|
|
19427
|
+
const cv = await readJsonMaybe(join(dossierDir, "cv.json"));
|
|
19428
|
+
if (cv !== void 0) await writeJson(roots, `applications/${jobId}/cv.json`, cv);
|
|
19429
|
+
const cover = await readTextMaybe(join(dossierDir, "cover.md"));
|
|
19430
|
+
if (cover !== void 0) await writeRaw(roots, `applications/${jobId}/cover.md`, cover);
|
|
19431
|
+
}
|
|
19432
|
+
const jobCount = normalized.length;
|
|
19433
|
+
const dossierCount = matched.size;
|
|
19434
|
+
await writeJson(roots, stateRel, {
|
|
19435
|
+
migratedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19436
|
+
jobCount,
|
|
19437
|
+
dossierCount,
|
|
19438
|
+
skippedFolders
|
|
19439
|
+
});
|
|
19440
|
+
return textResult({ appId: input.appId, migrated: true, jobCount, dossierCount, skippedFolders });
|
|
19441
|
+
}
|
|
19442
|
+
);
|
|
19443
|
+
}
|
|
21478
19444
|
var EMPTY_CATALOG = { apps: [] };
|
|
21479
19445
|
function defaultAppCatalogPath() {
|
|
21480
19446
|
return join(homedir(), ".agentproto", "app-catalog.json");
|
|
@@ -21528,25 +19494,40 @@ function resolveAgentRefsForWorkflow(appRegistry, workflowId) {
|
|
|
21528
19494
|
}
|
|
21529
19495
|
return refs;
|
|
21530
19496
|
}
|
|
21531
|
-
function
|
|
19497
|
+
function buildAgentRunSpawnConfig(agent, input) {
|
|
19498
|
+
const model = input.model ?? agent.model;
|
|
19499
|
+
const body = agent.body.trim();
|
|
19500
|
+
const prompt = body ? input.prompt ? `${body}
|
|
19501
|
+
|
|
19502
|
+
${input.prompt}` : body : input.prompt;
|
|
19503
|
+
return {
|
|
19504
|
+
...model !== void 0 ? { model } : {},
|
|
19505
|
+
...prompt !== void 0 ? { prompt } : {}
|
|
19506
|
+
};
|
|
19507
|
+
}
|
|
19508
|
+
async function loadAgentPromptDefaults(agentPath) {
|
|
19509
|
+
const { handle, body } = await loadAgent(agentPath);
|
|
19510
|
+
return { ...typeof handle.model === "string" ? { model: handle.model } : {}, body };
|
|
19511
|
+
}
|
|
19512
|
+
function textResult2(body) {
|
|
21532
19513
|
return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
|
|
21533
19514
|
}
|
|
21534
|
-
function
|
|
19515
|
+
function errorResult2(text10) {
|
|
21535
19516
|
return { content: [{ type: "text", text: JSON.stringify({ error: text10 }) }], isError: true };
|
|
21536
19517
|
}
|
|
21537
19518
|
function notEnabled(tool) {
|
|
21538
|
-
return
|
|
19519
|
+
return errorResult2(
|
|
21539
19520
|
`${tool} is not enabled \u2014 the daemon was started without an adapter resolver. Re-run the daemon with the \`@agentproto/cli\` shim wired (see playground/scripts/gateway.ts).`
|
|
21540
19521
|
);
|
|
21541
19522
|
}
|
|
21542
19523
|
async function performAppToolCall(appRegistry, input, deps2) {
|
|
21543
19524
|
const installed = appRegistry.getApp(input.appId);
|
|
21544
19525
|
if (!installed || !installed.ui) {
|
|
21545
|
-
return
|
|
19526
|
+
return errorResult2(`app_tool_call: app "${input.appId}" is not installed or has no UI.`);
|
|
21546
19527
|
}
|
|
21547
19528
|
const allowlist = installed.ui.tools ?? [];
|
|
21548
19529
|
if (!allowlist.includes(input.tool)) {
|
|
21549
|
-
return
|
|
19530
|
+
return errorResult2(
|
|
21550
19531
|
`app_tool_call: tool "${input.tool}" is not in app "${input.appId}"'s ui.tools allowlist: ${allowlist.length > 0 ? allowlist.join(", ") : "(empty)"}`
|
|
21551
19532
|
);
|
|
21552
19533
|
}
|
|
@@ -21557,18 +19538,18 @@ async function performAppToolCall(appRegistry, input, deps2) {
|
|
|
21557
19538
|
const rest = input.tool.slice("imported:".length);
|
|
21558
19539
|
const slash = rest.indexOf("/");
|
|
21559
19540
|
if (slash === -1) {
|
|
21560
|
-
return
|
|
19541
|
+
return errorResult2(
|
|
21561
19542
|
`app_tool_call: malformed imported tool id "${input.tool}" \u2014 expected "imported:<alias>/<toolName>".`
|
|
21562
19543
|
);
|
|
21563
19544
|
}
|
|
21564
19545
|
const result2 = await deps2.callImportedTool(rest.slice(0, slash), rest.slice(slash + 1), args);
|
|
21565
|
-
return
|
|
19546
|
+
return textResult2(result2);
|
|
21566
19547
|
}
|
|
21567
19548
|
if (!deps2.dispatchTool) return notEnabled("app_tool_call");
|
|
21568
19549
|
const result = await deps2.dispatchTool(input.tool, args);
|
|
21569
|
-
return
|
|
19550
|
+
return textResult2(result);
|
|
21570
19551
|
} catch (err) {
|
|
21571
|
-
return
|
|
19552
|
+
return errorResult2(`app_tool_call: ${err instanceof Error ? err.message : String(err)}`);
|
|
21572
19553
|
}
|
|
21573
19554
|
}
|
|
21574
19555
|
function refIdOf(ref) {
|
|
@@ -21630,7 +19611,19 @@ async function normalizeExternalReadRoots(roots) {
|
|
|
21630
19611
|
}
|
|
21631
19612
|
return { ok: true, roots: normalized };
|
|
21632
19613
|
}
|
|
21633
|
-
async function
|
|
19614
|
+
async function resolveInstallDataDir(input) {
|
|
19615
|
+
const raw = input.explicit ?? input.previous ?? input.hint;
|
|
19616
|
+
const dataDir = raw === void 0 ? resolve(input.dir, DEFAULT_APP_DATA_SUBDIR) : resolve(input.dir, expandHome2(raw));
|
|
19617
|
+
try {
|
|
19618
|
+
const st = await stat(dataDir);
|
|
19619
|
+
if (!st.isDirectory()) {
|
|
19620
|
+
return { ok: false, error: `dataDir "${dataDir}" exists but is not a directory.` };
|
|
19621
|
+
}
|
|
19622
|
+
} catch {
|
|
19623
|
+
}
|
|
19624
|
+
return { ok: true, dataDir };
|
|
19625
|
+
}
|
|
19626
|
+
async function performInstall(dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter, opts) {
|
|
21634
19627
|
let handle;
|
|
21635
19628
|
try {
|
|
21636
19629
|
handle = await loadAppHandle(dir);
|
|
@@ -21705,9 +19698,17 @@ async function performInstall(dir, appRegistry, listRegisteredToolIds, resolveAg
|
|
|
21705
19698
|
if (!result.ok) return { ok: false, error: `app_install: ${result.error}` };
|
|
21706
19699
|
externalReadRoots = result.roots;
|
|
21707
19700
|
}
|
|
19701
|
+
const dataDirResult = await resolveInstallDataDir({
|
|
19702
|
+
dir,
|
|
19703
|
+
...opts?.dataDir !== void 0 ? { explicit: opts.dataDir } : {},
|
|
19704
|
+
...appRegistry.getApp(handle.id)?.dataDir !== void 0 ? { previous: appRegistry.getApp(handle.id).dataDir } : {},
|
|
19705
|
+
...handle.data?.dir !== void 0 ? { hint: handle.data.dir } : {}
|
|
19706
|
+
});
|
|
19707
|
+
if (!dataDirResult.ok) return { ok: false, error: `app_install: ${dataDirResult.error}` };
|
|
21708
19708
|
const record2 = appRegistry.upsertApp({
|
|
21709
19709
|
appId: handle.id,
|
|
21710
19710
|
dir,
|
|
19711
|
+
dataDir: dataDirResult.dataDir,
|
|
21711
19712
|
...handle.version ? { version: handle.version } : {},
|
|
21712
19713
|
...handle.name ? { name: handle.name } : {},
|
|
21713
19714
|
...handle.description ? { description: handle.description } : {},
|
|
@@ -21732,12 +19733,19 @@ function registerAppTools(server, opts) {
|
|
|
21732
19733
|
});
|
|
21733
19734
|
server.tool(
|
|
21734
19735
|
"app_install",
|
|
21735
|
-
"Install an @agentproto/app-kit app from its emitted directory (`<dir>/.agentproto/APP.md` \u2014 see `defineApp().emit(dir)`). Validates every WORKFLOW.md `tool` step's id against the daemon's dispatchable tools (missing ids are reported ALL at once, instead of failing one at a time at STEP-DISPATCH time) and checks the `mastra-agent` adapter resolves. Agent-declared tool refs (workspace tools like `read_file`) are the adapter's own business and are never validated here \u2014 see `unvalidatedAgentTools` on the result. Re-installing the same appId upserts.",
|
|
21736
|
-
{
|
|
19736
|
+
"Install an @agentproto/app-kit app from its emitted directory (`<dir>/.agentproto/APP.md` \u2014 see `defineApp().emit(dir)`). Validates every WORKFLOW.md `tool` step's id against the daemon's dispatchable tools (missing ids are reported ALL at once, instead of failing one at a time at STEP-DISPATCH time) and checks the `mastra-agent` adapter resolves. Agent-declared tool refs (workspace tools like `read_file`) are the adapter's own business and are never validated here \u2014 see `unvalidatedAgentTools` on the result. Re-installing the same appId upserts (and keeps its existing `dataDir` unless a new one is passed).",
|
|
19737
|
+
{
|
|
19738
|
+
dir: z.string().describe("Absolute path to the app's directory."),
|
|
19739
|
+
dataDir: z.string().optional().describe(
|
|
19740
|
+
"Where the app's durable data (`app_data_*`) lives. Absolute or `~`-relative; a relative path is taken relative to `dir`. Defaults to the previously installed dataDir, else the APP.md `data.dir` hint, else `<dir>/data`."
|
|
19741
|
+
)
|
|
19742
|
+
},
|
|
21737
19743
|
async (input) => {
|
|
21738
|
-
const result = await performInstall(input.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter
|
|
21739
|
-
|
|
21740
|
-
|
|
19744
|
+
const result = await performInstall(input.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter, {
|
|
19745
|
+
...input.dataDir !== void 0 ? { dataDir: input.dataDir } : {}
|
|
19746
|
+
});
|
|
19747
|
+
if (!result.ok) return errorResult2(`app_install: ${result.error}`);
|
|
19748
|
+
return textResult2(result.record);
|
|
21741
19749
|
}
|
|
21742
19750
|
);
|
|
21743
19751
|
server.tool(
|
|
@@ -21748,6 +19756,7 @@ function registerAppTools(server, opts) {
|
|
|
21748
19756
|
const runs = appRegistry.listRuns();
|
|
21749
19757
|
const apps = appRegistry.listApps().map((app) => ({
|
|
21750
19758
|
...app,
|
|
19759
|
+
dataDir: appDataDir(app),
|
|
21751
19760
|
runs: runs.filter((r) => r.appId === app.appId).map((r) => ({
|
|
21752
19761
|
appRunId: r.appRunId,
|
|
21753
19762
|
status: r.status,
|
|
@@ -21759,12 +19768,12 @@ function registerAppTools(server, opts) {
|
|
|
21759
19768
|
sessions: r.sessions.length
|
|
21760
19769
|
}))
|
|
21761
19770
|
}));
|
|
21762
|
-
return
|
|
19771
|
+
return textResult2(apps);
|
|
21763
19772
|
}
|
|
21764
19773
|
);
|
|
21765
19774
|
server.tool(
|
|
21766
19775
|
"app_run",
|
|
21767
|
-
"Run an installed app's agents as live sessions \u2014 one `agent_start`-equivalent spawn per selected agent
|
|
19776
|
+
"Run an installed app's agents as live sessions \u2014 one `agent_start`-equivalent spawn per selected agent, grouped under a fresh appRunId. Re-reads the app's directory first, so a stale install record (paths moved, a workflow renamed) is refreshed before spawning \u2014 the same refreshed paths are what make `workflow_run_file` work against this app's WORKFLOW.md files. Poll with `app_status`, kill with `app_stop`.\n\nAdapter support: with the default adapter `mastra-agent` (or any other adapter whose manifest declares an `agent` option), each spawn is pointed straight at the agent's emitted AGENT.md via that option. Any OTHER adapter (`claude-code`, `hermes`, `codex`, ...) declares no such option, so its spawn is built FROM the AGENT.md instead: the frontmatter `model` becomes the spawn's default model (an explicit `model` arg here still wins) and the AGENT.md body becomes the system/prefix of the first prompt (a `prompt` arg is appended after it). `cwd` is still the app's dir, and the daemon's own MCP gateway is still mounted for adapters that get it by default (claude-code, hermes) \u2014 see `shouldInjectDaemonSelfMount` \u2014 so the spawned agent still reaches `app_data_*`/`mcp_imported_call` natively.\n\nOrchestration: pass `sequence` to run agents ONE-AT-A-TIME in the given order (each waits for its predecessor's session to reach a terminal state, bounded ~60\xD72s, before the next spawns) \u2014 the scout\u2192tailor workflow. Without `sequence`, `agents` spawn concurrently (legacy behaviour). When `sequence` is set every agent still lives under the SAME appRunId and is awaited; the run is marked `ended` once the last completes.\n\nRunner selection: `adapter`/`harness`/`model` are passed through to every spawn and mirrored onto the run record for observability. `harness` is the canonical slug and defaults `adapter` to itself when `adapter` is absent; a bare `adapter` sets `harness` to itself; both default to `mastra-agent`. `access.profileRef` pins a named auth profile (see `agent_start.access`) on every spawn this run makes \u2014 needed when an adapter's default credential profile is disabled on this host. An unresolvable adapter is collected as a per-agent error rather than failing the whole run.",
|
|
21768
19777
|
{
|
|
21769
19778
|
appId: z.string(),
|
|
21770
19779
|
agents: z.array(z.string()).optional().describe("Agent ids to run concurrently. Omit to run every agent the app bundles. Ignored when `sequence` is set."),
|
|
@@ -21774,7 +19783,12 @@ function registerAppTools(server, opts) {
|
|
|
21774
19783
|
scopeId: z.string().optional().describe("When passed, refuse to run if the app is not applied to this scope."),
|
|
21775
19784
|
adapter: z.string().optional().describe("Agent adapter slug (default `mastra-agent`). Used for the spawn; sets `harness` when `harness` is absent."),
|
|
21776
19785
|
harness: z.string().optional().describe("Canonical harness slug (defaults to `adapter`). Recorded on the run + each session; sets `adapter` when `adapter` is absent."),
|
|
21777
|
-
model: z.string().optional().describe(
|
|
19786
|
+
model: z.string().optional().describe(
|
|
19787
|
+
"Model id passed through to each spawned session. For an adapter with no `agent` option, this wins over the AGENT.md frontmatter's own `model`."
|
|
19788
|
+
),
|
|
19789
|
+
access: z.object({ profileRef: z.string().optional() }).optional().describe(
|
|
19790
|
+
"Named auth-profile pin threaded to every spawn's `agent_start`-equivalent (see `agent_start.access`) \u2014 e.g. `{ profileRef: \"claude-subs-agentik\" }` when the adapter's default credential profile is disabled on this host."
|
|
19791
|
+
)
|
|
21778
19792
|
// follow-up: no sandbox support in this WP — the e2b image doesn't carry
|
|
21779
19793
|
// the mastra-agent adapter yet (see output/phase-a-findings.md A3). Thread
|
|
21780
19794
|
// a `sandbox` field through to `spawnAgentSession` here once an image
|
|
@@ -21785,12 +19799,12 @@ function registerAppTools(server, opts) {
|
|
|
21785
19799
|
if (!resolveAgentAdapter) return notEnabled("app_run");
|
|
21786
19800
|
const installed = appRegistry.getApp(input.appId);
|
|
21787
19801
|
if (!installed) {
|
|
21788
|
-
return
|
|
19802
|
+
return errorResult2(`app_run: no installed app "${input.appId}" \u2014 call app_install first.`);
|
|
21789
19803
|
}
|
|
21790
19804
|
if (input.scopeId) {
|
|
21791
19805
|
const applied = appRegistry.listApplied(input.scopeId);
|
|
21792
19806
|
if (!applied.some((m) => m.appId === input.appId)) {
|
|
21793
|
-
return
|
|
19807
|
+
return errorResult2(
|
|
21794
19808
|
`app_run: app "${input.appId}" is not applied to scope "${input.scopeId}". Call app_apply first.`
|
|
21795
19809
|
);
|
|
21796
19810
|
}
|
|
@@ -21799,18 +19813,24 @@ function registerAppTools(server, opts) {
|
|
|
21799
19813
|
try {
|
|
21800
19814
|
refs = await readAppRefs(installed.dir);
|
|
21801
19815
|
} catch (err) {
|
|
21802
|
-
return
|
|
19816
|
+
return errorResult2(
|
|
21803
19817
|
`app_run: could not re-read "${installed.dir}": ${err instanceof Error ? err.message : String(err)}`
|
|
21804
19818
|
);
|
|
21805
19819
|
}
|
|
21806
19820
|
const app = appRegistry.upsertApp({ ...installed, agents: refs.agents, workflows: refs.workflows });
|
|
19821
|
+
if (app.agents.length === 0) {
|
|
19822
|
+
return errorResult2(`app_run: app "${app.appId}" declares no agents; open its UI panel instead.`);
|
|
19823
|
+
}
|
|
21807
19824
|
const adapter = input.adapter ?? input.harness ?? DEFAULT_AGENT_ADAPTER;
|
|
21808
19825
|
const harness = input.harness ?? input.adapter ?? DEFAULT_AGENT_ADAPTER;
|
|
21809
19826
|
const model = input.model;
|
|
19827
|
+
const resolvedAdapter = await resolveAgentAdapter(adapter);
|
|
19828
|
+
const declaredOptionsKnown = resolvedAdapter?.declaredOptions !== void 0;
|
|
19829
|
+
const declaresAgentOption = !declaredOptionsKnown || resolvedAdapter.declaredOptions.some((o) => o.id === "agent");
|
|
21810
19830
|
const ordered = input.sequence ?? input.agents ?? app.agents.map((a) => a.id);
|
|
21811
19831
|
const unknown = ordered.filter((id) => !app.agents.some((a) => a.id === id));
|
|
21812
19832
|
if (unknown.length > 0) {
|
|
21813
|
-
return
|
|
19833
|
+
return errorResult2(
|
|
21814
19834
|
`app_run: unknown agent id(s) for app "${app.appId}": ${unknown.join(", ")}`
|
|
21815
19835
|
);
|
|
21816
19836
|
}
|
|
@@ -21818,15 +19838,36 @@ function registerAppTools(server, opts) {
|
|
|
21818
19838
|
const errors = [];
|
|
21819
19839
|
const spawnOne = async (agentId) => {
|
|
21820
19840
|
const agentPath = app.agents.find((a) => a.id === agentId).path;
|
|
19841
|
+
let spawnModel = model;
|
|
19842
|
+
let spawnPrompt = input.prompt;
|
|
19843
|
+
let spawnOptions;
|
|
19844
|
+
if (declaresAgentOption) {
|
|
19845
|
+
spawnOptions = { agent: agentPath };
|
|
19846
|
+
} else {
|
|
19847
|
+
try {
|
|
19848
|
+
const defaults = await loadAgentPromptDefaults(agentPath);
|
|
19849
|
+
const built = buildAgentRunSpawnConfig(defaults, { model, prompt: input.prompt });
|
|
19850
|
+
spawnModel = built.model;
|
|
19851
|
+
spawnPrompt = built.prompt;
|
|
19852
|
+
} catch (err) {
|
|
19853
|
+
errors.push({
|
|
19854
|
+
agentId,
|
|
19855
|
+
error: `could not read AGENT.md "${agentPath}": ${err instanceof Error ? err.message : String(err)}`
|
|
19856
|
+
});
|
|
19857
|
+
return null;
|
|
19858
|
+
}
|
|
19859
|
+
}
|
|
21821
19860
|
const result = await spawnAgentSession(
|
|
21822
19861
|
{ registry, resolveAgentAdapter },
|
|
21823
19862
|
{
|
|
21824
19863
|
adapter,
|
|
21825
19864
|
...harness !== adapter ? { harness } : {},
|
|
21826
|
-
...
|
|
19865
|
+
...spawnModel !== void 0 ? { model: spawnModel } : {},
|
|
21827
19866
|
cwd: input.cwd ?? app.dir,
|
|
21828
|
-
...
|
|
21829
|
-
|
|
19867
|
+
...spawnPrompt ? { prompt: spawnPrompt } : {},
|
|
19868
|
+
...spawnOptions ? { options: spawnOptions } : {},
|
|
19869
|
+
...input.access ? { access: input.access } : {},
|
|
19870
|
+
appId: app.appId,
|
|
21830
19871
|
label: `app:${app.appId}:${agentId}`
|
|
21831
19872
|
}
|
|
21832
19873
|
);
|
|
@@ -21859,7 +19900,7 @@ function registerAppTools(server, opts) {
|
|
|
21859
19900
|
...model !== void 0 ? { model } : {}
|
|
21860
19901
|
});
|
|
21861
19902
|
appRegistry.endRun(run2.appRunId, { status: "ended" });
|
|
21862
|
-
return
|
|
19903
|
+
return textResult2({
|
|
21863
19904
|
appRunId: run2.appRunId,
|
|
21864
19905
|
status: run2.status,
|
|
21865
19906
|
...run2.endedAt ? { endedAt: run2.endedAt } : {},
|
|
@@ -21878,7 +19919,7 @@ function registerAppTools(server, opts) {
|
|
|
21878
19919
|
harness,
|
|
21879
19920
|
...model !== void 0 ? { model } : {}
|
|
21880
19921
|
});
|
|
21881
|
-
return
|
|
19922
|
+
return textResult2({
|
|
21882
19923
|
appRunId: run.appRunId,
|
|
21883
19924
|
adapter,
|
|
21884
19925
|
harness,
|
|
@@ -21894,7 +19935,7 @@ function registerAppTools(server, opts) {
|
|
|
21894
19935
|
{ appRunId: z.string() },
|
|
21895
19936
|
async (input) => {
|
|
21896
19937
|
const run = appRegistry.getRun(input.appRunId);
|
|
21897
|
-
if (!run) return
|
|
19938
|
+
if (!run) return errorResult2(`app_status: no app run "${input.appRunId}".`);
|
|
21898
19939
|
const app = appRegistry.getApp(run.appId);
|
|
21899
19940
|
const sessions = run.sessions.map((s) => ({
|
|
21900
19941
|
agentId: s.agentId,
|
|
@@ -21905,7 +19946,7 @@ function registerAppTools(server, opts) {
|
|
|
21905
19946
|
const storedTerminal = run.status !== "running";
|
|
21906
19947
|
const reconciledStatus = storedTerminal ? run.status : allSessionsTerminal ? "ended" : "running";
|
|
21907
19948
|
const workflowRuns = workflowRunner && app ? workflowRunner.list().filter((r) => app.workflows.some((w) => w.id === r.workflowId)) : [];
|
|
21908
|
-
return
|
|
19949
|
+
return textResult2({
|
|
21909
19950
|
appRunId: run.appRunId,
|
|
21910
19951
|
appId: run.appId,
|
|
21911
19952
|
status: reconciledStatus,
|
|
@@ -21925,7 +19966,7 @@ function registerAppTools(server, opts) {
|
|
|
21925
19966
|
{ appRunId: z.string() },
|
|
21926
19967
|
async (input) => {
|
|
21927
19968
|
const run = appRegistry.getRun(input.appRunId);
|
|
21928
|
-
if (!run) return
|
|
19969
|
+
if (!run) return errorResult2(`app_stop: no app run "${input.appRunId}".`);
|
|
21929
19970
|
const killed = [];
|
|
21930
19971
|
const notFound = [];
|
|
21931
19972
|
for (const s of run.sessions) {
|
|
@@ -21933,7 +19974,7 @@ function registerAppTools(server, opts) {
|
|
|
21933
19974
|
else notFound.push(s.sessionId);
|
|
21934
19975
|
}
|
|
21935
19976
|
const ended = appRegistry.endRun(input.appRunId);
|
|
21936
|
-
return
|
|
19977
|
+
return textResult2({
|
|
21937
19978
|
appRunId: input.appRunId,
|
|
21938
19979
|
killed,
|
|
21939
19980
|
...notFound.length > 0 ? { notFound } : {},
|
|
@@ -21947,17 +19988,20 @@ function registerAppTools(server, opts) {
|
|
|
21947
19988
|
{
|
|
21948
19989
|
appId: z.string(),
|
|
21949
19990
|
scopeId: z.string().optional().describe("Scope to apply to. Defaults to 'root'."),
|
|
21950
|
-
dir: z.string().optional().describe("Absolute path to install from if not already installed.")
|
|
19991
|
+
dir: z.string().optional().describe("Absolute path to install from if not already installed."),
|
|
19992
|
+
dataDir: z.string().optional().describe("Data root to install with (see app_install). Only used when installing.")
|
|
21951
19993
|
},
|
|
21952
19994
|
async (input) => {
|
|
21953
19995
|
const scopeId = input.scopeId ?? "root";
|
|
21954
19996
|
let installed = appRegistry.getApp(input.appId);
|
|
21955
19997
|
if (!installed && input.dir) {
|
|
21956
|
-
const installResult = await performInstall(input.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter
|
|
21957
|
-
|
|
19998
|
+
const installResult = await performInstall(input.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter, {
|
|
19999
|
+
...input.dataDir !== void 0 ? { dataDir: input.dataDir } : {}
|
|
20000
|
+
});
|
|
20001
|
+
if (!installResult.ok) return errorResult2(`app_apply: ${installResult.error}`);
|
|
21958
20002
|
installed = installResult.record;
|
|
21959
20003
|
} else if (!installed) {
|
|
21960
|
-
return
|
|
20004
|
+
return errorResult2(
|
|
21961
20005
|
`app_apply: app "${input.appId}" is not installed. Either call app_install first or provide a 'dir' parameter.`
|
|
21962
20006
|
);
|
|
21963
20007
|
}
|
|
@@ -21966,19 +20010,20 @@ function registerAppTools(server, opts) {
|
|
|
21966
20010
|
const appliedIds = new Set(applied.map((m) => m.appId));
|
|
21967
20011
|
const missing = installed.requires.filter((reqId) => !appliedIds.has(reqId));
|
|
21968
20012
|
if (missing.length > 0) {
|
|
21969
|
-
return
|
|
20013
|
+
return errorResult2(
|
|
21970
20014
|
`app_apply: app "${input.appId}" requires the following apps to be applied to scope "${scopeId}" first: ${missing.join(", ")}`
|
|
21971
20015
|
);
|
|
21972
20016
|
}
|
|
21973
20017
|
}
|
|
21974
20018
|
const mount = appRegistry.applyApp({ scopeId, appId: input.appId });
|
|
21975
|
-
return
|
|
20019
|
+
return textResult2({
|
|
21976
20020
|
scopeId: mount.scopeId,
|
|
21977
20021
|
appId: mount.appId,
|
|
21978
20022
|
appliedAt: mount.appliedAt,
|
|
21979
20023
|
agents: installed.agents,
|
|
21980
20024
|
workflows: installed.workflows,
|
|
21981
|
-
unvalidatedAgentTools: installed.unvalidatedAgentTools
|
|
20025
|
+
unvalidatedAgentTools: installed.unvalidatedAgentTools,
|
|
20026
|
+
...installed.agents.length === 0 ? { note: "app declares no agents \u2014 nothing to activate in this scope; open its UI panel directly." } : {}
|
|
21982
20027
|
});
|
|
21983
20028
|
}
|
|
21984
20029
|
);
|
|
@@ -22001,15 +20046,15 @@ function registerAppTools(server, opts) {
|
|
|
22001
20046
|
}
|
|
22002
20047
|
}
|
|
22003
20048
|
if (dependents.length > 0) {
|
|
22004
|
-
return
|
|
20049
|
+
return errorResult2(
|
|
22005
20050
|
`app_unapply: cannot unapply app "${input.appId}" from scope "${scopeId}" \u2014 the following apps in this scope require it: ${dependents.join(", ")}`
|
|
22006
20051
|
);
|
|
22007
20052
|
}
|
|
22008
20053
|
const removed = appRegistry.unapplyApp({ scopeId, appId: input.appId });
|
|
22009
20054
|
if (!removed) {
|
|
22010
|
-
return
|
|
20055
|
+
return errorResult2(`app_unapply: app "${input.appId}" is not applied to scope "${scopeId}".`);
|
|
22011
20056
|
}
|
|
22012
|
-
return
|
|
20057
|
+
return textResult2({ scopeId: removed.scopeId, appId: removed.appId, appliedAt: removed.appliedAt });
|
|
22013
20058
|
}
|
|
22014
20059
|
);
|
|
22015
20060
|
server.tool(
|
|
@@ -22033,7 +20078,7 @@ function registerAppTools(server, opts) {
|
|
|
22033
20078
|
} : {}
|
|
22034
20079
|
};
|
|
22035
20080
|
});
|
|
22036
|
-
return
|
|
20081
|
+
return textResult2(result);
|
|
22037
20082
|
}
|
|
22038
20083
|
);
|
|
22039
20084
|
server.tool(
|
|
@@ -22056,26 +20101,26 @@ function registerAppTools(server, opts) {
|
|
|
22056
20101
|
async (input) => {
|
|
22057
20102
|
const applied = appRegistry.listApplied().filter((m) => m.appId === input.appId);
|
|
22058
20103
|
if (applied.length > 0) {
|
|
22059
|
-
return
|
|
20104
|
+
return errorResult2(
|
|
22060
20105
|
`app_uninstall: app "${input.appId}" is applied to scope(s) ${applied.map((m) => m.scopeId).join(", ")} \u2014 unapply from scopes first.`
|
|
22061
20106
|
);
|
|
22062
20107
|
}
|
|
22063
20108
|
const runningRuns = appRegistry.listRuns().filter((r) => r.appId === input.appId && r.status === "running");
|
|
22064
20109
|
if (runningRuns.length > 0) {
|
|
22065
|
-
return
|
|
20110
|
+
return errorResult2(
|
|
22066
20111
|
`app_uninstall: app "${input.appId}" has running app_run(s) ${runningRuns.map((r) => r.appRunId).join(", ")} \u2014 stop app runs first.`
|
|
22067
20112
|
);
|
|
22068
20113
|
}
|
|
22069
20114
|
const removed = appRegistry.removeApp(input.appId);
|
|
22070
20115
|
if (!removed) {
|
|
22071
|
-
return
|
|
20116
|
+
return errorResult2(`app_uninstall: no installed app "${input.appId}".`);
|
|
22072
20117
|
}
|
|
22073
|
-
return
|
|
20118
|
+
return textResult2({ appId: removed.appId });
|
|
22074
20119
|
}
|
|
22075
20120
|
);
|
|
22076
20121
|
server.tool(
|
|
22077
20122
|
"app_catalog",
|
|
22078
|
-
"List browsable apps from the catalog file (default `~/.agentproto/app-catalog.json`, tolerates a missing file), merged with installed-app status \u2014 every entry reports `installed`, `hasUi`, `hasArtifact`, and `hasSkill`. Installed apps absent from the catalog file are included too
|
|
20123
|
+
"List browsable apps from the catalog file (default `~/.agentproto/app-catalog.json`, tolerates a missing file), merged with installed-app status \u2014 every entry reports `installed`, `hasUi`, `hasArtifact`, and `hasSkill`. Installed apps absent from the catalog file are included too, as are the five always-on builtin panels (category `builtin`) \u2014 they need no `app_install`.",
|
|
22079
20124
|
{
|
|
22080
20125
|
scopeId: z.string().optional().describe("Reserved for future scope-aware filtering. Currently unused.")
|
|
22081
20126
|
},
|
|
@@ -22114,7 +20159,8 @@ function registerAppTools(server, opts) {
|
|
|
22114
20159
|
hasSkill: app.skill !== void 0
|
|
22115
20160
|
});
|
|
22116
20161
|
}
|
|
22117
|
-
|
|
20162
|
+
entries.push(...builtinPanelCatalogEntries());
|
|
20163
|
+
return textResult2(entries);
|
|
22118
20164
|
}
|
|
22119
20165
|
);
|
|
22120
20166
|
server.tool(
|
|
@@ -22124,20 +20170,20 @@ function registerAppTools(server, opts) {
|
|
|
22124
20170
|
async (input) => {
|
|
22125
20171
|
const installed = appRegistry.getApp(input.appId);
|
|
22126
20172
|
if (!installed) {
|
|
22127
|
-
return
|
|
20173
|
+
return errorResult2(`app_artifact_get: no installed app "${input.appId}".`);
|
|
22128
20174
|
}
|
|
22129
20175
|
if (!installed.artifact) {
|
|
22130
|
-
return
|
|
20176
|
+
return errorResult2(`app_artifact_get: app "${input.appId}" has no artifact.`);
|
|
22131
20177
|
}
|
|
22132
20178
|
let html;
|
|
22133
20179
|
try {
|
|
22134
20180
|
html = await readFile(installed.artifact.path, "utf8");
|
|
22135
20181
|
} catch (err) {
|
|
22136
|
-
return
|
|
20182
|
+
return errorResult2(
|
|
22137
20183
|
`app_artifact_get: could not read artifact "${installed.artifact.path}": ${err instanceof Error ? err.message : String(err)}`
|
|
22138
20184
|
);
|
|
22139
20185
|
}
|
|
22140
|
-
return
|
|
20186
|
+
return textResult2({
|
|
22141
20187
|
appId: installed.appId,
|
|
22142
20188
|
...installed.artifact.title ? { title: installed.artifact.title } : {},
|
|
22143
20189
|
...installed.artifact.description ? { description: installed.artifact.description } : {},
|
|
@@ -22152,17 +20198,17 @@ function registerAppTools(server, opts) {
|
|
|
22152
20198
|
async (input) => {
|
|
22153
20199
|
const installed = appRegistry.getApp(input.appId);
|
|
22154
20200
|
if (!installed) {
|
|
22155
|
-
return
|
|
20201
|
+
return errorResult2(`app_skill_get: no installed app "${input.appId}".`);
|
|
22156
20202
|
}
|
|
22157
20203
|
if (!installed.skill) {
|
|
22158
|
-
return
|
|
20204
|
+
return errorResult2(`app_skill_get: app "${input.appId}" has no skill.`);
|
|
22159
20205
|
}
|
|
22160
20206
|
const skillDir = installed.skill.path;
|
|
22161
20207
|
let skillSource;
|
|
22162
20208
|
try {
|
|
22163
20209
|
skillSource = await readFile(join(skillDir, "SKILL.md"), "utf8");
|
|
22164
20210
|
} catch (err) {
|
|
22165
|
-
return
|
|
20211
|
+
return errorResult2(
|
|
22166
20212
|
`app_skill_get: could not read SKILL.md in "${skillDir}": ${err instanceof Error ? err.message : String(err)}`
|
|
22167
20213
|
);
|
|
22168
20214
|
}
|
|
@@ -22190,7 +20236,7 @@ function registerAppTools(server, opts) {
|
|
|
22190
20236
|
}
|
|
22191
20237
|
}
|
|
22192
20238
|
} catch (err) {
|
|
22193
|
-
return
|
|
20239
|
+
return errorResult2(
|
|
22194
20240
|
`app_skill_get: could not read skill directory "${skillDir}": ${err instanceof Error ? err.message : String(err)}`
|
|
22195
20241
|
);
|
|
22196
20242
|
}
|
|
@@ -22201,7 +20247,7 @@ function registerAppTools(server, opts) {
|
|
|
22201
20247
|
files
|
|
22202
20248
|
};
|
|
22203
20249
|
if (skipped.length > 0) result.skipped = skipped;
|
|
22204
|
-
return
|
|
20250
|
+
return textResult2(result);
|
|
22205
20251
|
}
|
|
22206
20252
|
);
|
|
22207
20253
|
}
|
|
@@ -22421,10 +20467,10 @@ async function assertExternalPathRealInside(root, target) {
|
|
|
22421
20467
|
throw new ExternalPathTraversalError(target);
|
|
22422
20468
|
}
|
|
22423
20469
|
}
|
|
22424
|
-
function
|
|
20470
|
+
function textResult3(body) {
|
|
22425
20471
|
return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
|
|
22426
20472
|
}
|
|
22427
|
-
function
|
|
20473
|
+
function errorResult3(text10) {
|
|
22428
20474
|
return { content: [{ type: "text", text: JSON.stringify({ error: text10 }) }], isError: true };
|
|
22429
20475
|
}
|
|
22430
20476
|
function registerAppExternalTools(server, opts) {
|
|
@@ -22439,27 +20485,27 @@ function registerAppExternalTools(server, opts) {
|
|
|
22439
20485
|
},
|
|
22440
20486
|
async (input) => {
|
|
22441
20487
|
const installed = appRegistry.getApp(input.appId);
|
|
22442
|
-
if (!installed) return
|
|
20488
|
+
if (!installed) return errorResult3(`app_external_list: no installed app "${input.appId}".`);
|
|
22443
20489
|
try {
|
|
22444
20490
|
assertRootGranted(installed, input.root);
|
|
22445
20491
|
} catch (err) {
|
|
22446
|
-
return
|
|
20492
|
+
return errorResult3(`app_external_list: ${err instanceof Error ? err.message : String(err)}`);
|
|
22447
20493
|
}
|
|
22448
20494
|
const root = await realpathExternalRoot(input.root);
|
|
22449
|
-
if (!root) return
|
|
20495
|
+
if (!root) return errorResult3(`app_external_list: root "${input.root}" is not accessible.`);
|
|
22450
20496
|
const relPath = input.path ?? "";
|
|
22451
20497
|
let target;
|
|
22452
20498
|
try {
|
|
22453
20499
|
target = resolveExternalPath(root, relPath);
|
|
22454
20500
|
await assertExternalPathRealInside(root, target);
|
|
22455
20501
|
} catch (err) {
|
|
22456
|
-
return
|
|
20502
|
+
return errorResult3(`app_external_list: ${err instanceof Error ? err.message : String(err)}`);
|
|
22457
20503
|
}
|
|
22458
20504
|
let dirents;
|
|
22459
20505
|
try {
|
|
22460
20506
|
dirents = await readdir(target, { withFileTypes: true });
|
|
22461
20507
|
} catch (err) {
|
|
22462
|
-
return
|
|
20508
|
+
return errorResult3(
|
|
22463
20509
|
`app_external_list: cannot list "${relPath || "."}": ${err instanceof Error ? err.message : String(err)}`
|
|
22464
20510
|
);
|
|
22465
20511
|
}
|
|
@@ -22479,7 +20525,7 @@ function registerAppExternalTools(server, opts) {
|
|
|
22479
20525
|
entries.push({ name: d.name, isDirectory, size });
|
|
22480
20526
|
}
|
|
22481
20527
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
22482
|
-
return
|
|
20528
|
+
return textResult3({ appId: input.appId, root: input.root, path: relPath, entries });
|
|
22483
20529
|
}
|
|
22484
20530
|
);
|
|
22485
20531
|
server.tool(
|
|
@@ -22492,40 +20538,40 @@ function registerAppExternalTools(server, opts) {
|
|
|
22492
20538
|
},
|
|
22493
20539
|
async (input) => {
|
|
22494
20540
|
const installed = appRegistry.getApp(input.appId);
|
|
22495
|
-
if (!installed) return
|
|
20541
|
+
if (!installed) return errorResult3(`app_external_read: no installed app "${input.appId}".`);
|
|
22496
20542
|
try {
|
|
22497
20543
|
assertRootGranted(installed, input.root);
|
|
22498
20544
|
} catch (err) {
|
|
22499
|
-
return
|
|
20545
|
+
return errorResult3(`app_external_read: ${err instanceof Error ? err.message : String(err)}`);
|
|
22500
20546
|
}
|
|
22501
20547
|
const ext = extname(input.path).toLowerCase();
|
|
22502
20548
|
if (!TEXT_EXTENSIONS.has(ext)) {
|
|
22503
|
-
return
|
|
20549
|
+
return errorResult3(
|
|
22504
20550
|
`app_external_read: "${input.path}" has extension "${ext || "(none)"}", which is not text-ish (allowed: ${[...TEXT_EXTENSIONS].sort().join(", ")}). Use GET /apps/${input.appId}/external-blob?root=${encodeURIComponent(input.root)}&path=${encodeURIComponent(input.path)} to fetch this file's bytes instead.`
|
|
22505
20551
|
);
|
|
22506
20552
|
}
|
|
22507
20553
|
const root = await realpathExternalRoot(input.root);
|
|
22508
|
-
if (!root) return
|
|
20554
|
+
if (!root) return errorResult3(`app_external_read: root "${input.root}" is not accessible.`);
|
|
22509
20555
|
let target;
|
|
22510
20556
|
try {
|
|
22511
20557
|
target = resolveExternalPath(root, input.path);
|
|
22512
20558
|
await assertExternalPathRealInside(root, target);
|
|
22513
20559
|
} catch (err) {
|
|
22514
|
-
return
|
|
20560
|
+
return errorResult3(`app_external_read: ${err instanceof Error ? err.message : String(err)}`);
|
|
22515
20561
|
}
|
|
22516
20562
|
let st;
|
|
22517
20563
|
try {
|
|
22518
20564
|
st = await stat(target);
|
|
22519
20565
|
} catch (err) {
|
|
22520
|
-
return
|
|
20566
|
+
return errorResult3(
|
|
22521
20567
|
`app_external_read: cannot stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
|
|
22522
20568
|
);
|
|
22523
20569
|
}
|
|
22524
20570
|
if (st.isDirectory()) {
|
|
22525
|
-
return
|
|
20571
|
+
return errorResult3(`app_external_read: "${input.path}" is a directory, not a file.`);
|
|
22526
20572
|
}
|
|
22527
20573
|
if (st.size > MAX_TEXT_READ_BYTES) {
|
|
22528
|
-
return
|
|
20574
|
+
return errorResult3(
|
|
22529
20575
|
`app_external_read: "${input.path}" is ${st.size} bytes, over the ${MAX_TEXT_READ_BYTES}-byte text-read cap. Use the external-blob HTTP route instead.`
|
|
22530
20576
|
);
|
|
22531
20577
|
}
|
|
@@ -22533,18 +20579,18 @@ function registerAppExternalTools(server, opts) {
|
|
|
22533
20579
|
try {
|
|
22534
20580
|
raw = await readFile(target, "utf8");
|
|
22535
20581
|
} catch (err) {
|
|
22536
|
-
return
|
|
20582
|
+
return errorResult3(
|
|
22537
20583
|
`app_external_read: cannot read "${input.path}": ${err instanceof Error ? err.message : String(err)}`
|
|
22538
20584
|
);
|
|
22539
20585
|
}
|
|
22540
20586
|
if (ext === ".json") {
|
|
22541
20587
|
try {
|
|
22542
|
-
return
|
|
20588
|
+
return textResult3({ appId: input.appId, root: input.root, path: input.path, content: JSON.parse(raw) });
|
|
22543
20589
|
} catch {
|
|
22544
|
-
return
|
|
20590
|
+
return textResult3({ appId: input.appId, root: input.root, path: input.path, content: raw });
|
|
22545
20591
|
}
|
|
22546
20592
|
}
|
|
22547
|
-
return
|
|
20593
|
+
return textResult3({ appId: input.appId, root: input.root, path: input.path, content: raw });
|
|
22548
20594
|
}
|
|
22549
20595
|
);
|
|
22550
20596
|
}
|
|
@@ -29631,15 +27677,15 @@ async function handleAppExternalBlob(req, res, appId, appRegistry) {
|
|
|
29631
27677
|
reply(403, { error: `root "${root}" is not granted to app "${appId}".` });
|
|
29632
27678
|
return;
|
|
29633
27679
|
}
|
|
29634
|
-
const
|
|
29635
|
-
if (!
|
|
27680
|
+
const safeRoot = await realpathExternalRoot(root);
|
|
27681
|
+
if (!safeRoot) {
|
|
29636
27682
|
reply(404, { error: `root "${root}" is not accessible.` });
|
|
29637
27683
|
return;
|
|
29638
27684
|
}
|
|
29639
27685
|
let target;
|
|
29640
27686
|
try {
|
|
29641
|
-
target = resolveExternalPath(
|
|
29642
|
-
await assertExternalPathRealInside(
|
|
27687
|
+
target = resolveExternalPath(safeRoot, relPath);
|
|
27688
|
+
await assertExternalPathRealInside(safeRoot, target);
|
|
29643
27689
|
} catch (err) {
|
|
29644
27690
|
reply(400, { error: err instanceof Error ? err.message : String(err) });
|
|
29645
27691
|
return;
|
|
@@ -29705,7 +27751,9 @@ async function handleApps(req, res, path, appRegistry, performInstall2, listRegi
|
|
|
29705
27751
|
const scopeId = body?.scopeId ?? "root";
|
|
29706
27752
|
let installed = appRegistry.getApp(appId);
|
|
29707
27753
|
if (!installed && body?.dir) {
|
|
29708
|
-
const installResult = await performInstall2(body.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter
|
|
27754
|
+
const installResult = await performInstall2(body.dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter, {
|
|
27755
|
+
...body.dataDir !== void 0 ? { dataDir: body.dataDir } : {}
|
|
27756
|
+
});
|
|
29709
27757
|
if (!installResult.ok) {
|
|
29710
27758
|
res.writeHead(400, { "content-type": "application/json" });
|
|
29711
27759
|
res.end(JSON.stringify({ error: installResult.error }));
|
|
@@ -30976,261 +29024,6 @@ stderr: ${stderrText.slice(-600)}` : msg);
|
|
|
30976
29024
|
`import "${entry.alias}" has unsupported transport type "${snap.type}"`
|
|
30977
29025
|
);
|
|
30978
29026
|
}
|
|
30979
|
-
var AppPathTraversalError = class extends Error {
|
|
30980
|
-
code = "APP_PATH_TRAVERSAL";
|
|
30981
|
-
constructor(relPath) {
|
|
30982
|
-
super(`app-data: path traversal rejected for "${relPath}" \u2014 must resolve inside the app dir.`);
|
|
30983
|
-
this.name = "AppPathTraversalError";
|
|
30984
|
-
}
|
|
30985
|
-
};
|
|
30986
|
-
function resolveAppDataPath(appDir, relPath) {
|
|
30987
|
-
if (isAbsolute(relPath)) throw new AppPathTraversalError(relPath);
|
|
30988
|
-
if (/^[A-Za-z]:/.test(relPath)) throw new AppPathTraversalError(relPath);
|
|
30989
|
-
const root = resolve(appDir);
|
|
30990
|
-
const target = resolve(appDir, relPath);
|
|
30991
|
-
const rootWithSep = root.endsWith(sep) ? root : root + sep;
|
|
30992
|
-
if (target !== root && !target.startsWith(rootWithSep)) {
|
|
30993
|
-
throw new AppPathTraversalError(relPath);
|
|
30994
|
-
}
|
|
30995
|
-
return target;
|
|
30996
|
-
}
|
|
30997
|
-
function textResult3(body) {
|
|
30998
|
-
return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
|
|
30999
|
-
}
|
|
31000
|
-
function errorResult3(text10) {
|
|
31001
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: text10 }) }], isError: true };
|
|
31002
|
-
}
|
|
31003
|
-
async function safeRoot(appDir) {
|
|
31004
|
-
try {
|
|
31005
|
-
return await realpath(appDir);
|
|
31006
|
-
} catch {
|
|
31007
|
-
return void 0;
|
|
31008
|
-
}
|
|
31009
|
-
}
|
|
31010
|
-
async function assertRealInside(root, target) {
|
|
31011
|
-
let real;
|
|
31012
|
-
try {
|
|
31013
|
-
real = await realpath(target);
|
|
31014
|
-
} catch {
|
|
31015
|
-
return;
|
|
31016
|
-
}
|
|
31017
|
-
const rootWithSep = root.endsWith(sep) ? root : root + sep;
|
|
31018
|
-
if (real !== root && !real.startsWith(rootWithSep)) {
|
|
31019
|
-
throw new AppPathTraversalError(target);
|
|
31020
|
-
}
|
|
31021
|
-
}
|
|
31022
|
-
async function atomicWrite(filePath, data) {
|
|
31023
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
31024
|
-
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
31025
|
-
await writeFile(tmp, data, "utf8");
|
|
31026
|
-
await rename(tmp, filePath);
|
|
31027
|
-
}
|
|
31028
|
-
async function writeRaw(root, rel, data) {
|
|
31029
|
-
await atomicWrite(resolveAppDataPath(root, rel), data);
|
|
31030
|
-
}
|
|
31031
|
-
async function writeJson(root, rel, value) {
|
|
31032
|
-
await writeRaw(root, rel, JSON.stringify(value, null, 2) + "\n");
|
|
31033
|
-
}
|
|
31034
|
-
async function readTextMaybe(path) {
|
|
31035
|
-
try {
|
|
31036
|
-
return await readFile(path, "utf8");
|
|
31037
|
-
} catch {
|
|
31038
|
-
return void 0;
|
|
31039
|
-
}
|
|
31040
|
-
}
|
|
31041
|
-
async function readJsonMaybe(path) {
|
|
31042
|
-
const raw = await readTextMaybe(path);
|
|
31043
|
-
if (raw === void 0) return void 0;
|
|
31044
|
-
try {
|
|
31045
|
-
return JSON.parse(raw);
|
|
31046
|
-
} catch {
|
|
31047
|
-
return void 0;
|
|
31048
|
-
}
|
|
31049
|
-
}
|
|
31050
|
-
function normalizeJob(raw) {
|
|
31051
|
-
const jobId = raw.jobId ?? raw.id;
|
|
31052
|
-
const out = { ...raw, id: jobId, jobId };
|
|
31053
|
-
if (out.applyUrl === void 0 || out.applyUrl === null) out.applyUrl = raw.url;
|
|
31054
|
-
return out;
|
|
31055
|
-
}
|
|
31056
|
-
async function readDossierJobId(dossierDir) {
|
|
31057
|
-
const parsed = await readJsonMaybe(join(dossierDir, "job.json"));
|
|
31058
|
-
if (!parsed || typeof parsed !== "object") return void 0;
|
|
31059
|
-
const jobId = parsed.jobId ?? parsed.id;
|
|
31060
|
-
return typeof jobId === "string" && jobId.length > 0 ? jobId : void 0;
|
|
31061
|
-
}
|
|
31062
|
-
function registerAppDataTools(server, opts) {
|
|
31063
|
-
const { appRegistry } = opts;
|
|
31064
|
-
server.tool(
|
|
31065
|
-
"app_data_read",
|
|
31066
|
-
"Read an app-scoped data file (app-relative path). JSON paths return the parsed value in `content`; everything else returns the raw text. Path traversal outside the app dir is rejected.",
|
|
31067
|
-
{ appId: z.string(), path: z.string().describe("App-relative path under the app's own dir.") },
|
|
31068
|
-
async (input) => {
|
|
31069
|
-
const installed = appRegistry.getApp(input.appId);
|
|
31070
|
-
if (!installed) return errorResult3(`app_data_read: no installed app "${input.appId}".`);
|
|
31071
|
-
const root = await safeRoot(installed.dir);
|
|
31072
|
-
if (!root) return errorResult3(`app_data_read: app dir "${installed.dir}" is not accessible.`);
|
|
31073
|
-
let target;
|
|
31074
|
-
try {
|
|
31075
|
-
target = resolveAppDataPath(root, input.path);
|
|
31076
|
-
await assertRealInside(root, target);
|
|
31077
|
-
} catch (err) {
|
|
31078
|
-
return errorResult3(`app_data_read: ${err instanceof Error ? err.message : String(err)}`);
|
|
31079
|
-
}
|
|
31080
|
-
const raw = await readTextMaybe(target);
|
|
31081
|
-
if (raw === void 0) return textResult3({ appId: input.appId, path: input.path, exists: false });
|
|
31082
|
-
if (input.path.endsWith(".json")) {
|
|
31083
|
-
try {
|
|
31084
|
-
return textResult3({ appId: input.appId, path: input.path, exists: true, content: JSON.parse(raw) });
|
|
31085
|
-
} catch {
|
|
31086
|
-
return textResult3({ appId: input.appId, path: input.path, exists: true, content: raw });
|
|
31087
|
-
}
|
|
31088
|
-
}
|
|
31089
|
-
return textResult3({ appId: input.appId, path: input.path, exists: true, content: raw });
|
|
31090
|
-
}
|
|
31091
|
-
);
|
|
31092
|
-
server.tool(
|
|
31093
|
-
"app_data_write",
|
|
31094
|
-
"Write an app-scoped data file (app-relative path), creating parent directories as needed. `.json` paths are JSON-stringified (pretty); other paths write the raw string passed as `content.text` (or a plain string `content`). Atomic write (tmp + rename). Path traversal outside the app dir is rejected.",
|
|
31095
|
-
{
|
|
31096
|
-
appId: z.string(),
|
|
31097
|
-
path: z.string().describe("App-relative path under the app's own dir."),
|
|
31098
|
-
content: z.unknown().describe("JSON value for `.json` paths, or `{ text }` / string for others.")
|
|
31099
|
-
},
|
|
31100
|
-
async (input) => {
|
|
31101
|
-
const installed = appRegistry.getApp(input.appId);
|
|
31102
|
-
if (!installed) return errorResult3(`app_data_write: no installed app "${input.appId}".`);
|
|
31103
|
-
const root = await safeRoot(installed.dir);
|
|
31104
|
-
if (!root) return errorResult3(`app_data_write: app dir "${installed.dir}" is not accessible.`);
|
|
31105
|
-
let target;
|
|
31106
|
-
try {
|
|
31107
|
-
target = resolveAppDataPath(root, input.path);
|
|
31108
|
-
} catch (err) {
|
|
31109
|
-
return errorResult3(`app_data_write: ${err instanceof Error ? err.message : String(err)}`);
|
|
31110
|
-
}
|
|
31111
|
-
let payload;
|
|
31112
|
-
if (input.path.endsWith(".json")) {
|
|
31113
|
-
payload = JSON.stringify(input.content, null, 2);
|
|
31114
|
-
} else {
|
|
31115
|
-
const raw = input.content;
|
|
31116
|
-
if (typeof raw === "string") payload = raw;
|
|
31117
|
-
else if (raw !== null && typeof raw === "object" && typeof raw.text === "string") {
|
|
31118
|
-
payload = raw.text;
|
|
31119
|
-
} else {
|
|
31120
|
-
payload = JSON.stringify(raw);
|
|
31121
|
-
}
|
|
31122
|
-
}
|
|
31123
|
-
try {
|
|
31124
|
-
await atomicWrite(target, payload);
|
|
31125
|
-
return textResult3({ appId: input.appId, path: input.path, size: Buffer.byteLength(payload, "utf8") });
|
|
31126
|
-
} catch (err) {
|
|
31127
|
-
return errorResult3(`app_data_write: ${err instanceof Error ? err.message : String(err)}`);
|
|
31128
|
-
}
|
|
31129
|
-
}
|
|
31130
|
-
);
|
|
31131
|
-
server.tool(
|
|
31132
|
-
"app_data_list",
|
|
31133
|
-
"List entries (name + type + size) under an app-relative directory (default `.`). A missing directory returns empty entries, not an error. Path traversal outside the app dir is rejected.",
|
|
31134
|
-
{
|
|
31135
|
-
appId: z.string(),
|
|
31136
|
-
dir: z.string().optional().describe("App-relative directory to list. Defaults to `.`.")
|
|
31137
|
-
},
|
|
31138
|
-
async (input) => {
|
|
31139
|
-
const installed = appRegistry.getApp(input.appId);
|
|
31140
|
-
if (!installed) return errorResult3(`app_data_list: no installed app "${input.appId}".`);
|
|
31141
|
-
const root = await safeRoot(installed.dir);
|
|
31142
|
-
if (!root) return errorResult3(`app_data_list: app dir "${installed.dir}" is not accessible.`);
|
|
31143
|
-
const relDir = input.dir ?? ".";
|
|
31144
|
-
let target;
|
|
31145
|
-
try {
|
|
31146
|
-
target = resolveAppDataPath(root, relDir);
|
|
31147
|
-
} catch (err) {
|
|
31148
|
-
return errorResult3(`app_data_list: ${err instanceof Error ? err.message : String(err)}`);
|
|
31149
|
-
}
|
|
31150
|
-
let dirents;
|
|
31151
|
-
try {
|
|
31152
|
-
dirents = await readdir(target, { withFileTypes: true });
|
|
31153
|
-
} catch {
|
|
31154
|
-
return textResult3({ appId: input.appId, dir: relDir, entries: [] });
|
|
31155
|
-
}
|
|
31156
|
-
const entries = [];
|
|
31157
|
-
for (const d of dirents) {
|
|
31158
|
-
const isDirectory = d.isDirectory();
|
|
31159
|
-
let size = 0;
|
|
31160
|
-
if (!isDirectory) {
|
|
31161
|
-
try {
|
|
31162
|
-
size = (await stat(join(target, d.name))).size;
|
|
31163
|
-
} catch {
|
|
31164
|
-
size = 0;
|
|
31165
|
-
}
|
|
31166
|
-
}
|
|
31167
|
-
entries.push({ name: d.name, type: isDirectory ? "directory" : "file", size });
|
|
31168
|
-
}
|
|
31169
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
31170
|
-
return textResult3({ appId: input.appId, dir: relDir, entries });
|
|
31171
|
-
}
|
|
31172
|
-
);
|
|
31173
|
-
server.tool(
|
|
31174
|
-
"app_data_migrate",
|
|
31175
|
-
"One-time import of legacy job-app data into the durable shape under the app dir: `data/jobs/<jobId>.json` (normalized id/jobId/applyUrl), `data/rankings/latest.json` (full ranked list) + per-job ranking artifacts, `applications/<jobId>/{job.json,cv.json,cover.md}` from matching `dossiers/*` folders, and `data/state.json`. Idempotent \u2014 re-running after migration returns `alreadyMigrated` unless `force`.",
|
|
31176
|
-
{
|
|
31177
|
-
appId: z.string(),
|
|
31178
|
-
force: z.boolean().optional().describe("Re-run even if already migrated.")
|
|
31179
|
-
},
|
|
31180
|
-
async (input) => {
|
|
31181
|
-
const installed = appRegistry.getApp(input.appId);
|
|
31182
|
-
if (!installed) return errorResult3(`app_data_migrate: no installed app "${input.appId}".`);
|
|
31183
|
-
const root = await safeRoot(installed.dir);
|
|
31184
|
-
if (!root) return errorResult3(`app_data_migrate: app dir "${installed.dir}" is not accessible.`);
|
|
31185
|
-
const stateRel = "data/state.json";
|
|
31186
|
-
if (!input.force && await readJsonMaybe(resolveAppDataPath(root, stateRel)) !== void 0) {
|
|
31187
|
-
return textResult3({ appId: input.appId, migrated: false, alreadyMigrated: true });
|
|
31188
|
-
}
|
|
31189
|
-
let jobs = [];
|
|
31190
|
-
const rankedRaw = await readJsonMaybe(resolveAppDataPath(root, "ranked-jobs.json"));
|
|
31191
|
-
if (Array.isArray(rankedRaw)) jobs = rankedRaw;
|
|
31192
|
-
const normalized = jobs.map(normalizeJob).filter((j) => typeof j.jobId === "string" && j.jobId.length > 0);
|
|
31193
|
-
for (const job of normalized) {
|
|
31194
|
-
const id = job.jobId;
|
|
31195
|
-
await writeJson(root, `data/jobs/${id}.json`, job);
|
|
31196
|
-
await writeJson(root, `data/rankings/${id}.json`, job);
|
|
31197
|
-
}
|
|
31198
|
-
await writeJson(root, "data/rankings/latest.json", normalized);
|
|
31199
|
-
let folderNames = [];
|
|
31200
|
-
try {
|
|
31201
|
-
folderNames = (await readdir(resolveAppDataPath(root, "dossiers"), { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
31202
|
-
} catch {
|
|
31203
|
-
folderNames = [];
|
|
31204
|
-
}
|
|
31205
|
-
const matched = /* @__PURE__ */ new Set();
|
|
31206
|
-
const skippedFolders = [];
|
|
31207
|
-
for (const name of folderNames) {
|
|
31208
|
-
const dossierDir = resolveAppDataPath(root, join("dossiers", name));
|
|
31209
|
-
const jobId = await readDossierJobId(dossierDir);
|
|
31210
|
-
const targetJob = normalized.find((j) => j.jobId === jobId);
|
|
31211
|
-
if (!jobId || !targetJob) {
|
|
31212
|
-
skippedFolders.push(name);
|
|
31213
|
-
continue;
|
|
31214
|
-
}
|
|
31215
|
-
matched.add(jobId);
|
|
31216
|
-
await writeJson(root, `applications/${jobId}/job.json`, targetJob);
|
|
31217
|
-
const cv = await readJsonMaybe(join(dossierDir, "cv.json"));
|
|
31218
|
-
if (cv !== void 0) await writeJson(root, `applications/${jobId}/cv.json`, cv);
|
|
31219
|
-
const cover = await readTextMaybe(join(dossierDir, "cover.md"));
|
|
31220
|
-
if (cover !== void 0) await writeRaw(root, `applications/${jobId}/cover.md`, cover);
|
|
31221
|
-
}
|
|
31222
|
-
const jobCount = normalized.length;
|
|
31223
|
-
const dossierCount = matched.size;
|
|
31224
|
-
await writeJson(root, stateRel, {
|
|
31225
|
-
migratedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31226
|
-
jobCount,
|
|
31227
|
-
dossierCount,
|
|
31228
|
-
skippedFolders
|
|
31229
|
-
});
|
|
31230
|
-
return textResult3({ appId: input.appId, migrated: true, jobCount, dossierCount, skippedFolders });
|
|
31231
|
-
}
|
|
31232
|
-
);
|
|
31233
|
-
}
|
|
31234
29027
|
function createSessionEventBus() {
|
|
31235
29028
|
const ee = new EventEmitter();
|
|
31236
29029
|
ee.setMaxListeners(100);
|
|
@@ -32134,9 +29927,14 @@ function withDeferredTools(server, opts) {
|
|
|
32134
29927
|
}
|
|
32135
29928
|
|
|
32136
29929
|
// src/pr-provenance-reconciler.ts
|
|
29930
|
+
function prNumberFromUrl(url) {
|
|
29931
|
+
const m = /\/pull\/(\d+)(?:[/?#]|$)/.exec(url);
|
|
29932
|
+
return m ? Number(m[1]) : null;
|
|
29933
|
+
}
|
|
32137
29934
|
var POLL_THROTTLE_MS = 15e3;
|
|
32138
29935
|
function createPrProvenanceReconciler(opts) {
|
|
32139
29936
|
const stampedPrUrls = /* @__PURE__ */ new Set();
|
|
29937
|
+
const costRefreshedPrUrls = /* @__PURE__ */ new Set();
|
|
32140
29938
|
const lastPollAt = /* @__PURE__ */ new Map();
|
|
32141
29939
|
const reconcile = async (sessionId, terminal) => {
|
|
32142
29940
|
const desc = opts.registry.get(sessionId);
|
|
@@ -32177,9 +29975,31 @@ function createPrProvenanceReconciler(opts) {
|
|
|
32177
29975
|
await stamp({ number: record2.createdPrNumber, url: record2.createdPrUrl });
|
|
32178
29976
|
}
|
|
32179
29977
|
const pr = await opts.resolveOpenPr(cwd);
|
|
32180
|
-
if (
|
|
32181
|
-
|
|
32182
|
-
|
|
29978
|
+
if (pr && shouldStamp(pr.url)) await stamp(pr);
|
|
29979
|
+
await refreshCost(desc, supervisor, cwd);
|
|
29980
|
+
};
|
|
29981
|
+
const refreshCost = async (desc, supervisor, cwd) => {
|
|
29982
|
+
if (typeof desc.costUsd !== "number") return;
|
|
29983
|
+
for (const opened of desc.openedPrs ?? []) {
|
|
29984
|
+
if (costRefreshedPrUrls.has(opened.url)) continue;
|
|
29985
|
+
const number = opened.number ?? prNumberFromUrl(opened.url);
|
|
29986
|
+
if (number === null) {
|
|
29987
|
+
costRefreshedPrUrls.add(opened.url);
|
|
29988
|
+
continue;
|
|
29989
|
+
}
|
|
29990
|
+
const outcome = await stampFooterOnPr({
|
|
29991
|
+
registry: opts.registry,
|
|
29992
|
+
session: desc,
|
|
29993
|
+
supervisor,
|
|
29994
|
+
prNumber: number,
|
|
29995
|
+
prUrl: opened.url,
|
|
29996
|
+
cwd,
|
|
29997
|
+
refresh: true,
|
|
29998
|
+
...opts.run ? { run: opts.run } : {},
|
|
29999
|
+
...opts.host ? { host: opts.host } : {}
|
|
30000
|
+
});
|
|
30001
|
+
if (outcome.stamped) costRefreshedPrUrls.add(opened.url);
|
|
30002
|
+
}
|
|
32183
30003
|
};
|
|
32184
30004
|
const safeReconcile = (sessionId, terminal) => {
|
|
32185
30005
|
void reconcile(sessionId, terminal).catch(() => {
|
|
@@ -32198,6 +30018,7 @@ function createPrProvenanceReconciler(opts) {
|
|
|
32198
30018
|
dispose() {
|
|
32199
30019
|
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
32200
30020
|
stampedPrUrls.clear();
|
|
30021
|
+
costRefreshedPrUrls.clear();
|
|
32201
30022
|
lastPollAt.clear();
|
|
32202
30023
|
}
|
|
32203
30024
|
};
|
|
@@ -37012,14 +34833,12 @@ async function createGateway(opts) {
|
|
|
37012
34833
|
};
|
|
37013
34834
|
registerAppPullTools(server, { registry: sessions });
|
|
37014
34835
|
const builtinPanelApps = [
|
|
37015
|
-
|
|
37016
|
-
|
|
37017
|
-
|
|
37018
|
-
|
|
37019
|
-
|
|
37020
|
-
|
|
37021
|
-
// httpBaseUrl = this daemon's own origin (SSE stream + bridge fallback).
|
|
37022
|
-
makeLiveSessionApp({ httpBaseUrl: `http://127.0.0.1:${port}` }),
|
|
34836
|
+
...makeBuiltinPanelApps({
|
|
34837
|
+
listSessions: listSessionsFiltered,
|
|
34838
|
+
// httpBaseUrl = this daemon's own origin (SSE stream + bridge
|
|
34839
|
+
// fallback for the live-session widget).
|
|
34840
|
+
httpBaseUrl: `http://127.0.0.1:${port}`
|
|
34841
|
+
}),
|
|
37023
34842
|
// Same ptyEnabled gate as terminal_start/terminal_input/… in
|
|
37024
34843
|
// session-tools.ts — the panel would be able to open the WS but
|
|
37025
34844
|
// every spawn/attach would fail once node-pty isn't available, so
|