@teamclaws/teamclaw 2026.3.26-2 → 2026.4.2-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 (46) hide show
  1. package/README.md +52 -8
  2. package/cli.mjs +538 -224
  3. package/index.ts +76 -27
  4. package/openclaw.plugin.json +53 -28
  5. package/package.json +5 -2
  6. package/skills/teamclaw/SKILL.md +213 -0
  7. package/skills/teamclaw/references/api-quick-ref.md +117 -0
  8. package/skills/teamclaw-setup/SKILL.md +81 -0
  9. package/skills/teamclaw-setup/references/install-modes.md +136 -0
  10. package/skills/teamclaw-setup/references/validation-checklist.md +73 -0
  11. package/src/config.ts +44 -16
  12. package/src/controller/controller-capacity.ts +2 -2
  13. package/src/controller/controller-service.ts +193 -47
  14. package/src/controller/controller-tools.ts +102 -2
  15. package/src/controller/delivery-report.ts +563 -0
  16. package/src/controller/http-server.ts +1907 -172
  17. package/src/controller/kickoff-orchestrator.ts +292 -0
  18. package/src/controller/managed-gateway-process.ts +330 -0
  19. package/src/controller/orchestration-manifest.ts +69 -1
  20. package/src/controller/preview-manager.ts +676 -0
  21. package/src/controller/prompt-injector.ts +116 -67
  22. package/src/controller/role-inference.ts +41 -0
  23. package/src/controller/websocket.ts +3 -1
  24. package/src/controller/worker-provisioning.ts +429 -74
  25. package/src/discovery.ts +1 -1
  26. package/src/git-collaboration.ts +198 -47
  27. package/src/identity.ts +12 -2
  28. package/src/interaction-contracts.ts +179 -3
  29. package/src/networking.ts +99 -0
  30. package/src/openclaw-workspace.ts +478 -11
  31. package/src/prompt-policy.ts +381 -0
  32. package/src/roles.ts +37 -36
  33. package/src/state.ts +40 -1
  34. package/src/task-executor.ts +282 -78
  35. package/src/types.ts +150 -7
  36. package/src/ui/app.js +1403 -175
  37. package/src/ui/assets/teamclaw-app-icon.png +0 -0
  38. package/src/ui/index.html +122 -40
  39. package/src/ui/style.css +829 -143
  40. package/src/worker/http-handler.ts +40 -4
  41. package/src/worker/prompt-injector.ts +9 -38
  42. package/src/worker/skill-installer.ts +2 -2
  43. package/src/worker/tools.ts +31 -5
  44. package/src/worker/worker-service.ts +49 -8
  45. package/src/workspace-browser.ts +20 -7
  46. package/src/controller/local-worker-manager.ts +0 -533
@@ -4,10 +4,11 @@ import type { TaskAssignmentPayload, TeamMessage } from "../types.js";
4
4
  import { parseJsonBody, sendJson, sendError } from "../protocol.js";
5
5
  import { MessageQueue } from "./message-queue.js";
6
6
 
7
- export type TaskExecutor = (assignment: TaskAssignmentPayload) => Promise<string>;
8
- export type ResultReporter = (taskId: string, result: string, error: string | null) => void;
7
+ export type TaskExecutor = (assignment: TaskAssignmentPayload) => Promise<string | { text: string; contract?: Record<string, unknown> }>;
8
+ export type ResultReporter = (taskId: string, result: string, error: string | null, contract?: Record<string, unknown>) => void;
9
9
  export type TaskCanceller = (taskId: string) => Promise<boolean> | boolean;
10
10
  export type TaskCancelChecker = (taskId: string) => boolean;
11
+ export type KickoffAssessor = (requirement: string, role: string) => Promise<Record<string, unknown>>;
11
12
 
