@ericsanchezok/synergy-workbench 0.0.0-stage → 3.1.1
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/AGENTS.md +30 -0
- package/README.md +16 -2
- package/dist/agents.js +11 -0
- package/dist/cli-adapter.js +8 -0
- package/dist/component.js +65 -0
- package/dist/config-schema.js +265 -0
- package/dist/execution/activity.js +78 -0
- package/dist/execution/content.js +184 -0
- package/dist/execution/json-sections.js +231 -0
- package/dist/execution/presentation.js +79 -0
- package/dist/execution/preview.js +13 -0
- package/dist/execution/process.js +125 -0
- package/dist/execution/projection.js +327 -0
- package/dist/execution/query.js +18 -0
- package/dist/execution/routes.js +189 -0
- package/dist/execution/schema.js +322 -0
- package/dist/execution/service.js +1993 -0
- package/dist/features.js +22 -0
- package/dist/http.js +65 -0
- package/dist/performance/analysis.js +330 -0
- package/dist/performance/catalog.js +391 -0
- package/dist/performance/dashboard.js +483 -0
- package/dist/performance/error.js +15 -0
- package/dist/performance/index.js +6 -0
- package/dist/performance/inflight.js +43 -0
- package/dist/performance/projection.js +138 -0
- package/dist/performance/prompt/base.txt +38 -0
- package/dist/performance/prompt/builder.js +21 -0
- package/dist/performance/routes/performance-route.js +439 -0
- package/dist/performance/schema.js +628 -0
- package/dist/performance/timeline.js +98 -0
- package/dist/performance/trace-detail.js +91 -0
- package/dist/presentation/service.js +270 -0
- package/dist/presentation/tools/workbench-view.js +65 -0
- package/dist/presentation/tools.js +10 -0
- package/dist/project/directories.js +363 -0
- package/dist/project/git-health.js +498 -0
- package/dist/project/ide.js +66 -0
- package/dist/project/migration.js +22 -0
- package/dist/project/routes/directories.js +89 -0
- package/dist/project/routes/git.js +30 -0
- package/dist/project/routes/task-defaults.js +42 -0
- package/dist/project/session-health.js +9 -0
- package/dist/project/startup.js +28 -0
- package/dist/project/task-defaults.js +68 -0
- package/dist/project/tools/session-control.js +664 -0
- package/dist/project/tools/session-control.txt +60 -0
- package/dist/project/tools/worktree-archive.js +76 -0
- package/dist/project/tools/worktree-enter.js +175 -0
- package/dist/project/tools/worktree-leave.js +119 -0
- package/dist/project/tools/worktree-list.js +52 -0
- package/dist/project/tools.js +26 -0
- package/dist/project/vcs-branch-watcher.js +76 -0
- package/dist/project/vcs.js +114 -0
- package/dist/project/worktree-janitor.js +140 -0
- package/dist/project/worktrees.js +98 -0
- package/dist/push/bridge.js +171 -0
- package/dist/push/index.js +4 -0
- package/dist/push/routes/push.js +224 -0
- package/dist/push/service.js +93 -0
- package/dist/push/store.js +121 -0
- package/dist/push/types.js +58 -0
- package/dist/review/git.js +226 -0
- package/dist/review/routes.js +45 -0
- package/dist/review/schema.js +32 -0
- package/dist/review/state.js +79 -0
- package/dist/stats/aggregator.js +252 -0
- package/dist/stats/canonical.js +89 -0
- package/dist/stats/cli/stats.js +302 -0
- package/dist/stats/engine.js +176 -0
- package/dist/stats/index.js +5 -0
- package/dist/stats/rollup.js +460 -0
- package/dist/stats/routes/stats.js +94 -0
- package/dist/stats/routes/usage.js +83 -0
- package/dist/stats/storage.js +86 -0
- package/dist/stats/types.js +267 -0
- package/dist/worker.js +6 -0
- package/package.json +232 -4
- package/src/agents.ts +15 -0
- package/src/cli-adapter.ts +11 -0
- package/src/component.ts +73 -0
- package/src/config-schema.ts +289 -0
- package/src/execution/activity.ts +83 -0
- package/src/execution/content.ts +189 -0
- package/src/execution/json-sections.ts +230 -0
- package/src/execution/presentation.ts +91 -0
- package/src/execution/preview.ts +12 -0
- package/src/execution/process.ts +118 -0
- package/src/execution/projection.ts +330 -0
- package/src/execution/query.ts +19 -0
- package/src/execution/routes.ts +324 -0
- package/src/execution/schema.ts +336 -0
- package/src/execution/service.ts +2149 -0
- package/src/features.ts +22 -0
- package/src/http.ts +75 -0
- package/src/performance/analysis.ts +356 -0
- package/src/performance/catalog.ts +902 -0
- package/src/performance/dashboard.ts +521 -0
- package/src/performance/error.ts +32 -0
- package/src/performance/index.ts +6 -0
- package/src/performance/inflight.ts +46 -0
- package/src/performance/projection.ts +143 -0
- package/src/performance/prompt/base.txt +38 -0
- package/src/performance/prompt/builder.ts +24 -0
- package/src/performance/routes/performance-route.ts +541 -0
- package/src/performance/schema.ts +695 -0
- package/src/performance/timeline.ts +100 -0
- package/src/performance/trace-detail.ts +97 -0
- package/src/presentation/service.ts +303 -0
- package/src/presentation/tools/workbench-view.ts +75 -0
- package/src/presentation/tools.ts +11 -0
- package/src/project/directories.ts +391 -0
- package/src/project/git-health.ts +577 -0
- package/src/project/ide.ts +80 -0
- package/src/project/migration.ts +24 -0
- package/src/project/routes/directories.ts +124 -0
- package/src/project/routes/git.ts +42 -0
- package/src/project/routes/task-defaults.ts +50 -0
- package/src/project/session-health.ts +10 -0
- package/src/project/startup.ts +28 -0
- package/src/project/task-defaults.ts +70 -0
- package/src/project/tools/session-control.ts +725 -0
- package/src/project/tools/session-control.txt +60 -0
- package/src/project/tools/worktree-archive.ts +92 -0
- package/src/project/tools/worktree-enter.ts +227 -0
- package/src/project/tools/worktree-leave.ts +140 -0
- package/src/project/tools/worktree-list.ts +68 -0
- package/src/project/tools.ts +29 -0
- package/src/project/vcs-branch-watcher.ts +81 -0
- package/src/project/vcs.ts +124 -0
- package/src/project/worktree-janitor.ts +153 -0
- package/src/project/worktrees.ts +110 -0
- package/src/push/bridge.ts +182 -0
- package/src/push/index.ts +4 -0
- package/src/push/routes/push.ts +267 -0
- package/src/push/service.ts +108 -0
- package/src/push/store.ts +120 -0
- package/src/push/types.ts +67 -0
- package/src/review/git.ts +221 -0
- package/src/review/routes.ts +65 -0
- package/src/review/schema.ts +34 -0
- package/src/review/state.ts +77 -0
- package/src/stats/aggregator.ts +277 -0
- package/src/stats/canonical.ts +93 -0
- package/src/stats/cli/stats.ts +348 -0
- package/src/stats/engine.ts +197 -0
- package/src/stats/index.ts +5 -0
- package/src/stats/rollup.ts +522 -0
- package/src/stats/routes/stats.ts +107 -0
- package/src/stats/routes/usage.ts +103 -0
- package/src/stats/storage.ts +78 -0
- package/src/stats/types.ts +351 -0
- package/src/worker.ts +7 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ObservabilityStore } from "@ericsanchezok/synergy-harness/observability/store";
|
|
2
|
+
import { PerformanceCatalog } from "./catalog.js";
|
|
3
|
+
import { ObservabilityConfig } from "@ericsanchezok/synergy-harness/observability/config";
|
|
4
|
+
import { PerformanceError } from "./error.js";
|
|
5
|
+
import { PerformanceSchema } from "./schema.js";
|
|
6
|
+
export var PerformanceTimeline;
|
|
7
|
+
(function (PerformanceTimeline) {
|
|
8
|
+
async function get(query) {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
const to = parseTime(query.to, now);
|
|
11
|
+
const from = parseTime(query.from, query.windowMs ? to - query.windowMs : to - 15 * 60 * 1000);
|
|
12
|
+
if (from >= to)
|
|
13
|
+
throw new PerformanceError("PERF_INVALID_QUERY", "Timeline from must be before to.", 400);
|
|
14
|
+
const config = ObservabilityConfig.current();
|
|
15
|
+
const bucketMs = query.bucketMs ?? Math.max(1000, Math.floor((to - from) / config.maxTimelineBuckets) + 1);
|
|
16
|
+
const bucketCount = Math.floor((to - from) / bucketMs) + 1;
|
|
17
|
+
if (bucketCount > config.maxTimelineBuckets) {
|
|
18
|
+
throw new PerformanceError("PERF_TOO_MANY_BUCKETS", "Timeline query exceeds the configured bucket limit.", 400, {
|
|
19
|
+
maxBuckets: config.maxTimelineBuckets,
|
|
20
|
+
bucketCount,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const metrics = normalizeMetrics(query.metric);
|
|
24
|
+
const buckets = bucketStarts(from, to, bucketMs);
|
|
25
|
+
const series = await Promise.all(metrics.map(async (name) => {
|
|
26
|
+
const info = PerformanceCatalog.get(name);
|
|
27
|
+
if (!info)
|
|
28
|
+
throw new PerformanceError("PERF_INVALID_QUERY", "Timeline metric is not allowed.", 400);
|
|
29
|
+
const stat = query.stat ?? info.defaultStat;
|
|
30
|
+
const rows = await ObservabilityStore.queryMetricBuckets({
|
|
31
|
+
since: from,
|
|
32
|
+
until: to,
|
|
33
|
+
names: [name],
|
|
34
|
+
module: query.module,
|
|
35
|
+
scopeID: query.scopeID,
|
|
36
|
+
sessionID: query.sessionID,
|
|
37
|
+
tool: query.tool,
|
|
38
|
+
providerID: query.providerID,
|
|
39
|
+
bucketMs,
|
|
40
|
+
});
|
|
41
|
+
const bucketed = new Map(rows.map((row) => [row.bucket, row]));
|
|
42
|
+
const points = buckets.map((time, index) => {
|
|
43
|
+
const bucket = bucketed.get(index);
|
|
44
|
+
return {
|
|
45
|
+
time,
|
|
46
|
+
value: bucket ? (stat === "rate" ? bucket.sum / Math.max(1, bucketMs / 1000) : bucket[stat]) : null,
|
|
47
|
+
sampleCount: bucket?.count ?? 0,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
const sampleCount = points.reduce((total, point) => total + (point.sampleCount ?? 0), 0);
|
|
51
|
+
return PerformanceSchema.TimelineSeries.parse({
|
|
52
|
+
name,
|
|
53
|
+
label: info.label,
|
|
54
|
+
unit: info.unit,
|
|
55
|
+
kind: info.kind,
|
|
56
|
+
stat,
|
|
57
|
+
sampleCount,
|
|
58
|
+
module: info.module,
|
|
59
|
+
source: info.source,
|
|
60
|
+
points,
|
|
61
|
+
});
|
|
62
|
+
}));
|
|
63
|
+
return PerformanceSchema.Timeline.parse({
|
|
64
|
+
generatedAt: new Date().toISOString(),
|
|
65
|
+
from,
|
|
66
|
+
to,
|
|
67
|
+
bucketMs,
|
|
68
|
+
series,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
PerformanceTimeline.get = get;
|
|
72
|
+
function parseTime(value, fallback) {
|
|
73
|
+
if (!value)
|
|
74
|
+
return fallback;
|
|
75
|
+
const parsed = Date.parse(value);
|
|
76
|
+
if (!Number.isFinite(parsed))
|
|
77
|
+
throw new PerformanceError("PERF_INVALID_QUERY", "Invalid timeline time range.", 400);
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
function normalizeMetrics(metric) {
|
|
81
|
+
const requested = Array.isArray(metric) ? metric : metric ? [metric] : PerformanceCatalog.defaultMetricNames;
|
|
82
|
+
const resolved = requested.map((name) => PerformanceCatalog.resolveName(name));
|
|
83
|
+
const unique = [...new Set(resolved)];
|
|
84
|
+
const invalid = unique.filter((name) => !PerformanceCatalog.get(name));
|
|
85
|
+
if (invalid.length)
|
|
86
|
+
throw new PerformanceError("PERF_INVALID_QUERY", "Timeline metric is not allowed.", 400, {
|
|
87
|
+
invalidMetrics: invalid,
|
|
88
|
+
});
|
|
89
|
+
return unique;
|
|
90
|
+
}
|
|
91
|
+
function bucketStarts(from, to, bucketMs) {
|
|
92
|
+
const buckets = [];
|
|
93
|
+
for (let bucket = from; bucket <= to; bucket += bucketMs)
|
|
94
|
+
buckets.push(bucket);
|
|
95
|
+
return buckets;
|
|
96
|
+
}
|
|
97
|
+
PerformanceTimeline.allowedMetricNames = PerformanceCatalog.allMetricNames();
|
|
98
|
+
})(PerformanceTimeline || (PerformanceTimeline = {}));
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Observability } from "@ericsanchezok/synergy-harness/observability";
|
|
2
|
+
import { ObservabilityStore } from "@ericsanchezok/synergy-harness/observability/store";
|
|
3
|
+
import { PerformanceError } from "./error.js";
|
|
4
|
+
import { PerformanceProjection } from "./projection.js";
|
|
5
|
+
import { PerformanceSchema } from "./schema.js";
|
|
6
|
+
import { parseJson } from "@ericsanchezok/synergy-harness/util/json-parse";
|
|
7
|
+
export var PerformanceTraceDetail;
|
|
8
|
+
(function (PerformanceTraceDetail) {
|
|
9
|
+
function list(query) {
|
|
10
|
+
const since = query.from ? Date.parse(query.from) : Date.now() - 15 * 60 * 1000;
|
|
11
|
+
const until = query.to ? Date.parse(query.to) : undefined;
|
|
12
|
+
const baseLimit = query.limit ?? 50;
|
|
13
|
+
const rows = ObservabilityStore.querySpans({
|
|
14
|
+
since: Number.isFinite(since) ? since : undefined,
|
|
15
|
+
until: until !== undefined && Number.isFinite(until) ? until : undefined,
|
|
16
|
+
limit: baseLimit,
|
|
17
|
+
minDurationMs: query.minDurationMs,
|
|
18
|
+
status: query.status,
|
|
19
|
+
scopeID: query.scopeID,
|
|
20
|
+
sessionID: query.sessionID,
|
|
21
|
+
kinds: query.kind ? PerformanceProjection.spanKinds(query.kind) : undefined,
|
|
22
|
+
distinctTrace: true,
|
|
23
|
+
});
|
|
24
|
+
return PerformanceSchema.TraceList.parse({
|
|
25
|
+
generatedAt: new Date().toISOString(),
|
|
26
|
+
items: rows.map(PerformanceProjection.traceRow),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
PerformanceTraceDetail.list = list;
|
|
30
|
+
async function detail(traceId, opts = {}) {
|
|
31
|
+
const spans = ObservabilityStore.querySpans({ traceId, limit: 10_000 }).sort((a, b) => a.start_time - b.start_time);
|
|
32
|
+
const events = opts.includeEvents === false
|
|
33
|
+
? []
|
|
34
|
+
: (await Observability.query({ traceId, limit: opts.maxEvents ?? 500 })).map(projectEvent);
|
|
35
|
+
if (!spans.length && !events.length)
|
|
36
|
+
throw new PerformanceError("PERF_TRACE_NOT_FOUND", "Performance trace was not found.", 404, { traceId });
|
|
37
|
+
const parsed = spans.map((row) => PerformanceSchema.Span.parse({
|
|
38
|
+
traceId: row.trace_id,
|
|
39
|
+
correlationId: row.correlation_id ?? undefined,
|
|
40
|
+
spanId: row.span_id,
|
|
41
|
+
parentSpanId: row.parent_span_id ?? undefined,
|
|
42
|
+
kind: row.kind,
|
|
43
|
+
name: row.name,
|
|
44
|
+
module: row.module,
|
|
45
|
+
source: row.source,
|
|
46
|
+
startTime: row.start_time,
|
|
47
|
+
endTime: row.end_time ?? undefined,
|
|
48
|
+
durationMs: row.duration_ms ?? undefined,
|
|
49
|
+
status: row.status,
|
|
50
|
+
lastActivityTime: row.last_activity_time ?? undefined,
|
|
51
|
+
heartbeatTime: row.heartbeat_time ?? undefined,
|
|
52
|
+
heartbeatCount: row.heartbeat_count ?? undefined,
|
|
53
|
+
stalled: row.stalled ? true : undefined,
|
|
54
|
+
errorCode: row.error_code ?? undefined,
|
|
55
|
+
errorMessage: row.error_message ?? undefined,
|
|
56
|
+
sessionID: row.session_id ?? undefined,
|
|
57
|
+
scopeID: row.scope_id ?? undefined,
|
|
58
|
+
rid: row.rid ?? undefined,
|
|
59
|
+
tool: row.tool ?? undefined,
|
|
60
|
+
attributes: opts.includeAttributes === false ? {} : parseJson(row.attributes_json),
|
|
61
|
+
}));
|
|
62
|
+
return PerformanceSchema.TraceDetail.parse({
|
|
63
|
+
generatedAt: new Date().toISOString(),
|
|
64
|
+
traceId,
|
|
65
|
+
root: parsed.find((span) => !span.parentSpanId) ?? parsed[0],
|
|
66
|
+
spans: parsed,
|
|
67
|
+
events,
|
|
68
|
+
redaction: { applied: true, omittedAttributes: opts.includeAttributes === false ? parsed.length : 0 },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
PerformanceTraceDetail.detail = detail;
|
|
72
|
+
function projectEvent(event) {
|
|
73
|
+
return PerformanceSchema.TraceEvent.parse({
|
|
74
|
+
time: event.time,
|
|
75
|
+
iso: event.iso,
|
|
76
|
+
type: event.type,
|
|
77
|
+
level: event.level,
|
|
78
|
+
traceId: event.traceId,
|
|
79
|
+
correlationId: event.correlationId,
|
|
80
|
+
sessionID: event.sessionID,
|
|
81
|
+
messageID: event.messageID,
|
|
82
|
+
callID: event.callID,
|
|
83
|
+
rid: event.rid,
|
|
84
|
+
tool: event.tool,
|
|
85
|
+
processId: event.processId,
|
|
86
|
+
pid: event.pid,
|
|
87
|
+
dataKeys: event.data && typeof event.data === "object" ? Object.keys(event.data).slice(0, 24) : [],
|
|
88
|
+
redactionApplied: event.redaction.applied,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
})(PerformanceTraceDetail || (PerformanceTraceDetail = {}));
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { RuntimeContext } from "@ericsanchezok/synergy-harness/lifecycle/context";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { ResourceReference } from "@ericsanchezok/synergy-util/resource-reference";
|
|
4
|
+
import { WorkbenchView } from "@ericsanchezok/synergy-util/workbench-view";
|
|
5
|
+
export var WorkbenchPresentation;
|
|
6
|
+
(function (WorkbenchPresentation) {
|
|
7
|
+
WorkbenchPresentation.DEFAULT_TIMEOUT_SECONDS = 30;
|
|
8
|
+
const state = RuntimeContext.state(() => ({
|
|
9
|
+
clients: new Map(),
|
|
10
|
+
attempts: new Map(),
|
|
11
|
+
disposed: false,
|
|
12
|
+
}));
|
|
13
|
+
const key = (caller) => JSON.stringify([caller.scopeID, caller.sessionID, caller.requestId]);
|
|
14
|
+
const unknown = (reason) => ({ status: "unknown", reason });
|
|
15
|
+
function targetIdentity(target) {
|
|
16
|
+
const id = target.kind === "note"
|
|
17
|
+
? target.id
|
|
18
|
+
: target.kind === "workspace-file"
|
|
19
|
+
? target.path
|
|
20
|
+
: target.kind === "browser-page"
|
|
21
|
+
? target.pageId
|
|
22
|
+
: target.url;
|
|
23
|
+
return JSON.stringify([target.kind, id]);
|
|
24
|
+
}
|
|
25
|
+
function init() {
|
|
26
|
+
const instance = state();
|
|
27
|
+
return () => {
|
|
28
|
+
if (instance.disposed)
|
|
29
|
+
return;
|
|
30
|
+
instance.disposed = true;
|
|
31
|
+
for (const client of instance.clients.values()) {
|
|
32
|
+
for (const attempt of client.attempts.values())
|
|
33
|
+
attempt.cancel?.("The Runtime is closing; no receipt confirms the effect.");
|
|
34
|
+
client.attempts.clear();
|
|
35
|
+
client.views.clear();
|
|
36
|
+
}
|
|
37
|
+
instance.clients.clear();
|
|
38
|
+
instance.attempts.clear();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
WorkbenchPresentation.init = init;
|
|
42
|
+
function contribution() {
|
|
43
|
+
const instance = state();
|
|
44
|
+
return {
|
|
45
|
+
connect({ clientId, send }) {
|
|
46
|
+
const client = { clientId, send, attempts: new Map(), views: new Map() };
|
|
47
|
+
if (!instance.disposed)
|
|
48
|
+
instance.clients.set(clientId, client);
|
|
49
|
+
let disposed = instance.disposed;
|
|
50
|
+
return {
|
|
51
|
+
receive(payload) {
|
|
52
|
+
if (disposed || instance.disposed)
|
|
53
|
+
return;
|
|
54
|
+
const parsed = WorkbenchView.ClientMessage.safeParse(payload);
|
|
55
|
+
if (!parsed.success)
|
|
56
|
+
return;
|
|
57
|
+
const message = parsed.data;
|
|
58
|
+
if (message.type === "client.workbench.ready") {
|
|
59
|
+
client.targets = new Set(message.properties.targets);
|
|
60
|
+
for (const attempt of client.attempts.values())
|
|
61
|
+
if (attempt.command?.action === "open" && !client.targets.has(attempt.command.target.kind))
|
|
62
|
+
attempt.cancel?.("The frontend no longer supports this target; no receipt confirms the effect.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!client.active)
|
|
66
|
+
return;
|
|
67
|
+
const attempt = client.attempts.get(key({ ...client.active, requestId: message.properties.requestId }));
|
|
68
|
+
if (!attempt?.settle)
|
|
69
|
+
return;
|
|
70
|
+
const outcome = message.properties.outcome;
|
|
71
|
+
if (outcome.status === "applied") {
|
|
72
|
+
const views = outcome.views ?? [];
|
|
73
|
+
if (new Set(views.map((view) => view.viewId)).size !== views.length)
|
|
74
|
+
return;
|
|
75
|
+
const command = attempt.command;
|
|
76
|
+
if (!command)
|
|
77
|
+
return;
|
|
78
|
+
const target = attempt.target;
|
|
79
|
+
if (command.action === "open" &&
|
|
80
|
+
(!target ||
|
|
81
|
+
!views.some((view) => view.visible && targetIdentity(view.target) === targetIdentity(target))))
|
|
82
|
+
return;
|
|
83
|
+
if (command.action === "hide" &&
|
|
84
|
+
(!target ||
|
|
85
|
+
!views.some((view) => view.viewId === command.viewId &&
|
|
86
|
+
!view.visible &&
|
|
87
|
+
targetIdentity(view.target) === targetIdentity(target))))
|
|
88
|
+
return;
|
|
89
|
+
if (command.action === "status")
|
|
90
|
+
client.views.clear();
|
|
91
|
+
for (const view of views)
|
|
92
|
+
client.views.set(view.viewId, view);
|
|
93
|
+
}
|
|
94
|
+
attempt.receipt = true;
|
|
95
|
+
attempt.settle(outcome);
|
|
96
|
+
},
|
|
97
|
+
active(active) {
|
|
98
|
+
if (disposed || instance.disposed)
|
|
99
|
+
return;
|
|
100
|
+
const previous = client.active;
|
|
101
|
+
client.active = active;
|
|
102
|
+
if (previous?.scopeID === active?.scopeID &&
|
|
103
|
+
previous?.sessionID === active?.sessionID &&
|
|
104
|
+
previous?.generation === active?.generation)
|
|
105
|
+
return;
|
|
106
|
+
for (const attempt of client.attempts.values())
|
|
107
|
+
attempt.cancel?.("The frontend changed its active view; no receipt confirms the effect.");
|
|
108
|
+
client.views.clear();
|
|
109
|
+
},
|
|
110
|
+
dispose() {
|
|
111
|
+
if (disposed)
|
|
112
|
+
return;
|
|
113
|
+
disposed = true;
|
|
114
|
+
instance.clients.delete(clientId);
|
|
115
|
+
for (const attempt of client.attempts.values()) {
|
|
116
|
+
attempt.settle?.(unknown("The frontend disconnected; no receipt confirms the effect."));
|
|
117
|
+
attempt.receipt = false;
|
|
118
|
+
attempt.result = {
|
|
119
|
+
requestId: attempt.caller.requestId,
|
|
120
|
+
...unknown("The original frontend connection is gone; its receipt is no longer retained."),
|
|
121
|
+
};
|
|
122
|
+
attempt.promise = Promise.resolve(attempt.result);
|
|
123
|
+
}
|
|
124
|
+
client.attempts.clear();
|
|
125
|
+
client.views.clear();
|
|
126
|
+
client.targets = undefined;
|
|
127
|
+
client.active = undefined;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
WorkbenchPresentation.contribution = contribution;
|
|
134
|
+
function request(parameters, caller) {
|
|
135
|
+
const instance = state();
|
|
136
|
+
const { input, clientId } = parameters;
|
|
137
|
+
if (input.action === "status" && input.requestId) {
|
|
138
|
+
const attempt = instance.attempts.get(key({ ...caller, requestId: input.requestId }));
|
|
139
|
+
return Promise.resolve(attempt?.receipt && attempt.result
|
|
140
|
+
? attempt.result
|
|
141
|
+
: { requestId: input.requestId, ...unknown("No confirmed receipt is retained for this Scope and Session.") });
|
|
142
|
+
}
|
|
143
|
+
const signature = createHash("sha256")
|
|
144
|
+
.update(JSON.stringify({ parameters, context: caller.context }))
|
|
145
|
+
.digest("hex");
|
|
146
|
+
const identity = { scopeID: caller.scopeID, sessionID: caller.sessionID, requestId: caller.requestId };
|
|
147
|
+
const existing = instance.attempts.get(key(caller));
|
|
148
|
+
if (existing)
|
|
149
|
+
return existing.signature === signature
|
|
150
|
+
? existing.result
|
|
151
|
+
? Promise.resolve(existing.result)
|
|
152
|
+
: existing.promise
|
|
153
|
+
: Promise.resolve({
|
|
154
|
+
requestId: caller.requestId,
|
|
155
|
+
status: "blocked",
|
|
156
|
+
reason: "This tool call identity already has different arguments; it cannot be reused.",
|
|
157
|
+
});
|
|
158
|
+
const immediate = (outcome) => {
|
|
159
|
+
const result = { requestId: caller.requestId, ...outcome };
|
|
160
|
+
const promise = Promise.resolve(result);
|
|
161
|
+
if (!instance.disposed)
|
|
162
|
+
instance.attempts.set(key(caller), { caller: identity, signature, promise, result, receipt: false });
|
|
163
|
+
return promise;
|
|
164
|
+
};
|
|
165
|
+
if (caller.signal.aborted)
|
|
166
|
+
return immediate({ status: "cancelled", reason: "Cancelled before dispatch." });
|
|
167
|
+
if (instance.disposed)
|
|
168
|
+
return immediate({ status: "unavailable", reason: "The Workbench presentation service is closed." });
|
|
169
|
+
if (input.action === "open" && input.target.kind === "workspace-file" && caller.context?.state !== "bound")
|
|
170
|
+
return immediate({
|
|
171
|
+
status: "blocked",
|
|
172
|
+
reason: "Opening a Workspace file requires the caller Session's captured valid Workspace; a viewer's Workspace cannot be used.",
|
|
173
|
+
});
|
|
174
|
+
let target = input.action === "open" ? input.target : undefined;
|
|
175
|
+
if (target?.kind === "workspace-file") {
|
|
176
|
+
const relative = ResourceReference.resolvePath(target.path, caller.context ?? { state: "none" });
|
|
177
|
+
if (!relative)
|
|
178
|
+
return immediate({ status: "blocked", reason: "The file is not inside the caller's captured Workspace." });
|
|
179
|
+
target = { ...target, path: relative };
|
|
180
|
+
}
|
|
181
|
+
const clients = [...instance.clients.values()].filter((client) => client.active?.scopeID === caller.scopeID &&
|
|
182
|
+
client.active.sessionID === caller.sessionID &&
|
|
183
|
+
client.targets?.size &&
|
|
184
|
+
(input.action !== "open" || client.targets.has(input.target.kind)));
|
|
185
|
+
const selected = clientId
|
|
186
|
+
? clients.find((client) => client.clientId === clientId)
|
|
187
|
+
: clients.length === 1
|
|
188
|
+
? clients[0]
|
|
189
|
+
: undefined;
|
|
190
|
+
if (!selected) {
|
|
191
|
+
if (!clientId && clients.length > 1)
|
|
192
|
+
return immediate({
|
|
193
|
+
status: "ambiguous",
|
|
194
|
+
reason: "Multiple eligible frontends show this Session. Choose a returned clientId; nothing was dispatched.",
|
|
195
|
+
clients: clients.map((client) => ({ clientId: client.clientId })),
|
|
196
|
+
});
|
|
197
|
+
return immediate({
|
|
198
|
+
status: "unavailable",
|
|
199
|
+
reason: clientId
|
|
200
|
+
? "The selected clientId is not a ready, supported frontend actively showing this Scope and Session."
|
|
201
|
+
: "No ready frontend supporting this target is actively showing this Scope and Session.",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (input.action === "hide")
|
|
205
|
+
target = selected.views.get(input.viewId)?.target;
|
|
206
|
+
if (input.action === "hide" && !target)
|
|
207
|
+
return immediate({
|
|
208
|
+
status: "blocked",
|
|
209
|
+
reason: "The selected frontend has no confirmed view with this viewId. Query current views before hiding it.",
|
|
210
|
+
});
|
|
211
|
+
let resolve;
|
|
212
|
+
const promise = new Promise((done) => {
|
|
213
|
+
resolve = done;
|
|
214
|
+
});
|
|
215
|
+
const attempt = { caller: identity, command: input, target, signature, promise, receipt: false };
|
|
216
|
+
instance.attempts.set(key(caller), attempt);
|
|
217
|
+
selected.attempts.set(key(caller), attempt);
|
|
218
|
+
let timer;
|
|
219
|
+
const aborted = () => attempt.cancel?.("Cancelled after dispatch; without a frontend receipt the effect is unknown.");
|
|
220
|
+
attempt.settle = (outcome) => {
|
|
221
|
+
if (!attempt.settle)
|
|
222
|
+
return;
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
caller.signal.removeEventListener("abort", aborted);
|
|
225
|
+
attempt.settle = undefined;
|
|
226
|
+
attempt.cancel = undefined;
|
|
227
|
+
attempt.command = undefined;
|
|
228
|
+
attempt.target = undefined;
|
|
229
|
+
attempt.result = { requestId: caller.requestId, clientId: selected.clientId, ...outcome };
|
|
230
|
+
resolve(attempt.result);
|
|
231
|
+
};
|
|
232
|
+
attempt.cancel = (reason) => {
|
|
233
|
+
attempt.settle?.(unknown(reason));
|
|
234
|
+
selected.send({
|
|
235
|
+
scopeID: caller.scopeID,
|
|
236
|
+
payload: { type: "server.workbench.cancel", properties: { requestId: caller.requestId } },
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
caller.signal.addEventListener("abort", aborted, { once: true });
|
|
240
|
+
const deadline = performance.now() + (parameters.timeoutSeconds ?? WorkbenchPresentation.DEFAULT_TIMEOUT_SECONDS) * 1000;
|
|
241
|
+
const wait = () => {
|
|
242
|
+
const remaining = deadline - performance.now();
|
|
243
|
+
if (remaining <= 0) {
|
|
244
|
+
attempt.cancel?.("The frontend receipt window expired; the effect is unknown. Check status without replaying the command.");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
timer = setTimeout(wait, Math.min(remaining, 2_147_483_647));
|
|
248
|
+
timer.unref();
|
|
249
|
+
};
|
|
250
|
+
wait();
|
|
251
|
+
if (!attempt.settle)
|
|
252
|
+
return promise;
|
|
253
|
+
if (!selected.send({
|
|
254
|
+
scopeID: caller.scopeID,
|
|
255
|
+
payload: {
|
|
256
|
+
type: "server.workbench.request",
|
|
257
|
+
properties: {
|
|
258
|
+
requestId: caller.requestId,
|
|
259
|
+
scopeID: caller.scopeID,
|
|
260
|
+
sessionID: caller.sessionID,
|
|
261
|
+
command: input,
|
|
262
|
+
...(caller.context && { context: caller.context }),
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
}))
|
|
266
|
+
attempt.cancel?.("The request could not be confirmed as sent; without a frontend receipt the effect is unknown.");
|
|
267
|
+
return promise;
|
|
268
|
+
}
|
|
269
|
+
WorkbenchPresentation.request = request;
|
|
270
|
+
})(WorkbenchPresentation || (WorkbenchPresentation = {}));
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { ScopeContext } from "@ericsanchezok/synergy-harness/scope/context";
|
|
3
|
+
import { Session } from "@ericsanchezok/synergy-harness/session";
|
|
4
|
+
import { Tool } from "@ericsanchezok/synergy-harness/tool/tool";
|
|
5
|
+
import { ResourceReference } from "@ericsanchezok/synergy-util/resource-reference";
|
|
6
|
+
import { FileView } from "@ericsanchezok/synergy-local-runtime/file/view";
|
|
7
|
+
import { WorkbenchView } from "@ericsanchezok/synergy-util/workbench-view";
|
|
8
|
+
import { WorkbenchPresentation } from "../service.js";
|
|
9
|
+
export const WorkbenchViewTool = Tool.define("workbench_view", {
|
|
10
|
+
description: "Show an existing note, Workspace file, Browser page or immutable asset in a frontend actively viewing this Session; hide a returned viewId or inspect views. This only presents existing resources: it does not create resources, navigate Browser pages or perform arbitrary UI actions. With multiple eligible frontends, choose a returned clientId. Applied means the frontend confirmed presentation, not that loading content is ready. Waits 30 seconds for a receipt by default; timeout, disconnection or cancellation after dispatch may leave the effect unknown. Use status with the returned requestId to read the retained runtime receipt without replay. Reconnecting and conversation history never replay commands.",
|
|
11
|
+
parameters: WorkbenchView.Parameters,
|
|
12
|
+
resourceNeeds(args) {
|
|
13
|
+
const parsed = WorkbenchView.Parameters.safeParse(args);
|
|
14
|
+
return {
|
|
15
|
+
workspace: parsed.success && parsed.data.input.action === "open" && parsed.data.input.target.kind === "workspace-file",
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
async execute(parameters, ctx) {
|
|
19
|
+
const scopeID = ScopeContext.current.scope.id;
|
|
20
|
+
const requestId = ctx.messageID && ctx.callID
|
|
21
|
+
? `wvr_${createHash("sha256")
|
|
22
|
+
.update(JSON.stringify([scopeID, ctx.sessionID, ctx.messageID, ctx.callID]))
|
|
23
|
+
.digest("hex")}`
|
|
24
|
+
: `wvr_${crypto.randomUUID()}`;
|
|
25
|
+
let context;
|
|
26
|
+
if (parameters.input.action === "open" && parameters.input.target.kind === "workspace-file") {
|
|
27
|
+
const session = await Session.get(ctx.sessionID);
|
|
28
|
+
const selected = ctx.resources?.workspace;
|
|
29
|
+
if (session.scope.id === scopeID &&
|
|
30
|
+
selected?.scopeID === scopeID &&
|
|
31
|
+
selected.id === session.workspaceID &&
|
|
32
|
+
(!session.workspace || selected.binding.generation === session.workspace.generation))
|
|
33
|
+
context = ResourceReference.capture({
|
|
34
|
+
id: selected.id,
|
|
35
|
+
generation: selected.binding.generation,
|
|
36
|
+
path: ctx.resources?.directory ?? selected.binding.path ?? "",
|
|
37
|
+
});
|
|
38
|
+
context ??= { state: "none" };
|
|
39
|
+
if (context.state === "bound") {
|
|
40
|
+
const relative = ResourceReference.resolvePath(parameters.input.target.path, context);
|
|
41
|
+
if (relative)
|
|
42
|
+
await ctx.ask({
|
|
43
|
+
permission: "read",
|
|
44
|
+
patterns: [FileView.resolve(relative, context.workspace.root)],
|
|
45
|
+
metadata: {},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const result = await WorkbenchPresentation.request(parameters, {
|
|
50
|
+
scopeID,
|
|
51
|
+
sessionID: ctx.sessionID,
|
|
52
|
+
requestId,
|
|
53
|
+
context,
|
|
54
|
+
signal: ctx.abort,
|
|
55
|
+
});
|
|
56
|
+
return { title: `Workbench: ${result.status}`, metadata: { ...result }, output: JSON.stringify(result) };
|
|
57
|
+
},
|
|
58
|
+
}, {
|
|
59
|
+
requiresWorkspace: false,
|
|
60
|
+
exposure: {
|
|
61
|
+
mode: "search",
|
|
62
|
+
title: "Workbench view",
|
|
63
|
+
keywords: ["show", "open", "preview", "note", "file", "asset", "browser", "panel"],
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RuntimeContext } from "@ericsanchezok/synergy-harness/lifecycle/context";
|
|
2
|
+
import { ToolRegistry } from "@ericsanchezok/synergy-harness/tool/registry";
|
|
3
|
+
import { WorkbenchViewTool } from "./tools/workbench-view.js";
|
|
4
|
+
const registered = RuntimeContext.state(() => ({ value: false }));
|
|
5
|
+
export function registerPresentationTools() {
|
|
6
|
+
if (registered().value)
|
|
7
|
+
return;
|
|
8
|
+
ToolRegistry.registerToolProvider("workbench-presentation", () => [WorkbenchViewTool]);
|
|
9
|
+
registered().value = true;
|
|
10
|
+
}
|