nolo-cli 0.1.21 → 0.1.23

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 (72) hide show
  1. package/agent-runtime/agentRecordConfig.ts +4 -0
  2. package/agent-runtime/hostAdapter.ts +2 -0
  3. package/agent-runtime/index.ts +7 -0
  4. package/agent-runtime/localLoop.ts +2 -0
  5. package/agent-runtime/platformChatProvider.ts +3 -0
  6. package/agent-runtime/runtimeToolPolicy.ts +92 -0
  7. package/agent-runtime/types.ts +42 -0
  8. package/agentRunCommand.ts +74 -1
  9. package/agentRuntimeCommands.ts +17 -89
  10. package/ai/agent/streamAgentChatTurn.ts +104 -20
  11. package/ai/chat/fetchUtils.native.ts +2 -0
  12. package/ai/chat/fetchUtils.ts +2 -0
  13. package/ai/chat/sendOpenAICompletionsRequest.ts +56 -0
  14. package/ai/chat/sendOpenAIResponseRequest.ts +64 -0
  15. package/ai/llm/kimi.ts +1 -1
  16. package/ai/llm/providers.ts +3 -0
  17. package/ai/llm/reasoningModels.ts +1 -0
  18. package/ai/skills/skillDocProtocol.ts +95 -3
  19. package/ai/taskRun/taskRunProtocol.ts +1 -0
  20. package/ai/tools/agent/agentTools.ts +17 -0
  21. package/ai/tools/agent/startAgentDialogTool.ts +53 -0
  22. package/ai/tools/modelUsageTools.ts +5 -0
  23. package/client/agentRun.test.ts +257 -7
  24. package/client/agentRun.ts +133 -34
  25. package/client/localRuntimeAdapter.test.ts +2 -0
  26. package/client/localRuntimeAdapter.ts +15 -2
  27. package/database/actions/common.ts +4 -3
  28. package/database/config.ts +19 -0
  29. package/machineCommands.ts +400 -45
  30. package/package.json +4 -2
  31. package/render/canvas/canvasEditContext.ts +127 -0
  32. package/render/canvas/canvasRuntime.ts +57 -0
  33. package/render/canvas/canvasSnapshotParser.ts +76 -0
  34. package/render/canvas/canvasTree.ts +308 -0
  35. package/render/canvas/types.ts +46 -0
  36. package/render/layout/deleteBehavior.ts +52 -0
  37. package/render/layout/mainLayoutSidebar.ts +17 -0
  38. package/render/layout/mainLayoutViewMode.ts +56 -0
  39. package/render/layout/topbarUtils.ts +87 -0
  40. package/render/layout/useDevReloadPending.ts +30 -0
  41. package/render/page/createPageAction.ts +183 -0
  42. package/render/page/docSlice.ts +468 -0
  43. package/render/page/server/createPage.ts +174 -0
  44. package/render/page/server/handleCreatePage.ts +91 -0
  45. package/render/page/server/index.ts +4 -0
  46. package/render/page/types.ts +17 -0
  47. package/render/page/useKeyboardSave.ts +48 -0
  48. package/render/styles/zIndex.ts +12 -0
  49. package/render/surf/WeatherIconStyles.ts +17 -0
  50. package/render/surf/color.ts +9 -0
  51. package/render/surf/config.ts +46 -0
  52. package/render/surf/screens/style.ts +1 -0
  53. package/render/surf/styles/ToggleButtonStyles.ts +8 -0
  54. package/render/surf/utils/groupedWeatherData.ts +32 -0
  55. package/render/surf/weatherUtils.ts +50 -0
  56. package/render/table/activityColumns.ts +6 -0
  57. package/render/table/createTableAction.ts +270 -0
  58. package/render/table/deleteTableAction.ts +129 -0
  59. package/render/table/fetchAndCacheTableRows.ts +174 -0
  60. package/render/table/tableSlice.ts +1106 -0
  61. package/render/table/tableView.ts +289 -0
  62. package/render/table/toolValueUtils.ts +363 -0
  63. package/render/table/types.ts +252 -0
  64. package/render/table/useCreateTable.ts +72 -0
  65. package/render/table/useTable.ts +61 -0
  66. package/render/table/utils/tableSerialization.ts +50 -0
  67. package/render/web/elements/artifactPreviewCode.ts +43 -0
  68. package/render/web/elements/artifactRuntimePreload.ts +52 -0
  69. package/render/web/elements/codeBlockAutoPreview.ts +10 -0
  70. package/render/web/elements/mermaidPreview.ts +21 -0
  71. package/render/web/ui/useInlineEdit.ts +135 -0
  72. package/tableCommands.ts +42 -5
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
4
4
  import { mkdirSync, openSync } from "node:fs";