12
13
  export function createWorkerHttpHandler(
13
14
  config: { role: string; port: number },
@@ -18,6 +19,7 @@ export function createWorkerHttpHandler(
18
19
  resultReporter?: ResultReporter,
19
20
  cancelTaskExecution?: TaskCanceller,
20
21
  isTaskCancelled?: TaskCancelChecker,
22
+ kickoffAssessor?: KickoffAssessor,
21
23
  ) {
22
24
  return async (req: IncomingMessage, res: ServerResponse) => {
23
25
  // CORS preflight
@@ -68,6 +70,9 @@ export function createWorkerHttpHandler(
68
70
  const executionIdempotencyKey = typeof body.executionIdempotencyKey === "string"
69
71
  ? body.executionIdempotencyKey
70
72
  : undefined;
73
+ const projectDir = typeof body.projectDir === "string"
74
+ ? body.projectDir
75
+ : undefined;
71
76
  const repo = body.repo && typeof body.repo === "object"
72
77
  ? body.repo as TaskAssignmentPayload["repo"]
73
78
  : undefined;
@@ -85,16 +90,18 @@ export function createWorkerHttpHandler(
85
90
  title,
86
91
  description,
87
92
  recommendedSkills,
93
+ projectDir,
88
94
  executionSessionKey,
89
95
  executionIdempotencyKey,
90
96
  repo,
91
97
  })
92
- .then((result) => {
98
+ .then((raw) => {
93
99
  if (isTaskCancelled?.(taskId)) {
94
100
  logger.info(`Worker: skipping result report for cancelled task ${taskId}`);
95
101
  return;
96
102
  }
97
- resultReporter(taskId, result, null);
103
+ const result = typeof raw === "string" ? { text: raw } : raw;
104
+ resultReporter(taskId, result.text, null, result.contract);
98
105
  })
99
106
  .catch((err) => {
100
107
  if (isTaskCancelled?.(taskId)) {
@@ -139,6 +146,35 @@ export function createWorkerHttpHandler(
139
146
  return;
140
147
  }
141
148
 
149
+ // POST /api/v1/kickoff/assess
150
+ if (req.method === "POST" && pathname === "/api/v1/kickoff/assess") {
151
+ if (!kickoffAssessor) {
152
+ sendError(res, 501, "Kickoff assessment is not supported by this worker");
153
+ return;
154
+ }
155
+
156
+ const body = await parseJsonBody(req);
157
+ const requirement = typeof body.requirement === "string" ? body.requirement : "";
158
+ const role = typeof body.role === "string" ? body.role : config.role;
159
+
160
+ if (!requirement) {
161
+ sendError(res, 400, "requirement is required");
162
+ return;
163
+ }
164
+
165
+ logger.info(`Worker: received kickoff assessment request for role ${role}`);
166
+
167
+ try {
168
+ const assessment = await kickoffAssessor(requirement, role);
169
+ sendJson(res, 200, { assessment });
170
+ } catch (err) {
171
+ const errorMsg = err instanceof Error ? err.message : String(err);
172
+ logger.error(`Worker: kickoff assessment failed: ${errorMsg}`);
173
+ sendError(res, 500, `Assessment failed: ${errorMsg}`);
174
+ }
175
+ return;
176
+ }
177
+
142
178
  sendError(res, 404, "Not found");
143
179
  } catch (err) {
144
180
  logger.error(`Worker HTTP error: ${err instanceof Error ? err.message : String(err)}`);
@@ -1,20 +1,8 @@
1
1
  import type { PluginConfig, WorkerIdentity } from "../types.js";
2
2
  import { getRole } from "../roles.js";
3
+ import { buildWorkerSessionRules, composePrompt } from "../prompt-policy.js";
3
4
  import { MessageQueue } from "./message-queue.js";
4
5
 
5
- const TEAMCLAW_ROLE_IDS_TEXT = [
6
- "pm",
7
- "architect",
8
- "developer",
9
- "qa",
10
- "release-engineer",
11
- "infra-engineer",
12
- "devops",
13
- "security-engineer",
14
- "designer",
15
- "marketing",
16
- ].join(", ");
17
-
18
6
  export function createWorkerPromptInjector(
19
7
  config: PluginConfig,
20
8
  getIdentity: () => WorkerIdentity | null,
@@ -31,30 +19,13 @@ export function createWorkerPromptInjector(
31
19
  return null;
32
20
  }
33
21
 
34
- const parts: string[] = [];
35
-
36
- // Role context
37
- parts.push(`## TeamClaw Role: ${roleDef.label} ${roleDef.icon}`);
38
- parts.push(roleDef.systemPrompt);
39
- parts.push("");
40
- parts.push("## Current Session Rules");
41
- parts.push("1. Complete only the task assigned to this session.");
42
- parts.push("2. Pending team messages are context, not permission to widen scope.");
43
- parts.push("3. Do NOT create new tasks, duplicate an existing task, or start a parallel task tree.");
44
- parts.push("4. If you are blocked by missing information, raise a clarification request and stop instead of guessing.");
45
- parts.push("5. If required infrastructure, credentials, or external tool access are unavailable in this runtime, raise a clarification request and stop instead of faking completion.");
46
- parts.push("6. Respect the task's requested deliverable: briefs, plans, matrices, reviews, and design artifacts are not implementation requests unless the task explicitly asks you to build code.");
47
- parts.push("7. If another role must continue later, use review/handoff tools on the current task instead of spawning work.");
48
- parts.push("8. Other workers' OpenClaw sessions are isolated from this worker. Do not attempt cross-session inspection; use task context, the shared workspace, and queued team messages instead.");
49
- parts.push("9. Do not mark the task completed or failed via progress updates. Return the final deliverable and let TeamClaw close the task.");
50
- parts.push(`10. Valid TeamClaw role IDs: ${TEAMCLAW_ROLE_IDS_TEXT}.`);
51
- parts.push("11. Treat file paths from documents, plans, and teammate messages as hints, not guarantees. Verify the real path exists in the current workspace before reading or editing it; if it does not exist, search for the closest real file and note the drift instead of repeatedly calling missing paths.");
52
- parts.push("12. The workspace may be backed by a TeamClaw-managed git repository. Treat the current checkout as canonical project state; do not delete `.git` or replace the repo with ad-hoc archives.");
53
- parts.push("13. If the assigned task includes recommended skills, use those exact skill slugs first. Missing skills should be searched/installed before execution when supported by the runtime.");
54
- parts.push("14. Important: submit structured collaboration contracts, not only prose. Use teamclaw_submit_result_contract before your final reply, use structured fields on progress/handoff/review/message tools, and use clarification tools instead of hiding questions inside freeform output.");
55
- parts.push("15. Do not use sessions_yield or end your turn while background work, coding agents, or process sessions are still running. A TeamClaw task is only done when you have the real final deliverable, not when a helper session is still working.");
56
- parts.push(`Worker ID: ${identity.workerId}`);
57
- parts.push(`Controller: ${identity.controllerUrl}`);
22
+ const parts: string[] = [
23
+ `## TeamClaw Role: ${roleDef.label} ${roleDef.icon}`,
24
+ roleDef.systemPrompt,
25
+ ...buildWorkerSessionRules(),
26
+ `Worker ID: ${identity.workerId}`,
27
+ `Controller: ${identity.controllerUrl}`,
28
+ ];
58
29
 
59
30
  // Pending messages
60
31
  const pendingMessages = messageQueue.peek();
@@ -69,7 +40,7 @@ export function createWorkerPromptInjector(
69
40
  }
70
41
 
71
42
  return {
72
- prependSystemContext: parts.join("\n"),
43
+ prependSystemContext: composePrompt(parts),
73
44
  };
74
45
  };
75
46
  }
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import type { PluginLogger } from "../../api.js";
5
5
  import { normalizeRecommendedSkills } from "../roles.js";
6
- import { resolveDefaultOpenClawWorkspaceDir } from "../openclaw-workspace.js";
6
+ import { resolveTeamClawWorkspaceDir } from "../openclaw-workspace.js";
7
7
  import type { TaskAssignmentPayload, TaskExecutionEventInput } from "../types.js";
8
8
 
9
9
  type SkillCli = "openclaw" | "clawhub";
@@ -146,7 +146,7 @@ export async function installRecommendedSkills(
146
146
  logger: PluginLogger,
147
147
  ): Promise<SkillInstallResult> {
148
148
  const recommendedSkills = normalizeRecommendedSkills(assignment.recommendedSkills ?? []);
149
- const workspaceDir = resolveDefaultOpenClawWorkspaceDir();
149
+ const workspaceDir = resolveTeamClawWorkspaceDir();
150
150
  const events: TaskExecutionEventInput[] = [];
151
151
  const installed: string[] = [];
152
152
  const skipped: string[] = [];
@@ -11,6 +11,7 @@ import {
11
11
  renderWorkerProgressText,
12
12
  } from "../interaction-contracts.js";
13
13
  import type { PluginConfig, WorkerIdentity } from "../types.js";
14
+ import { normalizeClarificationQuestionSchema } from "../controller/orchestration-manifest.js";
14
15
 
15
16
  const ALLOWED_PROGRESS_STATUSES = new Set(["in_progress", "review"]);
16
17
 
@@ -274,10 +275,15 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
274
275
  kind: Type.String({ description: "Deliverable kind: file, directory, command, artifact, or note" }),
275
276
  value: Type.String({ description: "Deliverable identifier, path, or note" }),
276
277
  summary: Type.Optional(Type.String({ description: "Optional short note about this deliverable" })),
278
+ artifactType: Type.Optional(Type.String({ description: "Artifact type: web-app, static-site, binary, or document" })),
279
+ previewCommand: Type.Optional(Type.String({ description: "Command to start a live preview server (for web-app artifacts)" })),
280
+ previewCwd: Type.Optional(Type.String({ description: "Working directory for the preview command" })),
281
+ previewReadyPath: Type.Optional(Type.String({ description: "Path to poll for health (default /)" })),
277
282
  }),
278
283
  ),
279
284
  ),
280
285
  keyPoints: Type.Optional(Type.Array(Type.String({ description: "Important decisions, findings, or implementation notes" }))),
286
+ discoveredPatterns: Type.Optional(Type.Array(Type.String({ description: "Reusable codebase patterns discovered during this task (e.g. 'This codebase uses X for Y', 'Always update Z when changing W'). These are consolidated into workspace memory for future tasks." }))),
281
287
  blockers: Type.Optional(Type.Array(Type.String({ description: "Any unresolved blockers or risks" }))),
282
288
  followUps: Type.Optional(
283
289
  Type.Array(
@@ -308,6 +314,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
308
314
  summary: params.summary,
309
315
  deliverables: params.deliverables,
310
316
  keyPoints: params.keyPoints,
317
+ discoveredPatterns: params.discoveredPatterns,
311
318
  blockers: params.blockers,
312
319
  followUps: params.followUps,
313
320
  questions: params.questions,
@@ -346,6 +353,23 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
346
353
  question: Type.String({ description: "The exact question that must be answered before work can continue" }),
347
354
  blockingReason: Type.String({ description: "Why this task cannot proceed safely without clarification" }),
348
355
  context: Type.Optional(Type.String({ description: "Optional brief context or the specific decision that is missing" })),
356
+ questionSchema: Type.Optional(Type.Object({
357
+ kind: Type.String({ description: "Question kind: single-select, multi-select, number, or text" }),
358
+ title: Type.String({ description: "Question title shown to the human" }),
359
+ description: Type.Optional(Type.String({ description: "Optional supporting context for the question" })),
360
+ required: Type.Optional(Type.Boolean({ description: "Whether the question must be answered" })),
361
+ options: Type.Optional(Type.Array(Type.Object({
362
+ value: Type.String({ description: "Stable option value" }),
363
+ label: Type.String({ description: "Human-visible option label" }),
364
+ hint: Type.Optional(Type.String({ description: "Optional helper text for this option" })),
365
+ }))),
366
+ allowOther: Type.Optional(Type.Boolean({ description: "Whether freeform 'other' text is allowed" })),
367
+ placeholder: Type.Optional(Type.String({ description: "Optional placeholder or hint for text/number input" })),
368
+ unit: Type.Optional(Type.String({ description: "Optional unit label for number questions" })),
369
+ min: Type.Optional(Type.Number({ description: "Optional minimum numeric value" })),
370
+ max: Type.Optional(Type.Number({ description: "Optional maximum numeric value" })),
371
+ step: Type.Optional(Type.Number({ description: "Optional numeric step size" })),
372
+ })),
349
373
  }),
350
374
  async execute(_id: string, params: Record<string, unknown>) {
351
375
  const identity = getIdentity();
@@ -357,6 +381,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
357
381
  const question = String(params.question ?? "");
358
382
  const blockingReason = String(params.blockingReason ?? "");
359
383
  const context = typeof params.context === "string" ? params.context : undefined;
384
+ const questionSchema = normalizeClarificationQuestionSchema(params.questionSchema);
360
385
 
361
386
  if (!taskId || !question || !blockingReason) {
362
387
  return { content: [{ type: "text" as const, text: "taskId, question, and blockingReason are required." }] };
@@ -370,11 +395,12 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
370
395
  taskId,
371
396
  requestedBy: identity.workerId,
372
397
  requestedByWorkerId: identity.workerId,
373
- requestedByRole: identity.role,
374
- question,
375
- blockingReason,
376
- context,
377
- }),
398
+ requestedByRole: identity.role,
399
+ question,
400
+ questionSchema,
401
+ blockingReason,
402
+ context,
403
+ }),
378
404
  });
