@omercnet/paseo-omp 0.2.1 → 0.3.0-next.101.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +25 -13
  3. package/SUPPORT.md +7 -3
  4. package/TESTING.md +21 -18
  5. package/client/composer-pill-settings.tsx +157 -0
  6. package/client/external-url.ts +15 -0
  7. package/client/mcp-authorization.tsx +169 -0
  8. package/client/mcp-popover.tsx +155 -0
  9. package/client/memory-panel.tsx +8 -3
  10. package/client/memory-popover.tsx +8 -4
  11. package/client/omp-config-surface.tsx +189 -29
  12. package/client/omp-plugin-manager.tsx +302 -131
  13. package/client/omp-store-picker.tsx +89 -0
  14. package/client/omp-store-state.ts +45 -0
  15. package/client/paseo-types.ts +9 -0
  16. package/client/provider-diagnostics-state.ts +18 -7
  17. package/client/quota-popover.tsx +8 -3
  18. package/client/quota-state.ts +16 -7
  19. package/client/sessions-popover.tsx +8 -3
  20. package/docs/alpha-release-checklist.md +6 -8
  21. package/docs/configuration.md +8 -4
  22. package/docs/core-provider-issue-audit.md +3 -2
  23. package/docs/images/mcp-authorization-compact.png +0 -0
  24. package/docs/images/mcp-controls-wide.png +0 -0
  25. package/docs/images/plugin-manager.png +0 -0
  26. package/docs/images/workspace-settings.png +0 -0
  27. package/docs/installation.md +35 -19
  28. package/index.client.tsx +339 -123
  29. package/index.server.ts +44 -14
  30. package/package.json +7 -8
  31. package/paseo-plugin.json +2 -2
  32. package/scripts/prepare-dependencies.mjs +24 -0
  33. package/server/mcp-browser.ts +95 -0
  34. package/server/memory.ts +2 -2
  35. package/server/omp-config.ts +16 -7
  36. package/server/omp-plugins.ts +70 -21
  37. package/server/omp-settings.ts +232 -24
  38. package/server/paths.ts +128 -11
  39. package/server/provider/catalog.ts +3 -4
  40. package/server/provider/connection.ts +248 -17
  41. package/server/provider/host-tools.ts +294 -26
  42. package/server/provider/omp-rpc.ts +499 -72
  43. package/server/provider/profile-providers.ts +249 -0
  44. package/server/provider/registration.ts +11 -0
  45. package/server/provider/security.ts +8 -10
  46. package/server/provider/session-descriptors.ts +340 -1
  47. package/server/provider/session.ts +716 -249
  48. package/server/provider/subsessions.ts +25 -2
  49. package/server/provider/timeline-projector.ts +104 -44
  50. package/server/provider-diagnostics.ts +122 -36
  51. package/server/quota.ts +3 -2
  52. package/server/sessions.ts +2 -2
  53. package/shared/composer-pill-settings.ts +28 -0
  54. package/shared/external-url.ts +21 -0
  55. package/shared/hub.ts +3 -3
  56. package/shared/mcp.ts +47 -0
  57. package/shared/memory.ts +2 -1
  58. package/shared/omp-config.ts +5 -1
  59. package/shared/omp-plugins.ts +74 -33
  60. package/shared/omp-settings.ts +8 -1
  61. package/shared/omp-store.ts +58 -0
  62. package/shared/provider-diagnostics.ts +12 -3
  63. package/shared/quota.ts +2 -1
  64. package/shared/sessions.ts +2 -1
@@ -94,6 +94,18 @@ const TaskResultDetailsSchema = z.object({
94
94
  exitCode: z.number().optional(),
95
95
  error: z.unknown().optional(),
96
96
  aborted: z.boolean().optional(),
97
+ status: z
98
+ .enum([
99
+ "pending",
100
+ "running",
101
+ "completed",
102
+ "failed",
103
+ "error",
104
+ "aborted",
105
+ "canceled",
106
+ "cancelled",
107
+ ])
108
+ .optional(),
97
109
  }),
98
110
  )
99
111
  .max(MAX_CHILDREN),