5
5
  import { homedir } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
+ import { runLocalAgentTurn, type LocalAgentToolEvent } from "./agent-runtime/localLoop";
7
8
  import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
8
9
  import {
9
10
  collectConnectorRunArtifact,
@@ -19,7 +20,9 @@ import {
19
20
  buildMachinePermissionPromptBlock,
20
21
  resolveMachineRunPermissionPolicy,
21
22
  } from "./ai/agent/machineRunPermissions";
22
- import { resolveConnectorWebSocketTarget } from "./connectorWebSocketTarget";
23
+ import { resolveConnectorWebSocketTarget } from "./connectorWebSocketTarget";
24
+ import { createCliLocalRuntimeAdapter } from "./client/localRuntimeAdapter";
25
+ import { prepareTaskWorktree } from "./client/taskWorktree";
23
26
 
24
27
  type EnvLike = Record<string, string | undefined>;
25
28
  type OutputLike = { write(chunk: string): unknown };
@@ -28,11 +31,35 @@ type ConnectorWebSocketOptions = {
28
31
  onMessage: (message: string) => void | Promise<void>;
29
32
  sentMessages: string[];
30
33
  };
31
- type LocalCliExecutor = (
34
+ export type LocalCliExecutor = (
32
35
  provider: string,
33
36
  prompt: string,
34
37
  options: { model?: string; timeout?: number; cwd?: string; yolo?: boolean; env?: EnvLike }
35
38
  ) => Promise<{ text: string; raw?: string; elapsed?: number }>;
39
+
40
+ type ConnectorRuntimePolicy = {
41
+ runtimeTools?: string[];
42
+ agentTools?: string[];
43
+ workspace?: {
44
+ mode?: string;
45
+ cwd?: string;
46
+ };
47
+ shell?: {
48
+ enabled?: boolean;
49
+ mode?: string;
50
+ };
51
+ };
52
+
53
+ type ConnectorRunProgress = {
54
+ eventType?: LocalAgentToolEvent["type"];
55
+ toolCallCount?: number;
56
+ toolResultCount?: number;
57
+ lastToolNames?: string[];
58
+ workspaceRoot?: string;
59
+ workspaceKind?: string;
60
+ message?: string;
61
+ updatedAt?: number;
62
+ };
36
63
 
37
64
  export type MachineSummary = {
38
65
  machineId: string;
@@ -264,8 +291,8 @@ function buildTaskRunBridgePrompt(args: {
264
291
  function buildConnectorCliPrompt(agentConfig: any, userInput: string, bridgeArgs?: {
265
292
  agentKey: string;
266
293
  runtimeContext: any;
267
- }) {
268
- const policy = resolveMachineRunPermissionPolicy(agentConfig);
294
+ }, permissionPolicy?: ReturnType<typeof resolveMachineRunPermissionPolicy>) {
295
+ const policy = permissionPolicy ?? resolveMachineRunPermissionPolicy(agentConfig);
269
296
  return [
270
297
  typeof agentConfig?.prompt === "string" ? agentConfig.prompt.trim() : "",
271
298
  bridgeArgs ? buildTaskRunBridgePrompt({
@@ -278,6 +305,250 @@ function buildConnectorCliPrompt(agentConfig: any, userInput: string, bridgeArgs
278
305
  ].filter(Boolean).join("\n\n");
279
306
  }
280
307
 
308
+ function isRecord(value: unknown): value is Record<string, any> {
309
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
310
+ }
311
+
312
+ function runtimePolicyFromConnectorPayload(parsed: any): ConnectorRuntimePolicy | undefined {
313
+ const fromMeta = parsed?.payload?.meta?.runtimeToolPolicySnapshot;
314
+ const fromAgent = parsed?.payload?.agentConfig?.runtimeToolPolicy;
315
+ const policy = isRecord(fromMeta) ? fromMeta : isRecord(fromAgent) ? fromAgent : null;
316
+ return policy ? policy as ConnectorRuntimePolicy : undefined;
317
+ }
318
+
319
+ function requestsTaskWorktree(policy?: ConnectorRuntimePolicy) {
320
+ return policy?.workspace?.mode === "task-worktree" || policy?.workspace?.mode === "lease";
321
+ }
322
+
323
+ function hasExplicitMachinePermissions(agentConfig: any) {
324
+ const runtimeBinding = isRecord(agentConfig?.runtimeBinding) ? agentConfig.runtimeBinding : {};
325
+ return Boolean(
326
+ isRecord(agentConfig?.machinePermissions) ||
327
+ isRecord(runtimeBinding.permissions) ||
328
+ isRecord(runtimeBinding.machinePermissions) ||
329
+ isRecord(agentConfig?.boundRuntimeMachine?.permissions)
330
+ );
331
+ }
332
+
333
+ function runtimeWorkspacePermissionPolicy(
334
+ runtimePolicy: ConnectorRuntimePolicy | undefined,
335
+ cwd: string
336
+ ): ReturnType<typeof resolveMachineRunPermissionPolicy> | null {
337
+ if (!requestsTaskWorktree(runtimePolicy)) return null;
338
+ return {
339
+ mode: "full_access",
340
+ allowFilesystemRead: true,
341
+ allowFilesystemWrite: true,
342
+ allowShell: runtimePolicy?.shell?.enabled !== false,
343
+ writableRoots: [cwd],
344
+ };
345
+ }
346
+
347
+ function scopePermissionPolicyToRuntimeWorkspace(
348
+ policy: ReturnType<typeof resolveMachineRunPermissionPolicy>,
349
+ runtimePolicy: ConnectorRuntimePolicy | undefined,
350
+ cwd: string
351
+ ): ReturnType<typeof resolveMachineRunPermissionPolicy> {
352
+ if (!requestsTaskWorktree(runtimePolicy)) return policy;
353
+ return {
354
+ ...policy,
355
+ writableRoots: policy.allowFilesystemWrite ? [cwd] : [],
356
+ };
357
+ }
358
+
359
+ function mergeToolNames(...values: unknown[]) {
360
+ const names: string[] = [];
361
+ for (const value of values) {
362
+ if (!Array.isArray(value)) continue;
363
+ for (const item of value) {
364
+ const name = typeof item === "string"
365
+ ? item
366
+ : isRecord(item) && typeof item.name === "string"
367
+ ? item.name
368
+ : isRecord(item) && typeof item.function?.name === "string"
369
+ ? item.function.name
370
+ : "";
371
+ if (name && !names.includes(name)) names.push(name);
372
+ }
373
+ }
374
+ return names;
375
+ }
376
+
377
+ function localRuntimeEnvFromPolicy(runtimeEnv: EnvLike, policy?: ConnectorRuntimePolicy): EnvLike {
378
+ return {
379
+ ...runtimeEnv,
380
+ ...(policy?.shell?.enabled && policy.shell.mode === "worktree"
381
+ ? { NOLO_LOCAL_SHELL_MODE: "worktree" }
382
+ : {}),
383
+ ...(policy?.workspace?.cwd ? { NOLO_LOCAL_WORKTREE: policy.workspace.cwd } : {}),
384
+ };
385
+ }
386
+
387
+ function runtimeWorkspaceRootFromTrace(trace: unknown): string | undefined {
388
+ if (!Array.isArray(trace)) return undefined;
389
+ for (const message of trace) {
390
+ if (!isRecord(message)) continue;
391
+ const metadata = isRecord(message.tool_result_metadata)
392
+ ? message.tool_result_metadata
393
+ : null;
394
+ const workspaceRoot = metadata && typeof metadata.workspaceRoot === "string"
395
+ ? metadata.workspaceRoot.trim()
396
+ : "";
397
+ if (workspaceRoot) return workspaceRoot;
398
+ }
399
+ return undefined;
400
+ }
401
+
402
+ function buildArtifactProgress(args: {
403
+ artifacts: unknown;
404
+ runtimePolicy?: ConnectorRuntimePolicy;
405
+ workspaceRoot: string;
406
+ workspaceKind?: string;
407
+ }): ConnectorRunProgress | null {
408
+ if (!isRecord(args.artifacts)) return null;
409
+ const changedFiles = Array.isArray(args.artifacts.changedFiles)
410
+ ? args.artifacts.changedFiles
411
+ : [];
412
+ const statusShort = typeof args.artifacts.statusShort === "string"
413
+ ? args.artifacts.statusShort.trim()
414
+ : "";
415
+ if (changedFiles.length === 0 && !statusShort) return null;
416
+ const runtimeTools = Array.isArray(args.runtimePolicy?.runtimeTools)
417
+ ? args.runtimePolicy.runtimeTools
418
+ : [];
419
+ const shellTool = runtimeTools.includes("execBash")
420
+ ? "execBash"
421
+ : runtimeTools.includes("execShell")
422
+ ? "execShell"
423
+ : "workspaceArtifact";
424
+ return {
425
+ eventType: "tool-result",
426
+ toolCallCount: 1,
427
+ toolResultCount: 1,
428
+ lastToolNames: [shellTool],
429
+ workspaceRoot: args.workspaceRoot,
430
+ ...(args.workspaceKind ? { workspaceKind: args.workspaceKind } : {}),
431
+ message: statusShort || `${changedFiles.length} changed file(s)`,
432
+ updatedAt: Date.now(),
433
+ };
434
+ }
435
+
436
+ async function runConnectorLocalRuntimeAgent(args: {
437
+ parsed: any;
438
+ runtimeEnv: EnvLike;
439
+ cwd: string;
440
+ fetchImpl?: typeof fetch;
441
+ onProgress?: (progress: ConnectorRunProgress) => void;
442
+ }): Promise<Awaited<ReturnType<typeof runLocalAgentTurn>> & { runtimeWorkspaceRoot?: string }> {
443
+ const agentKey = String(args.parsed.payload?.agentKey ?? "");
444
+ const payloadAgentConfig = isRecord(args.parsed.payload?.agentConfig)
445
+ ? args.parsed.payload.agentConfig
446
+ : {};
447
+ const policy = runtimePolicyFromConnectorPayload(args.parsed);
448
+ const agentRecord = {
449
+ ...payloadAgentConfig,
450
+ dbKey: agentKey,
451
+ id: agentKey,
452
+ key: agentKey,
453
+ apiSource: payloadAgentConfig.apiSource ?? "platform",
454
+ provider: payloadAgentConfig.provider ?? payloadAgentConfig.apiSource ?? "openai",
455
+ toolNames: mergeToolNames(
456
+ payloadAgentConfig.toolNames,
457
+ payloadAgentConfig.tools,
458
+ policy?.agentTools,
459
+ policy?.runtimeTools,
460
+ ),
461
+ ...(policy ? { runtimeToolPolicy: policy } : {}),
462
+ ...(policy?.workspace?.mode === "task-worktree" || policy?.workspace?.mode === "lease"
463
+ ? { localWorkspaceMode: "task-worktree" }
464
+ : {}),
465
+ };
466
+ const store = new Map<string, any>([
467
+ [agentKey, agentRecord],
468
+ [`agent-${args.runtimeEnv.NOLO_USER_ID || "local"}-${agentKey}`, agentRecord],
469
+ [`agent-${args.runtimeEnv.NOLO_LOCAL_USER_ID || args.runtimeEnv.NOLO_USER_ID || "local"}-${agentKey}`, agentRecord],
470
+ ]);
471
+ const env = localRuntimeEnvFromPolicy(args.runtimeEnv, policy);
472
+ const wantsTaskWorktree = requestsTaskWorktree(policy);
473
+ const adapter = createCliLocalRuntimeAdapter({
474
+ env,
475
+ cwd: args.cwd,
476
+ useCwdAsTaskWorkspaceBase: wantsTaskWorktree,
477
+ ...(wantsTaskWorktree
478
+ ? {
479
+ prepareTaskWorktree: (input) => prepareTaskWorktree({
480
+ ...input,
481
+ cwd: args.cwd,
482
+ }),
483
+ }
484
+ : {}),
485
+ fetchImpl: args.fetchImpl ?? fetch,
486
+ store: {
487
+ read: async (key) => store.get(key) ?? null,
488
+ batch: async (ops) => {
489
+ for (const op of ops) {
490
+ if (op.type === "put") store.set(op.key, op.value);
491
+ }
492
+ },
493
+ iterator: async function* (options) {
494
+ const keys = [...store.keys()].sort();
495
+ for (const key of keys) {
496
+ if (key < options.gte) continue;
497
+ if (options.lte && key > options.lte) continue;
498
+ if (options.lt && key >= options.lt) continue;
499
+ yield [key, store.get(key)];
500
+ }
501
+ },
502
+ },
503
+ });
504
+ let toolCallCount = 0;
505
+ let toolResultCount = 0;
506
+ const lastToolNames: string[] = [];
507
+ let runtimeWorkspaceRoot: string | undefined;
508
+ let runtimeWorkspaceKind: string | undefined;
509
+ const noteToolName = (toolName: string) => {
510
+ if (!toolName) return;
511
+ const existingIndex = lastToolNames.indexOf(toolName);
512
+ if (existingIndex >= 0) lastToolNames.splice(existingIndex, 1);
513
+ lastToolNames.push(toolName);
514
+ while (lastToolNames.length > 8) lastToolNames.shift();
515
+ };
516
+ const result = await runLocalAgentTurn({
517
+ adapter,
518
+ agentRef: agentKey,
519
+ input: String(args.parsed.payload?.userInput ?? ""),
520
+ continueDialogId: typeof args.parsed.payload?.continueDialogId === "string"
521
+ ? args.parsed.payload.continueDialogId
522
+ : undefined,
523
+ onToolEvent: (event) => {
524
+ if (event.type === "tool-call") toolCallCount += 1;
525
+ if (event.type === "tool-result") toolResultCount += 1;
526
+ noteToolName(event.toolName);
527
+ const metadata = isRecord(event.metadata) ? event.metadata : {};
528
+ if (typeof metadata.workspaceRoot === "string" && metadata.workspaceRoot.trim()) {
529
+ runtimeWorkspaceRoot = metadata.workspaceRoot.trim();
530
+ }
531
+ if (typeof metadata.workspaceKind === "string" && metadata.workspaceKind.trim()) {
532
+ runtimeWorkspaceKind = metadata.workspaceKind.trim();
533
+ }
534
+ args.onProgress?.({
535
+ eventType: event.type,
536
+ toolCallCount,
537
+ toolResultCount,
538
+ lastToolNames: [...lastToolNames],
539
+ ...(runtimeWorkspaceRoot ? { workspaceRoot: runtimeWorkspaceRoot } : {}),
540
+ ...(runtimeWorkspaceKind ? { workspaceKind: runtimeWorkspaceKind } : {}),
541
+ ...(event.message ? { message: event.message } : {}),
542
+ updatedAt: Date.now(),
543
+ });
544
+ },
545
+ });
546
+ return {
547
+ ...result,
548
+ runtimeWorkspaceRoot: runtimeWorkspaceRootFromTrace(result.trace) ?? runtimeWorkspaceRoot,
549
+ };
550
+ }
551
+
281
552
  function normalizeConnectorRunTimeoutMs(value: unknown): number | undefined {
282
553
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
283
554
  return undefined;
@@ -285,12 +556,13 @@ function normalizeConnectorRunTimeoutMs(value: unknown): number | undefined {
285
556
  return Math.floor(value);
286
557
  }
287
558
 
288
- async function handleConnectorRunMessage(
559
+ export async function handleConnectorRunMessage(
289
560
  machine: MachineHeartbeat,
290
561
  message: string,
291
562
  send: (message: string) => void,
292
563
  executeCli: LocalCliExecutor,
293
- runtimeEnv: EnvLike
564
+ runtimeEnv: EnvLike,
565
+ fetchImpl?: typeof fetch
294
566
  ) {
295
567
  let parsed: any;
296
568
  try {
@@ -298,51 +570,129 @@ async function handleConnectorRunMessage(
298
570
  } catch {
299
571
  return;
300
572
  }
301
- if (parsed?.type !== "agent.run" || typeof parsed.requestId !== "string") return;
302
- const agentConfig = parsed.payload?.agentConfig ?? {};
303
- try {
304
- if (agentConfig.apiSource !== "cli") {
305
- throw new Error("Connector can only execute CLI agents. Set the agent apiSource to cli and choose a cliProvider.");
306
- }
307
- const provider = String(agentConfig.cliProvider || "copilot");
308
- const policy = resolveMachineRunPermissionPolicy(agentConfig);
573
+ if (parsed?.type !== "agent.run" || typeof parsed.requestId !== "string") return;
574
+ const agentConfig = parsed.payload?.agentConfig ?? {};
575
+ const sendProgress = (progress: ConnectorRunProgress) => {
576
+ send(JSON.stringify({
577
+ type: "agent.run.progress",
578
+ requestId: parsed.requestId,
579
+ progress,
580
+ }));
581
+ };
582
+ try {
583
+ const machinePermissionPolicy = resolveMachineRunPermissionPolicy(agentConfig);
584
+ const runtimePolicy = runtimePolicyFromConnectorPayload(parsed);
309
585
  const userInput = String(parsed.payload?.userInput ?? "");
310
586
  const timeout = normalizeConnectorRunTimeoutMs(parsed.payload?.timeoutMs);
311
- assertMachineRunAllowed(userInput, policy);
312
- const cwd = resolveConnectorRunCwd({ env: runtimeEnv, policy });
313
- const baseSha = await readConnectorGitHead(cwd);
314
- const result = await executeCli(
315
- provider,
316
- buildConnectorCliPrompt(agentConfig, userInput, {
317
- agentKey: String(parsed.payload?.agentKey ?? ""),
318
- runtimeContext: parsed.payload?.runtimeContext,
319
- }),
320
- {
321
- model: agentConfig.model || undefined,
322
- timeout,
587
+ let cwd = resolveConnectorRunCwd({ env: runtimeEnv, policy: machinePermissionPolicy });
588
+ let runContent = "";
589
+ let runModel = agentConfig.model;
590
+ let runTrace: unknown[] = [];
591
+ let artifactCwd = cwd;
592
+ let baseSha: string | null = null;
593
+ if (agentConfig.apiSource === "cli") {
594
+ if (requestsTaskWorktree(runtimePolicy)) {
595
+ const prepared = await prepareTaskWorktree({
596
+ agentKey: String(parsed.payload?.agentKey ?? "agent"),
597
+ cwd,
598
+ env: runtimeEnv,
599
+ });
600
+ cwd = prepared.path;
601
+ artifactCwd = prepared.path;
602
+ }
603
+ const runtimePermissionPolicy = !hasExplicitMachinePermissions(agentConfig)
604
+ ? runtimeWorkspacePermissionPolicy(runtimePolicy, cwd)
605
+ : null;
606
+ const effectivePermissionPolicy = scopePermissionPolicyToRuntimeWorkspace(
607
+ runtimePermissionPolicy ?? machinePermissionPolicy,
608
+ runtimePolicy,
323
609
  cwd,
324
- yolo: true,
325
- env: {
326
- NOLO_SERVER: runtimeEnv.NOLO_SERVER || runtimeEnv.NOLO_SERVER_URL || runtimeEnv.BASE_URL,
327
- NOLO_SERVER_URL: runtimeEnv.NOLO_SERVER_URL || runtimeEnv.NOLO_SERVER || runtimeEnv.BASE_URL,
328
- BASE_URL: runtimeEnv.BASE_URL || runtimeEnv.NOLO_SERVER || runtimeEnv.NOLO_SERVER_URL,
329
- AUTH_TOKEN: runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH || runtimeEnv.NOLO_MACHINE_API_KEY,
330
- NOLO_MACHINE_API_KEY: runtimeEnv.NOLO_MACHINE_API_KEY || runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH,
610
+ );
611
+ assertMachineRunAllowed(userInput, effectivePermissionPolicy);
612
+ baseSha = await readConnectorGitHead(artifactCwd);
613
+ const provider = String(agentConfig.cliProvider || "copilot");
614
+ const result = await executeCli(
615
+ provider,
616
+ buildConnectorCliPrompt(agentConfig, userInput, {
617
+ agentKey: String(parsed.payload?.agentKey ?? ""),
618
+ runtimeContext: parsed.payload?.runtimeContext,
619
+ }, effectivePermissionPolicy),
620
+ {
621
+ model: agentConfig.model || undefined,
622
+ timeout,
623
+ cwd,
624
+ yolo: true,
625
+ env: {
626
+ NOLO_SERVER: runtimeEnv.NOLO_SERVER || runtimeEnv.NOLO_SERVER_URL || runtimeEnv.BASE_URL,
627
+ NOLO_SERVER_URL: runtimeEnv.NOLO_SERVER_URL || runtimeEnv.NOLO_SERVER || runtimeEnv.BASE_URL,
628
+ BASE_URL: runtimeEnv.BASE_URL || runtimeEnv.NOLO_SERVER || runtimeEnv.NOLO_SERVER_URL,
629
+ AUTH_TOKEN: runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH || runtimeEnv.NOLO_MACHINE_API_KEY,
630
+ NOLO_MACHINE_API_KEY: runtimeEnv.NOLO_MACHINE_API_KEY || runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH,
631
+ },
632
+ }
633
+ );
634
+ runContent = result.text;
635
+ runModel = agentConfig.model ?? provider;
636
+ runTrace = [{ role: "assistant", content: result.text }];
637
+ const artifacts = await collectConnectorRunArtifact({
638
+ cwd: artifactCwd,
639
+ baseSha,
640
+ exitStatus: "completed",
641
+ });
642
+ const artifactProgress = buildArtifactProgress({
643
+ artifacts,
644
+ runtimePolicy,
645
+ workspaceRoot: artifactCwd,
646
+ workspaceKind: requestsTaskWorktree(runtimePolicy) ? "task-worktree" : "current",
647
+ });
648
+ if (artifactProgress && requestsTaskWorktree(runtimePolicy)) sendProgress(artifactProgress);
649
+ send(JSON.stringify({
650
+ type: "agent.run.result",
651
+ requestId: parsed.requestId,
652
+ result: {
653
+ content: runContent,
654
+ model: runModel,
655
+ trace: runTrace,
656
+ artifacts,
331
657
  },
658
+ }));
659
+ return;
660
+ } else {
661
+ if (!runtimePolicy) {
662
+ throw new Error("Connector can only execute non-CLI agents when runtimeToolPolicySnapshot requests a local workspace runtime.");
332
663
  }
333
- );
664
+ baseSha = await readConnectorGitHead(cwd);
665
+ const result = await runConnectorLocalRuntimeAgent({
666
+ parsed,
667
+ runtimeEnv,
668
+ cwd,
669
+ fetchImpl,
670
+ onProgress: sendProgress,
671
+ });
672
+ runContent = result.content;
673
+ runModel = result.model;
674
+ runTrace = result.trace ?? [];
675
+ artifactCwd = result.runtimeWorkspaceRoot ?? cwd;
676
+ }
334
677
  const artifacts = await collectConnectorRunArtifact({
335
- cwd,
678
+ cwd: artifactCwd,
336
679
  baseSha,
337
680
  exitStatus: "completed",
338
681
  });
682
+ const artifactProgress = buildArtifactProgress({
683
+ artifacts,
684
+ runtimePolicy,
685
+ workspaceRoot: artifactCwd,
686
+ workspaceKind: requestsTaskWorktree(runtimePolicy) ? "task-worktree" : "current",
687
+ });
688
+ if (artifactProgress && requestsTaskWorktree(runtimePolicy)) sendProgress(artifactProgress);
339
689
  send(JSON.stringify({
340
- type: "agent.run.result",
341
- requestId: parsed.requestId,
342
- result: {
343
- content: result.text,
344
- model: agentConfig.model ?? provider,
345
- trace: [{ role: "assistant", content: result.text }],
690
+ type: "agent.run.result",
691
+ requestId: parsed.requestId,
692
+ result: {
693
+ content: runContent,
694
+ model: runModel,
695
+ trace: runTrace,
346
696
  artifacts,
347
697
  },
348
698
  }));
@@ -385,6 +735,10 @@ async function runConnectorWebSocketSession(options: {
385
735
  headers: { Authorization: `Bearer ${options.authToken}` },
386
736
  fetchImpl: options.fetchImpl,
387
737
  });
738
+ const runtimeServerUrl =
739
+ options.env.NOLO_CONNECT_RUNTIME_SERVER_URL ||
740
+ options.env.NOLO_RUNTIME_SERVER_URL ||
741
+ options.serverUrl;
388
742
  const websocketPromise = options.connectWebSocket(
389
743
  wsTarget,
390
744
  {
@@ -397,12 +751,13 @@ async function runConnectorWebSocketSession(options: {
397
751
  options.executeCli,
398
752
  {
399
753
  ...options.env,
400
- NOLO_SERVER: options.serverUrl,
401
- NOLO_SERVER_URL: options.serverUrl,
402
- BASE_URL: options.serverUrl,
754
+ NOLO_SERVER: runtimeServerUrl,
755
+ NOLO_SERVER_URL: runtimeServerUrl,
756
+ BASE_URL: runtimeServerUrl,
403
757
  AUTH_TOKEN: options.authToken,
404
758
  NOLO_MACHINE_API_KEY: options.authToken,
405
- }
759
+ },
760
+ options.fetchImpl
406
761
  ),
407
762
  }
408
763
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nolo-cli",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
6
  "bin": {
@@ -33,7 +33,8 @@
33
33
  "agent-runtime/**/*.ts",
34
34
  "database/**/*.ts",
35
35
  "connector-experimental/**/*.ts",
36
- "ai/**/*.ts"
36
+ "ai/**/*.ts",
37
+ "render/**/*.ts"
37
38
  ],
38
39
  "publishConfig": {
39
40
  "access": "public"
@@ -47,6 +48,7 @@
47
48
  "dependencies": {
48
49
  "date-fns-tz": "^2.0.0",
49
50
  "level": "^10.0.0",
51
+ "js-yaml": "^4.1.0",
50
52
  "ulid": "^2.3.0"
51
53
  }
52
54
  }
@@ -0,0 +1,127 @@
1
+ import React from "react";
2
+ import type { AgentRuntimeOptions } from "ai/agent/types";
3
+ import type { CanvasEvent } from "./types";
4
+
5
+ export type CanvasEditSelection = {
6
+ sourceMessageId?: string;
7
+ selectedNodeId: string;
8
+ part: string;
9
+ path: string[];
10
+ type: string;
11
+ props: Record<string, unknown>;
12
+ style: Record<string, unknown>;
13
+ };
14
+
15
+ const CANVAS_EDIT_SELECTION_EVENT = "nolo:canvas-edit-selection";
16
+
17
+ let currentSelection: CanvasEditSelection | null = null;
18
+ let pendingSelection: CanvasEditSelection | null = null;
19
+ const listeners = new Set<(selection: CanvasEditSelection | null) => void>();
20
+ const patchListeners = new Map<string, Set<(events: CanvasEvent[]) => void>>();
21
+ const queuedPatches = new Map<string, CanvasEvent[][]>();
22
+
23
+ export function publishCanvasEditSelection(
24
+ selection: CanvasEditSelection | null
25
+ ) {
26
+ currentSelection = selection;
27
+ listeners.forEach((listener) => listener(selection));
28
+
29
+ if (typeof window !== "undefined") {
30
+ window.dispatchEvent(
31
+ new CustomEvent(CANVAS_EDIT_SELECTION_EVENT, { detail: selection })
32
+ );
33
+ }
34
+ }
35
+
36
+ export function subscribeCanvasEditSelection(
37
+ listener: (selection: CanvasEditSelection | null) => void
38
+ ) {
39
+ listeners.add(listener);
40
+ listener(currentSelection);
41
+ return () => {
42
+ listeners.delete(listener);
43
+ };
44
+ }
45
+
46
+ export function useCanvasEditSelection() {
47
+ const [selection, setSelection] =
48
+ React.useState<CanvasEditSelection | null>(currentSelection);
49
+
50
+ React.useEffect(
51
+ () => subscribeCanvasEditSelection(setSelection),
52
+ []
53
+ );
54
+
55
+ return selection;
56
+ }
57
+
58
+ export function markPendingCanvasEditSelection(
59
+ selection: CanvasEditSelection | null
60
+ ) {
61
+ pendingSelection = selection;
62
+ }
63
+
64
+ export function consumePendingCanvasEditSelection() {
65
+ const selection = pendingSelection;
66
+ pendingSelection = null;
67
+ return selection;
68
+ }
69
+
70
+ export function publishCanvasMessagePatch(
71
+ sourceMessageId: string,
72
+ events: CanvasEvent[]
73
+ ) {
74
+ if (!events.length) return;
75
+
76
+ const queued = queuedPatches.get(sourceMessageId) ?? [];
77
+ queued.push(events);
78
+ queuedPatches.set(sourceMessageId, queued);
79
+
80
+ patchListeners
81
+ .get(sourceMessageId)
82
+ ?.forEach((listener) => listener(events));
83
+ }
84
+
85
+ export function subscribeCanvasMessagePatches(
86
+ sourceMessageId: string,
87
+ listener: (events: CanvasEvent[]) => void
88
+ ) {
89
+ const listenersForMessage = patchListeners.get(sourceMessageId) ?? new Set();
90
+ listenersForMessage.add(listener);
91
+ patchListeners.set(sourceMessageId, listenersForMessage);
92
+
93
+ queuedPatches
94
+ .get(sourceMessageId)
95
+ ?.forEach((events) => listener(events));
96
+
97
+ return () => {
98
+ listenersForMessage.delete(listener);
99
+ if (!listenersForMessage.size) {
100
+ patchListeners.delete(sourceMessageId);
101
+ }
102
+ };
103
+ }
104
+
105
+ export function buildCanvasNodeEditingTarget(
106
+ selection: CanvasEditSelection
107
+ ): NonNullable<AgentRuntimeOptions["editingTarget"]> {
108
+ return {
109
+ kind: "canvas_node",
110
+ key: selection.selectedNodeId,
111
+ title: selection.part,
112
+ summary: [
113
+ "用户正在编辑 Canvas Tree 画布中的一个已选中节点。",
114
+ "只输出针对选中节点的 canvas_snapshot updateNode 事件,除非用户明确要求新增内容。",
115
+ "不要重建整棵树,不要输出 Markdown、解释、React、HTML、CSS 或 JS 源码。",
116
+ ].join("\n"),
117
+ metadata: {
118
+ sourceMessageId: selection.sourceMessageId,
119
+ selectedNodeId: selection.selectedNodeId,
120
+ part: selection.part,
121
+ path: selection.path,
122
+ type: selection.type,
123
+ props: selection.props,
124
+ style: selection.style,
125
+ },
126
+ };
127
+ }