379
405
 
380
406
  if (!res.ok) {
@@ -5,13 +5,15 @@ import { createHeartbeatPayload } from "../protocol.js";
5
5
  import { IdentityManager } from "../identity.js";
6
6
  import { MessageQueue } from "./message-queue.js";
7
7
  import { createWorkerHttpHandler } from "./http-handler.js";
8
- import { ensureOpenClawWorkspaceMemoryDir } from "../openclaw-workspace.js";
8
+ import { buildTeamClawAgentSessionKey, ensureOpenClawWorkspaceMemoryDir } from "../openclaw-workspace.js";
9
+
10
+ export type TaskExecutorResultLike = string | { text: string; contract?: Record<string, unknown> };
9
11
 
10
12
  export type WorkerServiceDeps = {
11
13
  config: PluginConfig;
12
14
  logger: PluginLogger;
13
15
  onIdentityEstablished: (identity: WorkerIdentity) => void;
14
- taskExecutor?: (taskDescription: string, assignment: TaskAssignmentPayload) => Promise<string>;
16
+ taskExecutor?: (taskDescription: string, assignment: TaskAssignmentPayload) => Promise<TaskExecutorResultLike>;
15
17
  prepareTaskAssignment?: (assignment: TaskAssignmentPayload) => Promise<void> | void;
16
18
  publishTaskAssignment?: (assignment: TaskAssignmentPayload, result: string) => Promise<void> | void;
17
19
  cancelTaskExecution?: (taskId: string, sessionKey?: string) => Promise<boolean> | boolean;
@@ -31,19 +33,20 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
31
33
  const cancelledTaskIds = new Set<string>();
32
34
 
33
35
  const taskExecutor = externalTaskExecutor
34
- ? async (assignment: TaskAssignmentPayload): Promise<string> => {
36
+ ? async (assignment: TaskAssignmentPayload): Promise<{ text: string; contract?: Record<string, unknown> }> => {
35
37
  const taskId = assignment.taskId;
36
38
  cancelledTaskIds.delete(taskId);
37
39
  activeTaskId = taskId;
38
- activeTaskSessionKeys.set(taskId, assignment.executionSessionKey || `teamclaw-task-${taskId}`);
40
+ activeTaskSessionKeys.set(taskId, assignment.executionSessionKey || buildTeamClawAgentSessionKey(`teamclaw-task-${taskId}`));
39
41
  try {
40
42
  await deps.prepareTaskAssignment?.(assignment);
41
43
  const taskPrompt = [assignment.title.trim(), assignment.description.trim()].filter(Boolean).join("\n\n");
42
- const result = await externalTaskExecutor(taskPrompt, assignment);
44
+ const raw = await externalTaskExecutor(taskPrompt, assignment);
43
45
  if (cancelledTaskIds.has(taskId)) {
44
46
  throw new Error("Task execution cancelled by controller");
45
47
  }
46
- await deps.publishTaskAssignment?.(assignment, result);
48
+ const result = typeof raw === "string" ? { text: raw } : raw;
49
+ await deps.publishTaskAssignment?.(assignment, result.text);
47
50
  return result;
48
51
  } finally {
49
52
  activeTaskId = undefined;
@@ -55,16 +58,20 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
55
58
  }
56
59
  : undefined;
57
60
 
58
- function reportTaskResult(taskId: string, result: string, error: string | null): void {
61
+ function reportTaskResult(taskId: string, result: string, error: string | null, contract?: Record<string, unknown>): void {
59
62
  if (cancelledTaskIds.has(taskId)) {
60
63
  logger.info(`Worker: suppressing result report for cancelled task ${taskId}`);
61
64
  return;
62
65
  }
63
66
  if (!controllerUrl) return;
67
+ const body: Record<string, unknown> = { result, error, workerId };
68
+ if (contract) {
69
+ body.resultContract = contract;
70
+ }
64
71
  fetch(`${controllerUrl}/api/v1/tasks/${taskId}/result`, {
65
72
  method: "POST",
66
73
  headers: { "Content-Type": "application/json" },
67
- body: JSON.stringify({ result, error, workerId }),
74
+ body: JSON.stringify(body),
68
75
  }).catch((err) => {
69
76
  logger.error(`Worker: failed to report task result: ${String(err)}`);
70
77
  });
@@ -90,6 +97,39 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
90
97
  }
91
98
 
92
99
  async function startServer(): Promise<void> {
100
+ // Kickoff assessor — uses the worker's subagent runtime for lightweight assessment
101
+ const kickoffAssessor = externalTaskExecutor
102
+ ? async (requirement: string, role: string): Promise<Record<string, unknown>> => {
103
+ const { buildKickoffAssessmentPrompt } = await import("../controller/kickoff-orchestrator.js");
104
+ const prompt = buildKickoffAssessmentPrompt(role as import("../types.js").RoleId, requirement);
105
+ const sessionKey = buildTeamClawAgentSessionKey(`teamclaw-kickoff-${role}-${Date.now()}`);
106
+ const raw = await externalTaskExecutor(prompt, {
107
+ taskId: `kickoff-${role}`,
108
+ title: `Kickoff Assessment (${role})`,
109
+ description: prompt,
110
+ executionSessionKey: sessionKey,
111
+ });
112
+ const text = typeof raw === "string" ? raw : raw.text;
113
+ // Parse the JSON response
114
+ const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/) || text.match(/(\{[\s\S]*\})/);
115
+ const jsonStr = jsonMatch?.[1]?.trim() ?? text.trim();
116
+ try {
117
+ const parsed = JSON.parse(jsonStr);
118
+ return {
119
+ role,
120
+ needed: Boolean(parsed.needed),
121
+ scope: String(parsed.scope ?? ""),
122
+ suggestedTasks: Array.isArray(parsed.suggestedTasks) ? parsed.suggestedTasks : [],
123
+ dependencies: Array.isArray(parsed.dependencies) ? parsed.dependencies : [],
124
+ risks: Array.isArray(parsed.risks) ? parsed.risks : [],
125
+ questions: Array.isArray(parsed.questions) ? parsed.questions : [],
126
+ };
127
+ } catch {
128
+ return { role, needed: false, scope: `Could not parse: ${text.slice(0, 200)}`, suggestedTasks: [], dependencies: [], risks: [], questions: [] };
129
+ }
130
+ }
131
+ : undefined;
132
+
93
133
  const handler = createWorkerHttpHandler(
94
134
  { role: config.role, port: config.port },
95
135
  logger,
@@ -99,6 +139,7 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
99
139
  reportTaskResult,
100
140
  cancelAssignedTask,
101
141
  isTaskCancelled,
142
+ kickoffAssessor,
102
143
  );
103
144
 
104
145
  if (server) {
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { resolveDefaultOpenClawWorkspaceDir } from "./openclaw-workspace.js";
3
+ import { resolveTeamClawWorkspaceDir } from "./openclaw-workspace.js";
4
4
 
5
5
  const MAX_PREVIEW_BYTES = 256 * 1024;
6
6
  const MAX_TREE_DEPTH = 8;
@@ -81,15 +81,25 @@ export type WorkspaceFilePayload = {
81
81
  contentType: string;
82
82
  };
83
83
 
84
- export async function listWorkspaceTree(): Promise<WorkspaceTreePayload> {
84
+ export async function listWorkspaceTree(maxDepth?: number): Promise<WorkspaceTreePayload> {
85
85
  const workspaceDir = await ensureWorkspaceDir();
86
- const entries = await readTree(workspaceDir, "", 0);
86
+ const entries = await readTree(workspaceDir, "", 0, maxDepth);
87
87
  return {
88
88
  root: "/",
89
89
  entries,
90
90
  };
91
91
  }
92
92
 
93
+ /** Load children of a single directory (one level). Used for lazy-loading. */
94
+ export async function listWorkspaceSubtree(relativePath: string): Promise<WorkspaceTreeNode[]> {
95
+ const { absolutePath, normalizedPath } = await resolveWorkspacePath(relativePath);
96
+ const stat = await fs.stat(absolutePath);
97
+ if (!stat.isDirectory()) {
98
+ throw new Error("Path is not a directory");
99
+ }
100
+ return readTree(absolutePath, normalizedPath, 0, 1);
101
+ }
102
+
93
103
  export async function readWorkspaceFile(relativePath: string): Promise<WorkspaceFilePayload> {
94
104
  const { normalizedPath, absolutePath } = await resolveWorkspacePath(relativePath);
95
105
  const stat = await fs.stat(absolutePath);
@@ -147,7 +157,7 @@ export function buildWorkspaceRawUrl(relativePath: string): string {
147
157
  }
148
158
 
149
159
  async function ensureWorkspaceDir(): Promise<string> {
150
- const workspaceDir = resolveDefaultOpenClawWorkspaceDir();
160
+ const workspaceDir = resolveTeamClawWorkspaceDir();
151
161
  await fs.mkdir(workspaceDir, { recursive: true });
152
162
  return workspaceDir;
153
163
  }
@@ -189,8 +199,9 @@ function normalizeWorkspacePath(relativePath: string): string {
189
199
  return normalized;
190
200
  }
191
201
 
192
- async function readTree(dirPath: string, relativeDir: string, depth: number): Promise<WorkspaceTreeNode[]> {
193
- if (depth > MAX_TREE_DEPTH) {
202
+ async function readTree(dirPath: string, relativeDir: string, depth: number, maxDepth?: number): Promise<WorkspaceTreeNode[]> {
203
+ const effectiveMaxDepth = maxDepth ?? MAX_TREE_DEPTH;
204
+ if (depth > effectiveMaxDepth) {
194
205
  return [];
195
206
  }
196
207
 
@@ -209,11 +220,13 @@ async function readTree(dirPath: string, relativeDir: string, depth: number): Pr
209
220
  const childAbsolutePath = path.join(dirPath, dirent.name);
210
221
 
211
222
  if (dirent.isDirectory()) {
223
+ // At max depth, mark directory as lazy-loadable (no children yet)
224
+ const atLimit = depth + 1 > effectiveMaxDepth;
212
225
  nodes.push({
213
226
  name: dirent.name,
214
227
  path: childRelativePath,
215
228
  type: "directory",
216
- children: await readTree(childAbsolutePath, childRelativePath, depth + 1),
229
+ children: atLimit ? undefined : await readTree(childAbsolutePath, childRelativePath, depth + 1, effectiveMaxDepth),
217
230
  });
218
231
  continue;
219
232
  }