@@ -192,8 +204,15 @@ function replayChildren(messages: readonly OmpMessage[]): ReplayChildRef[] {
192
204
  const details = taskResultDetails(message);
193
205
  const results = details?.results ?? [];
194
206
  for (const result of results) {
207
+ const canceled =
208
+ result.aborted === true ||
209
+ result.status === "aborted" ||
210
+ result.status === "canceled" ||
211
+ result.status === "cancelled";
195
212
  const failed =
196
213
  message.isError === true ||
214
+ result.status === "failed" ||
215
+ result.status === "error" ||
197
216
  Boolean(result.error) ||
198
217
  (typeof result.exitCode === "number" && result.exitCode !== 0);
199
218
  children.push({
@@ -201,7 +220,7 @@ function replayChildren(messages: readonly OmpMessage[]): ReplayChildRef[] {
201
220
  agent: result.agent ?? call?.title,
202
221
  description: call?.description,
203
222
  parentToolCallId: message.toolCallId,
204
- status: result.aborted === true ? "canceled" : failed ? "failed" : "completed",
223
+ status: canceled ? "canceled" : failed ? "failed" : "completed",
205
224
  });
206
225
  }
207
226
  const resultIds = new Set(results.map((result) => result.id));
@@ -304,6 +323,7 @@ export class OmpSubsessionProjector {
304
323
  private readonly scheduler: OmpTimelineScheduler,
305
324
  private readonly onActivityChange: () => void,
306
325
  private readonly outputRedactionValues: readonly string[],
326
+ private readonly pluginTimelineEnabled = false,
307
327
  ) {
308
328
  this.dataFilter = new OmpPublicDataSerializer(outputRedactionValues);
309
329
  }
@@ -540,6 +560,8 @@ export class OmpSubsessionProjector {
540
560
  this.emit,
541
561
  this.scheduler,
542
562
  this.outputRedactionValues,
563
+ false,
564
+ this.pluginTimelineEnabled,
543
565
  ),
544
566
  };
545
567
  this.children.set(sessionId, child);
@@ -552,7 +574,8 @@ export class OmpSubsessionProjector {
552
574
  type: "session.opened",
553
575
  sessionId,
554
576
  parentSessionId,
555
- capabilities: [],
577
+ toolCallId: ref.parentToolCallId,
578
+ capabilities: ["session.subsession"],
556
579
  restoration: "parent",
557
580
  cwd: this.cwd,
558
581
  title,
@@ -4,19 +4,22 @@ import type {
4
4
  ProviderTimelineItem,
5
5
  ProviderToolCallDetail,
6
6
  } from "@getpaseo/plugin/server/provider";
7
+ import { OMP_MCP_AUTH_TIMELINE_KIND } from "../../shared/mcp";
7
8
  import { isOmpImageMimeType, isValidImagePayload, type OmpImageMimeType } from "./image";
8
- import type { OmpMessage, OmpRpcEvent } from "./omp-rpc";
9
+ import { OMP_MAX_CONTENT_PARTS, type OmpMessage, type OmpRpcEvent } from "./omp-rpc";
9
10
  import {
10
11
  boundedJsonBytes,
11
12
  type JsonValue,
12
13
  OmpPublicDataSerializer,
13
14
  OmpPublicError,
15
+ truncateUtf8,
14
16
  utf8Bytes,
15
17
  } from "./security";
16
18
 
17
19
  const STREAM_FRAME_MS = 32;
18
- const MAX_STREAM_CONTENT_BLOCKS = 64;
19
20
  const MAX_STREAM_TEXT_LENGTH = 4 * 1024 * 1024;
21
+ const MAX_RAW_STREAM_TEXT_LENGTH = 8 * 1024 * 1024;
22
+ const MAX_REDACTED_STREAM_TEXT_LENGTH = MAX_RAW_STREAM_TEXT_LENGTH * 3;
20
23
  const MAX_ACTIVE_TOOLS = 64;
21
24
  const MAX_TODOS = 256;
22
25
  const MAX_TURN_NATIVE_IDENTITIES = 1_024;
@@ -54,6 +57,7 @@ type StreamSnapshot = {
54
57
  nativeIdentity?: string;
55
58
  published: boolean;
56
59
  retainedBytes: number;
60
+ textBytes: number;
57
61
  publishedBytes: number;
58
62
  blocks: Map<number, StreamBlockSnapshot>;
59
63
  dirtyBlocks: Set<number>;
@@ -196,6 +200,17 @@ function publishableHttpUrl(value: string | undefined): string | undefined {
196
200
  return undefined;
197
201
  }
198
202
  }
203
+ function isOmpOAuthLaunchUrl(value: string | undefined): boolean {
204
+ const url = publishableHttpUrl(value);
205
+ if (!url) return false;
206
+ const parsed = new URL(url);
207
+ return (
208
+ (parsed.hostname === "localhost" ||
209
+ parsed.hostname === "127.0.0.1" ||
210
+ parsed.hostname === "[::1]") &&
211
+ parsed.pathname === "/launch"
212
+ );
213
+ }
199
214
 
200
215
  function todoPublicId(nativeId: string | undefined, index: number): string {
201
216
  if (!nativeId) return `omp:todo:${index}`;
@@ -240,7 +255,7 @@ function nativeImageResult(
240
255
  boundedJsonBytes(
241
256
  value,
242
257
  MAX_NATIVE_IMAGE_RESULT_BYTES,
243
- MAX_STREAM_CONTENT_BLOCKS,
258
+ OMP_MAX_CONTENT_PARTS,
244
259
  8 * 1024 * 1024,
245
260
  512,
246
261
  ) === Number.POSITIVE_INFINITY
@@ -350,6 +365,7 @@ export class OmpTimelineProjector {
350
365
  private readonly replayCandidates = new Map<string, ReplayCandidate>();
351
366
  private readonly replayOverflowCandidates = new Map<string, string>();
352
367
  private projectingReplay = false;
368
+ private readonly replayToolCalls = new Map<string, { toolName: string; args: unknown }>();
353
369
  private customSequence = 0;
354
370
  private compactionSequence = 0;
355
371
  private activeCompaction: CompactionSlot | null = null;
@@ -366,6 +382,7 @@ export class OmpTimelineProjector {
366
382
  private closed = false;
367
383
 
368
384
  private readonly dataFilter: OmpPublicDataSerializer;
385
+ private browserAuthorizationIssuer: ((url: string) => string | undefined) | null = null;
369
386
 
370
387
  constructor(
371
388
  private readonly sessionId: string,
@@ -373,10 +390,14 @@ export class OmpTimelineProjector {
373
390
  private readonly scheduler: OmpTimelineScheduler = defaultOmpTimelineScheduler,
374
391
  outputRedactionValues: readonly string[] = [],
375
392
  private readonly conversationRevertEnabled = false,
393
+ private readonly pluginTimelineEnabled = false,
376
394
  private readonly hostToolLabels: ReadonlyMap<string, string> = new Map(),
377
395
  ) {
378
396
  this.dataFilter = new OmpPublicDataSerializer(outputRedactionValues);
379
397
  }
398
+ setBrowserAuthorizationIssuer(issue: ((url: string) => string | undefined) | null): void {
399
+ this.browserAuthorizationIssuer = issue;
400
+ }
380
401
 
381
402
  project(event: OmpRpcEvent, turnId: string, bypassReplayFilter = false): void {
382
403
  if (this.closed) return;
@@ -601,15 +622,40 @@ export class OmpTimelineProjector {
601
622
  return;
602
623
  }
603
624
  if (event.type === "extension_ui_request" && event.method === "open_url") {
604
- const url = publishableHttpUrl(event.launchUrl ?? event.url);
625
+ const url = publishableHttpUrl(event.url);
605
626
  if (!url) return;
606
627
  this.noticeSequence += 1;
607
- const message = [event.instructions, url].filter(Boolean).join("\n");
628
+ const instructions = event.instructions
629
+ ? this.dataFilter.text(event.instructions, 64 * 1024)
630
+ : undefined;
631
+ const publicUrl = this.dataFilter.text(url, 16_384);
632
+ const isMcpAuthorization = isOmpOAuthLaunchUrl(event.launchUrl);
633
+ if (this.pluginTimelineEnabled && isMcpAuthorization && publishableHttpUrl(publicUrl)) {
634
+ const browserAuthorizationToken = this.browserAuthorizationIssuer?.(publicUrl);
635
+ this.publish({
636
+ type: "plugin",
637
+ id: `omp:ui:${this.noticeSequence}`,
638
+ pluginId: "paseo-omp",
639
+ kind: OMP_MCP_AUTH_TIMELINE_KIND,
640
+ version: 1,
641
+ data: {
642
+ url: publicUrl,
643
+ ...(instructions ? { instructions } : {}),
644
+ loopbackCallback: true,
645
+ ...(browserAuthorizationToken ? { browserAuthorizationToken } : {}),
646
+ },
647
+ });
648
+ return;
649
+ }
650
+ const remoteHint = isMcpAuthorization
651
+ ? "Remote client: if the localhost callback cannot connect, paste the final redirect URL or authorization code into the OMP prompt in this chat."
652
+ : undefined;
653
+ const message = [instructions, publicUrl, remoteHint].filter(Boolean).join("\n");
608
654
  this.publish({
609
655
  type: "notification",
610
656
  id: `omp:ui:${this.noticeSequence}`,
611
657
  level: "info",
612
- message: this.dataFilter.text(message, 64 * 1024),
658
+ message,
613
659
  });
614
660
  return;
615
661
  }
@@ -783,6 +829,7 @@ export class OmpTimelineProjector {
783
829
  this.replayOverflowCandidates.clear();
784
830
  this.projectingReplay = false;
785
831
  this.activeToolBytes = 0;
832
+ this.replayToolCalls.clear();
786
833
  this.tools.clear();
787
834
  this.commandText = "";
788
835
  this.commandPublishedText = "";
@@ -795,6 +842,7 @@ export class OmpTimelineProjector {
795
842
  const nativeIdentity = assistantIdentity(message);
796
843
  this.replaySequence += 1;
797
844
  if (message.role === "user") {
845
+ this.replayToolCalls.clear();
798
846
  if (this.replayTurnId) this.finishTurn(this.replayTurnId);
799
847
  this.replayTurnId = `omp:replay-turn:${this.replaySequence}`;
800
848
  const text =
@@ -823,15 +871,7 @@ export class OmpTimelineProjector {
823
871
  for (const part of message.content) {
824
872
  if (part.type !== "toolCall" || !part.id || !part.name || part.arguments === undefined)
825
873
  continue;
826
- this.project(
827
- {
828
- type: "tool_execution_start",
829
- toolCallId: part.id,
830
- toolName: part.name,
831
- args: part.arguments,
832
- },
833
- this.replayTurnId,
834
- );
874
+ this.replayToolCalls.set(part.id, { toolName: part.name, args: part.arguments });
835
875
  }
836
876
  }
837
877
  } finally {
@@ -842,13 +882,15 @@ export class OmpTimelineProjector {
842
882
  }
843
883
  this.replayTurnId ??= `omp:replay-turn:${this.replaySequence}`;
844
884
  if (message.role === "toolResult") {
885
+ const replayCall = this.replayToolCalls.get(message.toolCallId);
886
+ this.replayToolCalls.delete(message.toolCallId);
845
887
  if (!this.tools.has(message.toolCallId)) {
846
888
  this.project(
847
889
  {
848
890
  type: "tool_execution_start",
849
891
  toolCallId: message.toolCallId,
850
- toolName: message.toolName,
851
- args: null,
892
+ toolName: replayCall?.toolName ?? message.toolName,
893
+ args: replayCall?.args ?? null,
852
894
  },
853
895
  this.replayTurnId,
854
896
  );
@@ -869,10 +911,7 @@ export class OmpTimelineProjector {
869
911
  return;
870
912
  }
871
913
  if (message.role === "bashExecution") {
872
- const text = message.output
873
- ? `$ ${message.command}\n${message.output}`
874
- : `$ ${message.command}`;
875
- this.project({ type: "command_output", text }, this.replayTurnId);
914
+ this.publishCustomMessage(message);
876
915
  this.finishTurn(this.replayTurnId);
877
916
  this.replayTurnId = null;
878
917
  }
@@ -880,6 +919,7 @@ export class OmpTimelineProjector {
880
919
 
881
920
  finishReplay(): void {
882
921
  if (this.replayTurnId) this.finishTurn(this.replayTurnId);
922
+ this.replayToolCalls.clear();
883
923
  this.replayTurnId = null;
884
924
  }
885
925
  acceptLiveTurn(turnId: string): void {
@@ -967,13 +1007,28 @@ export class OmpTimelineProjector {
967
1007
  if (!this.stream || this.closed || this.stream.dirtyBlocks.size === 0) return;
968
1008
  const stream = this.stream;
969
1009
  if (!stream.nativeIdentity && !finalizeFallback) return;
970
- const indexes = [...stream.dirtyBlocks].sort((left, right) => left - right);
1010
+ const indexes = [...stream.blocks.keys()].sort((left, right) => left - right);
1011
+ let remainingTextBytes = MAX_STREAM_TEXT_LENGTH;
971
1012
  stream.dirtyBlocks.clear();
972
1013
  for (const contentIndex of indexes) {
973
1014
  const block = stream.blocks.get(contentIndex);
974
- if (!block?.text) continue;
975
- const publicText = block.kind === "image" ? block.text : this.dataFilter.text(block.text);
976
- if (!publicText || block.publishedText === publicText) continue;
1015
+ if (!block) continue;
1016
+ const publicText =
1017
+ block.kind === "image"
1018
+ ? block.text
1019
+ : truncateUtf8(
1020
+ this.dataFilter.text(block.text, MAX_REDACTED_STREAM_TEXT_LENGTH),
1021
+ remainingTextBytes,
1022
+ );
1023
+ if (block.kind !== "image") {
1024
+ remainingTextBytes = Math.max(0, remainingTextBytes - utf8Bytes(publicText));
1025
+ }
1026
+ if (
1027
+ block.publishedText === publicText ||
1028
+ (!publicText && block.publishedText === undefined)
1029
+ ) {
1030
+ continue;
1031
+ }
977
1032
  const nextPublishedBytes = utf8Bytes(publicText);
978
1033
  if (
979
1034
  stream.retainedBytes + stream.publishedBytes + nextPublishedBytes >
@@ -1027,9 +1082,11 @@ export class OmpTimelineProjector {
1027
1082
  if (this.currentTurnId) this.publishCommand(this.currentTurnId);
1028
1083
  this.retireCompactions("OMP compaction ended when the session closed");
1029
1084
  this.closed = true;
1085
+ this.browserAuthorizationIssuer = null;
1030
1086
  this.clearFlushTimer();
1031
1087
  this.stream = null;
1032
1088
  this.tools.clear();
1089
+ this.replayToolCalls.clear();
1033
1090
  this.activeToolBytes = 0;
1034
1091
  this.replayCandidates.clear();
1035
1092
  this.replayOverflowCandidates.clear();
@@ -1091,6 +1148,7 @@ export class OmpTimelineProjector {
1091
1148
  ...(nativeIdentity ? { nativeIdentity } : {}),
1092
1149
  published: false,
1093
1150
  retainedBytes: 0,
1151
+ textBytes: 0,
1094
1152
  publishedBytes: 0,
1095
1153
  blocks: new Map(),
1096
1154
  dirtyBlocks: new Set(),
@@ -1155,7 +1213,7 @@ export class OmpTimelineProjector {
1155
1213
  return;
1156
1214
  }
1157
1215
  if (!Array.isArray(message.content)) return;
1158
- const blockCount = Math.min(message.content.length, MAX_STREAM_CONTENT_BLOCKS);
1216
+ const blockCount = Math.min(message.content.length, OMP_MAX_CONTENT_PARTS);
1159
1217
  for (let index = 0; index < blockCount; index += 1) {
1160
1218
  const block = blockText(message, index);
1161
1219
  if (block) this.setBlock(stream, index, block);
@@ -1215,28 +1273,22 @@ export class OmpTimelineProjector {
1215
1273
  if (snapshot.kind === "image" && utf8Bytes(snapshot.text) > MAX_IMAGE_ENCODED_LENGTH + 256)
1216
1274
  return;
1217
1275
  const previous = stream.blocks.get(contentIndex);
1218
- let retainedBytes = utf8Bytes(snapshot.text);
1219
- let textBytes = snapshot.kind === "image" ? 0 : retainedBytes;
1220
- for (const [index, block] of stream.blocks) {
1221
- if (index === contentIndex) continue;
1222
- const blockBytes = utf8Bytes(block.text);
1223
- retainedBytes += blockBytes;
1224
- if (block.kind !== "image") textBytes += blockBytes;
1225
- if (
1226
- retainedBytes + stream.publishedBytes > MAX_STREAM_TOTAL_BYTES ||
1227
- textBytes > MAX_STREAM_TEXT_LENGTH
1228
- ) {
1229
- return;
1230
- }
1231
- }
1276
+ const previousBytes = previous ? utf8Bytes(previous.text) : 0;
1277
+ const snapshotBytes = utf8Bytes(snapshot.text);
1278
+ const retainedBytes = stream.retainedBytes - previousBytes + snapshotBytes;
1279
+ const textBytes =
1280
+ stream.textBytes -
1281
+ (previous && previous.kind !== "image" ? previousBytes : 0) +
1282
+ (snapshot.kind === "image" ? 0 : snapshotBytes);
1232
1283
  if (
1233
1284
  retainedBytes + stream.publishedBytes > MAX_STREAM_TOTAL_BYTES ||
1234
- textBytes > MAX_STREAM_TEXT_LENGTH
1285
+ textBytes > MAX_RAW_STREAM_TEXT_LENGTH
1235
1286
  ) {
1236
1287
  return;
1237
1288
  }
1238
1289
  if (previous?.kind === snapshot.kind && previous.text === snapshot.text) return;
1239
1290
  stream.retainedBytes = retainedBytes;
1291
+ stream.textBytes = textBytes;
1240
1292
  stream.blocks.set(contentIndex, {
1241
1293
  ...snapshot,
1242
1294
  ...(previous?.kind === snapshot.kind ? { publishedText: previous.publishedText } : {}),
@@ -1248,13 +1300,13 @@ export class OmpTimelineProjector {
1248
1300
  return (
1249
1301
  Number.isSafeInteger(contentIndex) &&
1250
1302
  contentIndex >= 0 &&
1251
- contentIndex < MAX_STREAM_CONTENT_BLOCKS
1303
+ contentIndex < OMP_MAX_CONTENT_PARTS
1252
1304
  );
1253
1305
  }
1254
1306
 
1255
1307
  private publishCommand(turnId: string): void {
1256
1308
  if (!this.commandText) return;
1257
- const publicText = this.dataFilter.text(this.commandText);
1309
+ const publicText = this.dataFilter.text(this.commandText, MAX_STREAM_TEXT_LENGTH);
1258
1310
  if (!publicText || publicText === this.commandPublishedText) return;
1259
1311
  this.commandPublishedText = publicText;
1260
1312
  this.publish({
@@ -1340,13 +1392,21 @@ export class OmpTimelineProjector {
1340
1392
  type: "shell",
1341
1393
  command,
1342
1394
  ...(firstString(details, "cwd") ? { cwd: firstString(details, "cwd") } : {}),
1343
- ...(output ? { output: this.dataFilter.text(output) } : {}),
1395
+ ...(output ? { output: this.dataFilter.text(output, MAX_STREAM_TEXT_LENGTH) } : {}),
1344
1396
  ...(typeof message.exitCode === "number" || message.exitCode === null
1345
1397
  ? { exitCode: message.exitCode }
1346
1398
  : typeof details?.exitCode === "number"
1347
1399
  ? { exitCode: details.exitCode }
1348
1400
  : {}),
1349
1401
  },
1402
+ ...(message.cancelled !== undefined || message.truncated !== undefined
1403
+ ? {
1404
+ metadata: {
1405
+ ...(message.cancelled !== undefined ? { cancelled: message.cancelled } : {}),
1406
+ ...(message.truncated !== undefined ? { truncated: message.truncated } : {}),
1407
+ },
1408
+ }
1409
+ : {}),
1350
1410
  status: message.cancelled ? "canceled" : "completed",
1351
1411
  error: null,
1352
1412
  });