@tt-a1i/hive 2.1.14 → 2.1.16

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 (65) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/src/cli/team.js +19 -6
  3. package/dist/src/server/agent-run-bootstrap.d.ts +3 -0
  4. package/dist/src/server/agent-run-bootstrap.js +1 -0
  5. package/dist/src/server/agent-run-exit-handler.js +2 -7
  6. package/dist/src/server/agent-run-start-context.d.ts +4 -4
  7. package/dist/src/server/agent-run-starter.js +12 -5
  8. package/dist/src/server/agent-startup-instructions.d.ts +1 -0
  9. package/dist/src/server/agent-startup-instructions.js +4 -1
  10. package/dist/src/server/hive-team-guidance.js +2 -0
  11. package/dist/src/server/resumed-session-cleanup.d.ts +15 -0
  12. package/dist/src/server/resumed-session-cleanup.js +26 -0
  13. package/dist/src/server/route-types.d.ts +1 -0
  14. package/dist/src/server/routes-team.js +3 -0
  15. package/dist/src/server/routes-workers.d.ts +2 -0
  16. package/dist/src/server/routes-workers.js +112 -0
  17. package/dist/src/server/routes-workspaces.js +4 -85
  18. package/dist/src/server/routes.js +2 -0
  19. package/dist/src/server/runtime-store-contract.d.ts +5 -0
  20. package/dist/src/server/runtime-store.js +2 -0
  21. package/dist/src/server/session-capture-codex.d.ts +1 -0
  22. package/dist/src/server/session-capture-codex.js +22 -2
  23. package/dist/src/server/session-capture.d.ts +1 -0
  24. package/dist/src/server/session-capture.js +5 -4
  25. package/dist/src/server/sqlite-schema-v39.d.ts +2 -0
  26. package/dist/src/server/sqlite-schema-v39.js +6 -0
  27. package/dist/src/server/sqlite-schema.d.ts +1 -1
  28. package/dist/src/server/sqlite-schema.js +8 -1
  29. package/dist/src/server/team-list-serializer.d.ts +5 -1
  30. package/dist/src/server/team-list-serializer.js +2 -1
  31. package/dist/src/server/team-memory-dream-prompt.js +18 -5
  32. package/dist/src/server/team-operations.d.ts +4 -0
  33. package/dist/src/server/team-operations.js +18 -5
  34. package/dist/src/server/workspace-store-contract.d.ts +6 -0
  35. package/dist/src/server/workspace-store-hydration.js +2 -1
  36. package/dist/src/server/workspace-store-support.d.ts +1 -0
  37. package/dist/src/server/workspace-store.js +57 -2
  38. package/dist/src/shared/types.d.ts +4 -0
  39. package/dist/src/shared/worker-avatar.d.ts +2 -0
  40. package/dist/src/shared/worker-avatar.js +55 -0
  41. package/package.json +1 -1
  42. package/web/dist/assets/AddWorkerDialog-CPQkMvDz.js +2 -0
  43. package/web/dist/assets/AddWorkspaceFlow-CkOOz4Kj.js +1 -0
  44. package/web/dist/assets/FirstRunWizard-DzAnGniE.js +1 -0
  45. package/web/dist/assets/{MarketplaceDrawer-BHTvxLMz.js → MarketplaceDrawer-DAWF9idk.js} +1 -1
  46. package/web/dist/assets/{TaskGraphDrawer-woCXhUwp.js → TaskGraphDrawer-DRVJ4Z7z.js} +1 -1
  47. package/web/dist/assets/WhatsNewDialog-BDsPg5Fa.js +1 -0
  48. package/web/dist/assets/WorkerModal-BN3jQTBO.js +1 -0
  49. package/web/dist/assets/{WorkflowsDrawer-Diw0TiiJ.js → WorkflowsDrawer-Clv5ROhE.js} +1 -1
  50. package/web/dist/assets/WorkspaceMemoryDrawer-DVZT9TqV.js +1 -0
  51. package/web/dist/assets/{WorkspaceTaskDrawer-D10RcAUj.js → WorkspaceTaskDrawer-C14sALb8.js} +1 -1
  52. package/web/dist/assets/index-Cs7E1UKt.css +1 -0
  53. package/web/dist/assets/index-DoOf8jsj.js +84 -0
  54. package/web/dist/assets/{search-D6bjVJKH.js → search-CPePg8x3.js} +1 -1
  55. package/web/dist/assets/{square-terminal-BUBEjTQZ.js → square-terminal-DwJxHx2m.js} +1 -1
  56. package/web/dist/index.html +2 -2
  57. package/web/dist/sw.js +1 -1
  58. package/web/dist/assets/AddWorkerDialog-BdIc_ZDz.js +0 -2
  59. package/web/dist/assets/AddWorkspaceFlow-h-UH433o.js +0 -1
  60. package/web/dist/assets/FirstRunWizard-ChuB1siy.js +0 -1
  61. package/web/dist/assets/WhatsNewDialog-BcRbRAJI.js +0 -1
  62. package/web/dist/assets/WorkerModal-CTf1k881.js +0 -1
  63. package/web/dist/assets/WorkspaceMemoryDrawer-B7Z1_COx.js +0 -1
  64. package/web/dist/assets/index-BTaZgzo0.js +0 -84
  65. package/web/dist/assets/index-DYIPzuPD.css +0 -1
@@ -1,6 +1,6 @@
1
1
  import { closeSync, existsSync, openSync, readdirSync, readSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
- import { join } from 'node:path';
3
+ import { basename, join } from 'node:path';
4
4
  import { captureSessionIdWithCoordinator } from './claude-session-coordinator.js';
5
5
  import { arePathsEqual, expandHomePath, indexOfPathMarker } from './platform-path.js';
6
6
  const CODEX_SESSION_FILE = /^rollout-.*\.jsonl$/i;
@@ -93,7 +93,27 @@ const listSessionIds = (cwd, codexHome = getDefaultCodexHome(), platform = proce
93
93
  })
94
94
  .sort((left, right) => left.localeCompare(right));
95
95
  };
96
- export const hasCodexSession = (cwd, sessionId, pattern, platform = process.platform, codexHome = getCodexHome(pattern, platform)) => listSessionIds(cwd, codexHome, platform).includes(sessionId);
96
+ const fileNameMatchesSessionId = (filePath, sessionId) => basename(filePath).endsWith(`-${sessionId}.jsonl`);
97
+ export const getCodexSessionExistence = (cwd, sessionId, pattern, platform = process.platform, codexHome = getCodexHome(pattern, platform)) => {
98
+ const sessionsRoot = join(codexHome, 'sessions');
99
+ let foundUnverifiableMatchingFile = false;
100
+ for (const filePath of walkSessionFiles(sessionsRoot)) {
101
+ try {
102
+ const session = parseCodexSession(filePath);
103
+ if (session?.id === sessionId)
104
+ return arePathsEqual(session.cwd, cwd, platform);
105
+ if (!session && fileNameMatchesSessionId(filePath, sessionId)) {
106
+ foundUnverifiableMatchingFile = true;
107
+ }
108
+ }
109
+ catch {
110
+ if (fileNameMatchesSessionId(filePath, sessionId))
111
+ foundUnverifiableMatchingFile = true;
112
+ }
113
+ }
114
+ return foundUnverifiableMatchingFile ? undefined : false;
115
+ };
116
+ export const hasCodexSession = (cwd, sessionId, pattern, platform = process.platform, codexHome = getCodexHome(pattern, platform)) => getCodexSessionExistence(cwd, sessionId, pattern, platform, codexHome) ?? false;
97
117
  export const snapshotCodexSessionIds = (cwd, codexHome = getDefaultCodexHome(), platform = process.platform) => new Set(listSessionIds(cwd, codexHome, platform));
98
118
  export const captureCodexSessionId = async (cwd, knownSessionIds, onCapture, timeoutMs = 5000, intervalMs = 100, codexHome = getDefaultCodexHome()) => {
99
119
  await captureSessionIdWithCoordinator({
@@ -90,5 +90,6 @@ export declare const snapshotSessionIdsForCapture: (cwd: string, capture: Sessio
90
90
  } | undefined;
91
91
  export declare const getSessionCaptureEnvironment: (snapshot: SessionCaptureSnapshot | undefined) => Record<string, string>;
92
92
  export declare const captureSessionIdForCapture: (cwd: string, capture: SessionIdCaptureConfig, snapshot: SessionCaptureSnapshot, onCapture: (sessionId: string) => void, timeoutMs?: number, intervalMs?: number) => Promise<void>;
93
+ export declare const getCapturedSessionExistence: (cwd: string, capture: SessionIdCaptureConfig, sessionId: string, discriminator?: SessionCaptureSnapshot["discriminator"]) => boolean | undefined;
93
94
  export declare const doesCapturedSessionExist: (cwd: string, capture: SessionIdCaptureConfig, sessionId: string, discriminator?: SessionCaptureSnapshot["discriminator"]) => boolean;
94
95
  export declare const captureStdoutRegexSessionId: (pattern: string, getOutput: (() => string | null) | undefined, knownSessionIds: Set<string>, onCapture: (sessionId: string) => void, timeoutMs?: number, intervalMs?: number) => Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { captureClaudeSessionId, getClaudeProjectsRoot, hasClaudeSessionFile, snapshotClaudeSessionIds, } from './session-capture-claude.js';
2
- import { captureCodexSessionId, getCodexHome, hasCodexSession, snapshotCodexSessionIds, } from './session-capture-codex.js';
2
+ import { captureCodexSessionId, getCodexHome, getCodexSessionExistence, snapshotCodexSessionIds, } from './session-capture-codex.js';
3
3
  import { captureGeminiSessionId, getGeminiHome, hasGeminiSession, snapshotGeminiSessionIds, } from './session-capture-gemini.js';
4
4
  import { captureOpenCodeSessionId, getOpenCodeDbPath, hasOpenCodeSession, snapshotOpenCodeSessionIds, } from './session-capture-opencode.js';
5
5
  import { captureQwenSessionId, getQwenHome, hasQwenSession, snapshotQwenSessionIds, } from './session-capture-qwen.js';
@@ -95,12 +95,12 @@ export const captureSessionIdForCapture = async (cwd, capture, snapshot, onCaptu
95
95
  await captureStdoutRegexSessionId(capture.pattern, snapshot.getOutput, snapshot.knownSessionIds, onCapture, timeoutMs, intervalMs);
96
96
  }
97
97
  };
98
- export const doesCapturedSessionExist = (cwd, capture, sessionId, discriminator) => {
98
+ export const getCapturedSessionExistence = (cwd, capture, sessionId, discriminator) => {
99
99
  if (capture.source === 'claude_project_jsonl_dir') {
100
100
  return hasClaudeSessionFile(cwd, sessionId, capture.pattern, discriminator);
101
101
  }
102
102
  if (capture.source === 'codex_session_jsonl_dir') {
103
- return hasCodexSession(cwd, sessionId, capture.pattern);
103
+ return getCodexSessionExistence(cwd, sessionId, capture.pattern);
104
104
  }
105
105
  if (capture.source === 'gemini_session_json_dir') {
106
106
  return hasGeminiSession(cwd, sessionId, capture.pattern);
@@ -111,8 +111,9 @@ export const doesCapturedSessionExist = (cwd, capture, sessionId, discriminator)
111
111
  if (capture.source === 'qwen_session_json_dir') {
112
112
  return hasQwenSession(cwd, sessionId, capture.pattern);
113
113
  }
114
- return false;
114
+ return undefined;
115
115
  };
116
+ export const doesCapturedSessionExist = (cwd, capture, sessionId, discriminator) => getCapturedSessionExistence(cwd, capture, sessionId, discriminator) ?? false;
116
117
  const compileCaptureRegex = (pattern) => {
117
118
  try {
118
119
  return new RegExp(pattern, 'u');
@@ -0,0 +1,2 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ export declare const applySchemaVersion39: (db: Database) => void;
@@ -0,0 +1,6 @@
1
+ export const applySchemaVersion39 = (db) => {
2
+ const workerColumns = new Set(db.prepare('PRAGMA table_info(workers)').all().map((column) => column.name));
3
+ if (!workerColumns.has('avatar')) {
4
+ db.exec('ALTER TABLE workers ADD COLUMN avatar TEXT');
5
+ }
6
+ };
@@ -1,3 +1,3 @@
1
1
  import type { Database } from 'better-sqlite3';
2
- export declare const CURRENT_SCHEMA_VERSION = 38;
2
+ export declare const CURRENT_SCHEMA_VERSION = 39;
3
3
  export declare const initializeRuntimeDatabase: (db: Database) => void;
@@ -31,7 +31,8 @@ import { applySchemaVersion35 } from './sqlite-schema-v35.js';
31
31
  import { applySchemaVersion36 } from './sqlite-schema-v36.js';
32
32
  import { applySchemaVersion37 } from './sqlite-schema-v37.js';
33
33
  import { applySchemaVersion38 } from './sqlite-schema-v38.js';
34
- export const CURRENT_SCHEMA_VERSION = 38;
34
+ import { applySchemaVersion39 } from './sqlite-schema-v39.js';
35
+ export const CURRENT_SCHEMA_VERSION = 39;
35
36
  // Idempotent column-add helper. SQLite doesn't have `ALTER TABLE … ADD COLUMN
36
37
  // IF NOT EXISTS`, so PRAGMA-check first. Safe to call on every init; required
37
38
  // for foreign-built DBs where the version-gated migration is skipped.
@@ -60,6 +61,7 @@ export const initializeRuntimeDatabase = (db) => {
60
61
  workspace_id TEXT NOT NULL,
61
62
  name TEXT NOT NULL,
62
63
  description TEXT,
64
+ avatar TEXT,
63
65
  last_session_id TEXT,
64
66
  role TEXT NOT NULL,
65
67
  created_at INTEGER NOT NULL
@@ -239,6 +241,7 @@ export const initializeRuntimeDatabase = (db) => {
239
241
  // migration leaves these absent. PRAGMA + ALTER here makes the writes safe.
240
242
  ensureColumn(db, 'workers', 'ephemeral', 'INTEGER NOT NULL DEFAULT 0');
241
243
  ensureColumn(db, 'workers', 'spawned_by', 'TEXT');
244
+ ensureColumn(db, 'workers', 'avatar', 'TEXT');
242
245
  ensureColumn(db, 'dispatches', 'workflow_run_id', 'TEXT');
243
246
  ensureColumn(db, 'dispatches', 'step_index', 'INTEGER');
244
247
  // M9 — phase + label on dispatches so the workflow UI can render the
@@ -449,4 +452,8 @@ export const initializeRuntimeDatabase = (db) => {
449
452
  if (!appliedVersions.has(38)) {
450
453
  db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(38, Date.now());
451
454
  }
455
+ applySchemaVersion39(db);
456
+ if (!appliedVersions.has(39)) {
457
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(39, Date.now());
458
+ }
452
459
  };
@@ -1,2 +1,6 @@
1
1
  import type { TeamListItem, TeamListItemPayload, TeamListOpenDispatchPayload } from '../shared/types.js';
2
- export declare const serializeTeamListItem: ({ commandPresetId, ephemeral, id, lastPtyLine, name, pendingTaskCount, role, spawnedBy, status, }: TeamListItem, openDispatches?: TeamListOpenDispatchPayload[]) => TeamListItemPayload;
2
+ type SerializeTeamListItemOptions = {
3
+ includeAvatar?: boolean;
4
+ };
5
+ export declare const serializeTeamListItem: ({ avatar, commandPresetId, ephemeral, id, lastPtyLine, name, pendingTaskCount, role, spawnedBy, status, }: TeamListItem, openDispatches?: TeamListOpenDispatchPayload[], options?: SerializeTeamListItemOptions) => TeamListItemPayload;
6
+ export {};
@@ -1,9 +1,10 @@
1
- export const serializeTeamListItem = ({ commandPresetId, ephemeral, id, lastPtyLine, name, pendingTaskCount, role, spawnedBy, status, }, openDispatches) => ({
1
+ export const serializeTeamListItem = ({ avatar, commandPresetId, ephemeral, id, lastPtyLine, name, pendingTaskCount, role, spawnedBy, status, }, openDispatches, options = {}) => ({
2
2
  id,
3
3
  name,
4
4
  role,
5
5
  status,
6
6
  pending_task_count: pendingTaskCount,
7
+ ...(options.includeAvatar === true && avatar ? { avatar } : {}),
7
8
  last_pty_line: lastPtyLine ?? null,
8
9
  command_preset_id: commandPresetId ?? null,
9
10
  ...(ephemeral === true ? { ephemeral: true } : {}),
@@ -1,3 +1,4 @@
1
+ import { DREAM_MEMORY_BODY_MAX_CHARS, DREAM_MEMORY_MAX_ADDS_PER_RUN, MEMORY_PROCEDURE_REF_ID_MAX_CHARS, MEMORY_PROCEDURE_REF_TITLE_MAX_CHARS, MEMORY_TAG_MAX_CHARS, MEMORY_TAG_MAX_COUNT, } from '../shared/team-memory.js';
1
2
  import { escapeHiveEnvelopeAttribute, escapeHiveEnvelopeText } from './hive-envelope-escape.js';
2
3
  export const DREAM_PROMPT_MESSAGE_LIMIT = 200;
3
4
  export const DREAM_PROMPT_MEMORY_LIMIT = 100;
@@ -38,14 +39,25 @@ export const buildDreamPrompt = (input) => [
38
39
  'Read only the protocol messages and memory entries below.',
39
40
  'Return strict JSON only, with shape {"ops":[...]} and no prose.',
40
41
  '',
42
+ 'Output contract (runtime-validated):',
43
+ '- Return exactly {"ops":[...]} as JSON. No markdown, no prose.',
44
+ `- Add no more than ${DREAM_MEMORY_MAX_ADDS_PER_RUN} memories per run.`,
45
+ `- For add/rewrite/merge, body is required and must be <= ${DREAM_MEMORY_BODY_MAX_CHARS} characters. Prefer one compact sentence.`,
46
+ `- If any proposed body would exceed ${DREAM_MEMORY_BODY_MAX_CHARS} characters, shorten it before returning JSON. Never emit an over-limit body.`,
47
+ `- tags must be an array of <= ${MEMORY_TAG_MAX_COUNT} non-empty strings, each <= ${MEMORY_TAG_MAX_CHARS} characters.`,
48
+ '- confidence, when present, must be a number from 0 to 1.',
49
+ '- sources, when present, must be [{"sequence": <positive integer>}] from the protocol window.',
50
+ `- procedure_ref.id must be <= ${MEMORY_PROCEDURE_REF_ID_MAX_CHARS} characters; procedure_ref.title, when present, must be <= ${MEMORY_PROCEDURE_REF_TITLE_MAX_CHARS} characters.`,
51
+ '- For rewrite/merge, omit kind, tags, confidence, and procedure_ref to preserve current values. Use procedure_ref:null only when intentionally clearing an existing ref; provide a structured procedure_ref when the resulting kind is procedure_ref.',
52
+ '',
41
53
  'Allowed ops:',
42
- '- {"op":"add","kind":"fact|preference|decision|pitfall","body":"...","tags":[],"confidence":0.0-1.0,"sources":[{"sequence":123}]}',
43
- '- {"op":"add","kind":"procedure_ref","body":"...","tags":[],"confidence":0.0-1.0,"sources":[{"sequence":123}],"procedure_ref":{"type":"workflow|skill|procedure|template|doc","id":"...","title":"..."}}',
44
- '- {"op":"rewrite","id":"<memory-id>","body":"...","tags":[],"procedure_ref":null}',
54
+ `- {"op":"add","kind":"fact|preference|decision|pitfall","body":"<=${DREAM_MEMORY_BODY_MAX_CHARS} chars","tags":[],"confidence":0.0-1.0,"sources":[{"sequence":123}]}`,
55
+ `- {"op":"add","kind":"procedure_ref","body":"<=${DREAM_MEMORY_BODY_MAX_CHARS} chars","tags":[],"confidence":0.0-1.0,"sources":[{"sequence":123}],"procedure_ref":{"type":"workflow|skill|procedure|template|doc","id":"<=${MEMORY_PROCEDURE_REF_ID_MAX_CHARS} chars","title":"<=${MEMORY_PROCEDURE_REF_TITLE_MAX_CHARS} chars"}}`,
56
+ `- {"op":"rewrite","id":"<memory-id>","body":"<=${DREAM_MEMORY_BODY_MAX_CHARS} chars"}`,
45
57
  '- {"op":"archive","id":"<memory-id>","reason":"..."}',
46
- '- {"op":"merge","into":"<memory-id>","from":["<memory-id>"],"body":"..."}',
58
+ `- {"op":"merge","into":"<memory-id>","from":["<memory-id>"],"body":"<=${DREAM_MEMORY_BODY_MAX_CHARS} chars"}`,
47
59
  '',
48
- 'Rules: add at most 10 entries; every body must be 500 characters or fewer; archive never deletes; if nothing is worth remembering, return {"ops":[]}.',
60
+ `Rules: add at most ${DREAM_MEMORY_MAX_ADDS_PER_RUN} entries; archive never deletes; if nothing is worth remembering, return {"ops":[]}.`,
49
61
  'Do not use kind "procedure_ref" for ordinary workflow advice, command sequences, or procedural lessons. Use fact, decision, preference, or pitfall instead.',
50
62
  'Use kind "procedure_ref" only when the memory points to a durable existing workflow/skill/procedure/template/doc identity, and always include procedure_ref.type plus procedure_ref.id. Keep body as a short reason to consult it.',
51
63
  'Use protocol evidence only: report text, dispatch status, user input, and artifacts. Do not trust self-praise without a concrete report or failure signal.',
@@ -88,6 +100,7 @@ export const buildDreamMaintenancePayload = (run) => [
88
100
  '- Members may run team memory dream show only when you explicitly assign Dream review; only the orchestrator may run team memory apply.',
89
101
  '- Do not edit .hive/memory.md directly and do not use team memory add/forget for this Dream run.',
90
102
  '- If nothing is worth remembering, apply {"ops":[]}.',
103
+ `- Follow the Dream output contract from dream show; every submitted body must be ${DREAM_MEMORY_BODY_MAX_CHARS} characters or fewer.`,
91
104
  '- The runtime will validate the run id, workspace, source window, operation shape, and touched memory ids transactionally.',
92
105
  '',
93
106
  '</hive-system-memory-maintenance>',
@@ -88,7 +88,9 @@ export interface CancelTaskInput {
88
88
  fromAgentId: string;
89
89
  reason: string;
90
90
  }
91
+ export type ReportDeliveryState = 'delivered' | 'delivering' | 'queued' | 'failed';
91
92
  export interface ReportTaskResult {
93
+ deliveryState?: ReportDeliveryState;
92
94
  dispatch: DispatchRecord | null;
93
95
  forwardError: string | null;
94
96
  forwarded: boolean;
@@ -121,11 +123,13 @@ export declare const createTeamOperations: ({ agentRuntime, createDispatch, dele
121
123
  dispatch: null;
122
124
  forwardError: string | null;
123
125
  forwarded: boolean;
126
+ deliveryState?: ReportDeliveryState;
124
127
  };
125
128
  reportTask(workspaceId: string, workerId: string, input?: ReportTaskInput): {
126
129
  pendingWarning?: string;
127
130
  dispatch: DispatchRecord;
128
131
  forwardError: string | null;
129
132
  forwarded: boolean;
133
+ deliveryState?: ReportDeliveryState;
130
134
  };
131
135
  };
@@ -538,6 +538,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
538
538
  const messageHandle = insertMessage(createStatusMessage(workspaceId, workerId, text, artifacts));
539
539
  recordProtocolEvent?.('status');
540
540
  try {
541
+ let deliveryState;
541
542
  let forwardError = null;
542
543
  let forwarded = false;
543
544
  if (input.requireActiveRun === true) {
@@ -545,15 +546,18 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
545
546
  agentRuntime.writeStatusPrompt(workspaceId, worker.name, workerId, text, artifacts, {
546
547
  requireActiveRun: input.requireActiveRun,
547
548
  });
549
+ deliveryState = 'delivered';
548
550
  forwarded = true;
549
551
  }
550
552
  catch (error) {
553
+ deliveryState = 'failed';
551
554
  forwardError = reportForwardErrorMessage(error);
552
555
  console.error('[hive] swallowed:teamStatus.forward', error);
553
556
  }
554
557
  }
555
558
  const pendingWarning = buildPendingWarning(worker.name, worker.pendingTaskCount, 'status update');
556
559
  return {
560
+ ...(deliveryState ? { deliveryState } : {}),
557
561
  dispatch: null,
558
562
  forwardError,
559
563
  forwarded,
@@ -635,6 +639,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
635
639
  const remainingPendingTaskCount = workspaceStore.getWorker(workspaceId, workerId).pendingTaskCount;
636
640
  let forwardError = null;
637
641
  let forwarded = false;
642
+ let deliveryState;
638
643
  const pendingWarning = buildPendingWarning(worker.name, remainingPendingTaskCount, 'report');
639
644
  // Workflow-sourced dispatches: the source is the in-process runner, not a
640
645
  // PTY. Resolve its awaiting Promise instead of injecting into orchestrator
@@ -646,13 +651,16 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
646
651
  text,
647
652
  ...(status ? { status } : {}),
648
653
  });
654
+ deliveryState = 'delivered';
649
655
  forwarded = true;
650
656
  }
651
657
  catch (error) {
658
+ deliveryState = 'failed';
652
659
  forwardError = reportForwardErrorMessage(error);
653
660
  console.error('[hive] swallowed:teamReport.workflowForward', error);
654
661
  }
655
662
  return {
663
+ ...(deliveryState ? { deliveryState } : {}),
656
664
  dispatch: committedDispatch,
657
665
  forwardError,
658
666
  forwarded,
@@ -673,16 +681,20 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
673
681
  if (agentRuntime.getActiveRunByAgentId(workspaceId, orchestratorId)) {
674
682
  const drainResult = drainReportOutbox(workspaceId, orchestratorId);
675
683
  forwarded = false;
676
- forwardError =
677
- drainResult.firstSyncError ??
678
- (reportQueuedBeforeCommit
679
- ? 'Report queued for durable delivery; redelivery is in progress.'
680
- : null);
684
+ if (drainResult.firstSyncError) {
685
+ deliveryState = reportQueuedBeforeCommit ? 'queued' : 'failed';
686
+ forwardError = drainResult.firstSyncError;
687
+ }
688
+ else if (reportQueuedBeforeCommit) {
689
+ deliveryState = 'delivering';
690
+ forwardError = null;
691
+ }
681
692
  }
682
693
  else {
683
694
  // Orchestrator is down. Queue the report instead of dropping it;
684
695
  // the CLI surfaces forwarded:false and the backlog drains on the
685
696
  // orchestrator's next `team list` after it restarts.
697
+ deliveryState = reportQueuedBeforeCommit ? 'queued' : 'failed';
686
698
  forwardError = reportQueuedBeforeCommit
687
699
  ? 'Orchestrator is not running; report queued for delivery.'
688
700
  : 'Orchestrator is not running; report could not be queued for delivery.';
@@ -716,6 +728,7 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
716
728
  });
717
729
  }
718
730
  return {
731
+ ...(deliveryState ? { deliveryState } : {}),
719
732
  dispatch: committedDispatch,
720
733
  forwardError,
721
734
  forwarded,
@@ -4,6 +4,7 @@ export interface WorkspaceRecord {
4
4
  agents: AgentSummary[];
5
5
  }
6
6
  export interface WorkerInput {
7
+ avatar?: string | null;
7
8
  description?: string;
8
9
  name: string;
9
10
  role: WorkerRole;
@@ -17,6 +18,11 @@ export interface WorkspaceStore {
17
18
  deleteWorkspaceData: (workspaceId: string) => void;
18
19
  forgetWorkspace: (workspaceId: string) => void;
19
20
  deleteWorker: (workspaceId: string, workerId: string) => void;
21
+ updateWorkerProfile: (workspaceId: string, workerId: string, input: {
22
+ avatar?: string | null;
23
+ name?: string;
24
+ }) => AgentSummary;
25
+ updateWorkerAvatar: (workspaceId: string, workerId: string, avatar: string | null) => AgentSummary;
20
26
  renameWorker: (workspaceId: string, workerId: string, name: string) => AgentSummary;
21
27
  getAgent: (workspaceId: string, agentId: string) => AgentSummary;
22
28
  getWorker: (workspaceId: string, workerId: string) => AgentSummary;
@@ -8,6 +8,7 @@ const createWorkerSummary = (workspaceId, row) => ({
8
8
  role: row.role,
9
9
  status: 'stopped',
10
10
  pendingTaskCount: 0,
11
+ ...(row.avatar ? { avatar: row.avatar } : {}),
11
12
  ephemeral: row.ephemeral === 1,
12
13
  spawnedBy: row.spawned_by ?? null,
13
14
  });
@@ -22,7 +23,7 @@ const createWorkerSummary = (workspaceId, row) => ({
22
23
  */
23
24
  const buildWorkerSelect = (db, whereClause) => {
24
25
  const present = new Set(db.prepare('PRAGMA table_info(workers)').all().map((c) => c.name));
25
- const optional = ['ephemeral', 'spawned_by'].filter((column) => present.has(column));
26
+ const optional = ['avatar', 'ephemeral', 'spawned_by'].filter((column) => present.has(column));
26
27
  const columns = ['id', 'workspace_id', 'name', 'description', 'role', ...optional].join(', ');
27
28
  const where = whereClause ? `${whereClause} ` : '';
28
29
  return `SELECT ${columns} FROM workers ${where}ORDER BY created_at ASC`;
@@ -15,6 +15,7 @@ export interface WorkerRow {
15
15
  name: string;
16
16
  description: string | null;
17
17
  role: WorkerRole;
18
+ avatar?: string | null;
18
19
  ephemeral: number;
19
20
  spawned_by: string | null;
20
21
  }
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { normalizeWorkerAvatar } from '../shared/worker-avatar.js';
2
3
  import { ConflictError } from './http-errors.js';
3
4
  import { sameFilesystemPath } from './path-canonicalization.js';
4
5
  import { getDefaultRoleDescription } from './role-templates.js';
@@ -78,6 +79,7 @@ export const createWorkspaceStore = (db, listOpenDispatchKindsInput) => {
78
79
  addWorker(workspaceId, input) {
79
80
  const workspace = getWorkspace(workspaceId);
80
81
  const name = normalizeWorkerName(input.name);
82
+ const avatar = normalizeWorkerAvatar(input.avatar);
81
83
  if (workspace.agents.some((agent) => agent.name === name && isWorkerAgent(agent))) {
82
84
  throw new ConflictError(`Worker name already exists: ${name}`);
83
85
  }
@@ -89,10 +91,11 @@ export const createWorkspaceStore = (db, listOpenDispatchKindsInput) => {
89
91
  role: input.role,
90
92
  status: 'stopped',
91
93
  pendingTaskCount: 0,
94
+ ...(avatar ? { avatar } : {}),
92
95
  ephemeral: input.ephemeral ?? false,
93
96
  spawnedBy: input.spawnedBy ?? null,
94
97
  };
95
- db.prepare('INSERT INTO workers (id, workspace_id, name, description, role, ephemeral, spawned_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(worker.id, workspaceId, worker.name, worker.description, worker.role, worker.ephemeral ? 1 : 0, worker.spawnedBy, Date.now());
98
+ db.prepare('INSERT INTO workers (id, workspace_id, name, description, role, avatar, ephemeral, spawned_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)').run(worker.id, workspaceId, worker.name, worker.description, worker.role, avatar, worker.ephemeral ? 1 : 0, worker.spawnedBy, Date.now());
96
99
  workspace.agents.push(worker);
97
100
  return worker;
98
101
  },
@@ -129,6 +132,57 @@ export const createWorkspaceStore = (db, listOpenDispatchKindsInput) => {
129
132
  worker.name = trimmed;
130
133
  return worker;
131
134
  },
135
+ updateWorkerProfile(workspaceId, workerId, input) {
136
+ const workspace = getWorkspace(workspaceId);
137
+ const worker = getWorkerRecord(workspaces, workspaceId, workerId);
138
+ const hasName = Object.hasOwn(input, 'name');
139
+ const hasAvatar = Object.hasOwn(input, 'avatar');
140
+ const trimmed = hasName ? normalizeWorkerName(input.name ?? '') : undefined;
141
+ const avatar = hasAvatar ? normalizeWorkerAvatar(input.avatar) : undefined;
142
+ const nameChanged = trimmed !== undefined && trimmed !== worker.name;
143
+ if (nameChanged &&
144
+ workspace.agents.some((agent) => agent.id !== workerId && agent.name === trimmed && isWorkerAgent(agent))) {
145
+ throw new ConflictError(`Worker name already exists: ${trimmed}`);
146
+ }
147
+ db.transaction(() => {
148
+ if (nameChanged && hasAvatar) {
149
+ db.prepare('UPDATE workers SET name = ?, avatar = ? WHERE workspace_id = ? AND id = ?').run(trimmed, avatar, workspaceId, workerId);
150
+ return;
151
+ }
152
+ if (nameChanged) {
153
+ db.prepare('UPDATE workers SET name = ? WHERE workspace_id = ? AND id = ?').run(trimmed, workspaceId, workerId);
154
+ return;
155
+ }
156
+ if (hasAvatar) {
157
+ db.prepare('UPDATE workers SET avatar = ? WHERE workspace_id = ? AND id = ?').run(avatar, workspaceId, workerId);
158
+ }
159
+ })();
160
+ if (nameChanged && trimmed !== undefined) {
161
+ worker.name = trimmed;
162
+ }
163
+ if (hasAvatar) {
164
+ if (avatar) {
165
+ worker.avatar = avatar;
166
+ }
167
+ else {
168
+ delete worker.avatar;
169
+ }
170
+ }
171
+ return worker;
172
+ },
173
+ updateWorkerAvatar(workspaceId, workerId, inputAvatar) {
174
+ getWorkspace(workspaceId);
175
+ const worker = getWorkerRecord(workspaces, workspaceId, workerId);
176
+ const avatar = normalizeWorkerAvatar(inputAvatar);
177
+ db.prepare('UPDATE workers SET avatar = ? WHERE workspace_id = ? AND id = ?').run(avatar, workspaceId, workerId);
178
+ if (avatar) {
179
+ worker.avatar = avatar;
180
+ }
181
+ else {
182
+ delete worker.avatar;
183
+ }
184
+ return worker;
185
+ },
132
186
  deleteWorker(workspaceId, workerId) {
133
187
  const workspace = getWorkspace(workspaceId);
134
188
  getWorkerRecord(workspaces, workspaceId, workerId);
@@ -161,12 +215,13 @@ export const createWorkspaceStore = (db, listOpenDispatchKindsInput) => {
161
215
  listWorkers(workspaceId) {
162
216
  return getWorkspace(workspaceId)
163
217
  .agents.filter(isWorkerAgent)
164
- .map(({ id, name, role, status, pendingTaskCount, ephemeral, spawnedBy }) => ({
218
+ .map(({ avatar, id, name, role, status, pendingTaskCount, ephemeral, spawnedBy }) => ({
165
219
  id,
166
220
  name,
167
221
  role,
168
222
  status,
169
223
  pendingTaskCount,
224
+ ...(avatar ? { avatar } : {}),
170
225
  // Carry the lifecycle marker through to the team panel so workflow-
171
226
  // spawned ephemeral workers can be visually distinguished from the
172
227
  // user's persistent team (M10).
@@ -18,6 +18,7 @@ export interface AgentSummary {
18
18
  role: WorkerRole | 'orchestrator' | 'workflow';
19
19
  status: AgentStatus;
20
20
  pendingTaskCount: number;
21
+ avatar?: string | null;
21
22
  ephemeral?: boolean;
22
23
  spawnedBy?: WorkerSpawnSource | null;
23
24
  }
@@ -27,6 +28,8 @@ export interface TeamListItem {
27
28
  role: WorkerRole;
28
29
  status: AgentStatus;
29
30
  pendingTaskCount: number;
31
+ /** Optional user-uploaded avatar. When absent/null, the UI uses the CLI preset logo or role icon. */
32
+ avatar?: string | null;
30
33
  /**
31
34
  * Last raw line printed to the worker's PTY. Kept as non-authoritative UI context only —
32
35
  * not a worker reply. Real replies arrive as [Hive 系统消息] entries on orchestrator stdin.
@@ -69,6 +72,7 @@ export interface TeamListItemPayload {
69
72
  role: WorkerRole;
70
73
  status: AgentStatus;
71
74
  pending_task_count: number;
75
+ avatar?: string | null;
72
76
  last_pty_line: string | null;
73
77
  command_preset_id: string | null;
74
78
  ephemeral?: boolean;
@@ -0,0 +1,2 @@
1
+ export declare const WORKER_AVATAR_MAX_CHARS = 160000;
2
+ export declare const normalizeWorkerAvatar: (input: unknown) => string | null;
@@ -0,0 +1,55 @@
1
+ export const WORKER_AVATAR_MAX_CHARS = 160_000;
2
+ const WORKER_AVATAR_DATA_URL_PATTERN = /^data:image\/(png|jpeg|webp);base64,([A-Za-z0-9+/]+={0,2})$/;
3
+ const decodeBase64Bytes = (value) => {
4
+ try {
5
+ const binary = globalThis.atob(value);
6
+ const bytes = new Uint8Array(binary.length);
7
+ for (let index = 0; index < binary.length; index += 1) {
8
+ bytes[index] = binary.charCodeAt(index);
9
+ }
10
+ return bytes;
11
+ }
12
+ catch {
13
+ throw new Error('Worker avatar must be valid base64');
14
+ }
15
+ };
16
+ const hasPrefix = (bytes, prefix) => bytes.length >= prefix.length && prefix.every((value, index) => bytes[index] === value);
17
+ const matchesImageType = (mediaType, bytes) => {
18
+ if (mediaType === 'png')
19
+ return hasPrefix(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
20
+ if (mediaType === 'jpeg')
21
+ return hasPrefix(bytes, [0xff, 0xd8, 0xff]);
22
+ return (bytes.length >= 12 &&
23
+ hasPrefix(bytes, [0x52, 0x49, 0x46, 0x46]) &&
24
+ bytes[8] === 0x57 &&
25
+ bytes[9] === 0x45 &&
26
+ bytes[10] === 0x42 &&
27
+ bytes[11] === 0x50);
28
+ };
29
+ export const normalizeWorkerAvatar = (input) => {
30
+ if (input === null || input === undefined || input === '')
31
+ return null;
32
+ if (typeof input !== 'string') {
33
+ throw new Error('Worker avatar must be an image data URL or null');
34
+ }
35
+ const value = input.trim();
36
+ if (!value)
37
+ return null;
38
+ if (value.length > WORKER_AVATAR_MAX_CHARS) {
39
+ throw new Error('Worker avatar is too large');
40
+ }
41
+ const match = WORKER_AVATAR_DATA_URL_PATTERN.exec(value);
42
+ if (!match) {
43
+ throw new Error('Worker avatar must be a PNG, JPEG, or WebP data URL');
44
+ }
45
+ const mediaType = match[1];
46
+ const base64Body = match[2];
47
+ if (!mediaType || !base64Body) {
48
+ throw new Error('Worker avatar must be a PNG, JPEG, or WebP data URL');
49
+ }
50
+ const bytes = decodeBase64Bytes(base64Body);
51
+ if (!matchesImageType(mediaType, bytes)) {
52
+ throw new Error('Worker avatar data does not match its image type');
53
+ }
54
+ return value;
55
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tt-a1i/hive",
3
- "version": "2.1.14",
3
+ "version": "2.1.16",
4
4
  "description": "Run Claude Code, Codex, Gemini, OpenCode, Qwen, Pi, and other CLI agents as a visible local team in your browser.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.30.3",
@@ -0,0 +1,2 @@
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/MarketplaceDrawer-DAWF9idk.js","assets/index-DoOf8jsj.js","assets/index-Cs7E1UKt.css","assets/search-CPePg8x3.js"])))=>i.map(i=>d[i]);
2
+ import{c as M,j as e,u as y,r as c,C as L,x as P,t as R,H as ee,K as te,ae,af as re,e as se,i as ne,R as oe,P as le,O as ie,k as de,m as ce,_ as me,z as xe,X as ue,T as pe,ag as he,ah as fe}from"./index-DoOf8jsj.js";import{S as ge}from"./search-CPePg8x3.js";import{S as be}from"./square-terminal-DwJxHx2m.js";const ve=[["path",{d:"M12 7v6",key:"lw1j43"}],["path",{d:"M15 10H9",key:"o6yqo3"}],["path",{d:"M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z",key:"oz39mx"}]],ke=M("bookmark-plus",ve);const je=[["rect",{width:"12",height:"12",x:"2",y:"10",rx:"2",ry:"2",key:"6agr2n"}],["path",{d:"m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6",key:"1o487t"}],["path",{d:"M6 18h.01",key:"uhywen"}],["path",{d:"M10 14h.01",key:"ssrbsk"}],["path",{d:"M15 6h.01",key:"cblpky"}],["path",{d:"M18 9h.01",key:"2061c0"}]],ye=M("dices",je);const Ne=[["path",{d:"M15 21v-5a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v5",key:"slp6dd"}],["path",{d:"M17.774 10.31a1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.451 0 1.12 1.12 0 0 0-1.548 0 2.5 2.5 0 0 1-3.452 0 1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.77-3.248l2.889-4.184A2 2 0 0 1 7 2h10a2 2 0 0 1 1.653.873l2.895 4.192a2.5 2.5 0 0 1-3.774 3.244",key:"o0xfot"}],["path",{d:"M4 10.95V19a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8.05",key:"wn3emo"}]],Ce=M("store",Ne),we=[{value:"coder"},{value:"reviewer"},{value:"tester"},{value:"sentinel"},{value:"custom",dashed:!0}],B=t=>`role.${t}`,z=({children:t})=>e.jsx("span",{className:"text-sm font-medium text-sec",children:t}),We=({active:t,spec:a,onSelect:s})=>{const{t:n}=y();return e.jsxs("button",{type:"button",onClick:s,"aria-pressed":t,"data-testid":`role-card-${a.value}`,className:`selectable-card${a.dashed?" selectable-card--dashed":""} flex items-center gap-3 px-3 py-2`,children:[e.jsx(ae,{role:a.value,size:20}),e.jsx("span",{className:"flex-1 text-left text-base font-medium text-pri",children:n(B(a.value))}),t?e.jsx(P,{size:14,className:"shrink-0 text-accent","aria-hidden":!0}):null]})},Se=({onRoleChange:t,workerRole:a})=>{const{t:s}=y();return e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsx(z,{children:s("addWorker.role")}),e.jsx("div",{className:"grid grid-cols-2 gap-2",children:we.map(n=>e.jsx(We,{active:a===n.value,spec:n,onSelect:()=>t(n.value)},n.value))})]})},ze=({customTemplates:t,disabledReason:a,onDeleteTemplate:s,onSelect:n,selectedTemplateId:l})=>{const{t:o}=y(),[f,m]=c.useState(!1),[x,u]=c.useState(""),[b,j]=c.useState(null),N=c.useRef(null),C=c.useMemo(()=>t.find(r=>r.id===l)??null,[t,l]),v=c.useMemo(()=>{const r=x.trim().toLowerCase();return r?t.filter(p=>p.name.toLowerCase().includes(r)||p.description.toLowerCase().includes(r)):t},[t,x]);return c.useEffect(()=>{if(!f)return;const r=g=>{g.key==="Escape"&&m(!1)},p=g=>{const i=N.current;i&&!i.contains(g.target)&&m(!1)};return document.addEventListener("keydown",r),document.addEventListener("pointerdown",p),()=>{document.removeEventListener("keydown",r),document.removeEventListener("pointerdown",p)}},[f]),e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsx(z,{children:o("addWorker.template")}),e.jsxs("div",{ref:N,className:"relative",children:[e.jsxs("button",{type:"button","aria-haspopup":"listbox","aria-expanded":f,"data-testid":"role-template-picker-trigger",onClick:()=>m(r=>!r),className:"flex w-full items-center justify-between gap-2 rounded border px-3 py-2 text-left text-sm transition-colors hover:bg-3",style:{borderColor:"var(--border)",background:"var(--bg-1)"},children:[e.jsx("span",{className:"min-w-0 flex-1 truncate text-pri",children:C?C.name:o("addWorker.templatePickPlaceholder")}),e.jsx(L,{size:14,className:"shrink-0 text-ter","aria-hidden":!0})]}),f?e.jsxs("div",{role:"listbox","aria-label":o("addWorker.template"),"data-testid":"role-template-picker-menu",className:"elev-2 absolute left-0 right-0 top-full z-30 mt-1 flex max-h-72 flex-col overflow-hidden rounded border",style:{background:"var(--bg-elevated)",borderColor:"var(--border-bright)"},children:[e.jsxs("div",{className:"flex items-center gap-2 border-b px-2 py-1.5",style:{borderColor:"var(--border)"},children:[e.jsx(ge,{size:14,className:"text-ter","aria-hidden":!0}),e.jsx("input",{value:x,onChange:r=>u(r.currentTarget.value),placeholder:o("addWorker.templateSearchPlaceholder"),"data-testid":"role-template-search-input",className:"w-full bg-transparent text-sm text-pri outline-none placeholder:text-ter",spellCheck:!1})]}),e.jsx("div",{className:"flex-1 overflow-y-auto py-1",children:t.length===0?e.jsx("div",{"data-testid":"role-template-empty-state",className:"px-3 py-3 text-center text-sm text-ter",children:o("addWorker.templateEmpty")}):v.length===0?e.jsx("div",{className:"px-3 py-3 text-center text-sm text-ter",children:o("addWorker.templateNoMatch")}):v.map(r=>{const p=r.id===l;return e.jsxs("div",{className:"relative",children:[e.jsxs("button",{type:"button",role:"option","aria-selected":p,"data-testid":`role-template-option-${r.id}`,onClick:()=>{n(r.id),m(!1),u("")},className:"flex w-full items-center gap-2 px-3 py-1.5 pr-9 text-left text-sm text-pri hover:bg-3",style:p?{background:"var(--bg-3)"}:void 0,children:[e.jsx("span",{className:"min-w-0 flex-1 truncate",children:r.name}),p?e.jsx(P,{size:14,className:"shrink-0 text-accent","aria-hidden":!0}):null]}),e.jsx("button",{type:"button","aria-label":o("addWorker.templateDeleteAria",{name:r.name}),"data-testid":`role-template-delete-${r.id}`,disabled:!!a,title:a??void 0,onClick:g=>{g.preventDefault(),g.stopPropagation(),!a&&j(r)},className:"absolute right-1 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded text-ter transition-colors hover:bg-3 hover:text-pri",children:e.jsx(R,{size:14,"aria-hidden":!0})})]},r.id)})}),l!==null?e.jsx("button",{type:"button","data-testid":"role-template-clear",onClick:()=>{n(null),m(!1),u("")},className:"border-t px-3 py-1.5 text-left text-sm text-ter transition-colors hover:bg-3 hover:text-pri",style:{borderColor:"var(--border)"},children:o("addWorker.templateClear")}):null]}):null]}),e.jsx(ee,{open:b!==null,onOpenChange:r=>{r||j(null)},title:o("addWorker.templateDeleteTitle"),description:b?o("addWorker.templateDeleteConfirm",{name:b.name}):"",confirmLabel:o("addWorker.templateDeleteConfirmLabel"),confirmKind:"danger",onConfirm:()=>{if(!b||a)return;const r=b.id;j(null),s(r)}})]})},Me=({canSaveAsTemplate:t,modified:a,onChange:s,onReset:n,onSaveAsTemplate:l,roleDescription:o,templateBusy:f,workerRole:m,writeDisabledReason:x})=>{const{t:u,language:b}=y(),[j,N]=c.useState(!1),[C,v]=c.useState(!1),[r,p]=c.useState("");c.useEffect(()=>{(m==="custom"||a)&&N(!0)},[a,m]),c.useEffect(()=>{t||(v(!1),p(""))},[t]);const g=b==="zh"?"点击展开":"Click to expand";return e.jsxs("details",{open:j,onToggle:i=>N(i.currentTarget.open),className:"group flex flex-col gap-2",children:[e.jsxs("summary",{className:"flex cursor-pointer select-none items-center justify-between gap-2 list-none rounded-lg p-2 -mx-2 hover:bg-3/50 transition-colors group/summary",children:[e.jsxs("span",{className:"flex items-center gap-2",children:[e.jsx("span",{className:"flex h-5 w-5 items-center justify-center rounded bg-3 border border-bright/30 text-ter transition-colors group-hover/summary:text-pri group-hover/summary:border-accent/40",children:e.jsx(L,{size:12,"aria-hidden":!0,className:"-rotate-90 transition-transform duration-150 group-open:rotate-0"})}),e.jsx("span",{className:"text-sm font-semibold text-pri transition-colors group-hover/summary:text-accent",children:u("addWorker.roleInstructions")}),a?e.jsxs("span",{className:"text-xs text-ter",children:["· ",u("addWorker.modifiedFrom",{role:u(B(m))})]}):null]}),e.jsxs("div",{className:"flex items-center gap-2",children:[j?null:e.jsx("span",{className:"text-[10px] text-ter font-semibold tracking-wider uppercase opacity-60 group-hover/summary:opacity-100 group-hover/summary:text-accent transition-all pr-1",children:g}),t&&!C?e.jsxs("button",{type:"button","data-testid":"role-template-save",disabled:!!x,title:x??void 0,onClick:i=>{i.preventDefault(),i.stopPropagation(),v(!0)},className:"flex items-center gap-1 rounded px-2 py-0.5 text-xs font-medium transition-colors hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50",style:{color:"var(--accent)",background:"color-mix(in oklab, var(--accent) 14%, transparent)"},children:[e.jsx(ke,{size:12,"aria-hidden":!0}),u("addWorker.saveAsTemplate")]}):null,a?e.jsxs("button",{type:"button",className:"flex items-center gap-1 rounded px-1.5 py-0.5 text-xs text-ter transition-colors hover:bg-3 hover:text-sec",onClick:i=>{i.preventDefault(),i.stopPropagation(),n()},children:[e.jsx(te,{size:12,"aria-hidden":!0}),u("addWorker.reset")]}):null]})]}),e.jsx("textarea",{"aria-label":"Role instructions",id:"add-worker-role-instructions",value:o,rows:5,onChange:i=>s(i.currentTarget.value),placeholder:m==="custom"?u("addWorker.customPlaceholder"):void 0,title:u("addWorker.roleInstructionsTitle"),className:"input mono resize-y text-sm",style:{minHeight:150},"data-testid":"role-instructions-textarea"}),t&&C?e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("input",{autoFocus:!0,value:r,onChange:i=>p(i.currentTarget.value),placeholder:u("addWorker.templateNamePlaceholder"),"data-testid":"role-template-save-name",className:"input flex-1 text-sm"}),e.jsx("button",{type:"button",disabled:f||!r.trim()||!!x,title:x??void 0,"data-testid":"role-template-save-confirm",onClick:async()=>{if(x)return;const i=r.trim();if(i)try{await l(i),v(!1),p("")}catch{}},className:"icon-btn icon-btn--primary text-xs",children:u("addWorker.templateSaveConfirm")}),e.jsx("button",{type:"button","data-testid":"role-template-save-cancel",onClick:()=>{v(!1),p("")},className:"icon-btn text-xs",children:u("common.cancel")})]}):null]})},q=({active:t,command:a,displayName:s,logoPresetId:n,notFound:l=!1,testId:o,onSelect:f})=>{const{t:m}=y(),x=e.jsx("span",{className:"inline-flex h-5 w-5 shrink-0 items-center justify-center rounded border border-border bg-surface-1 text-ter","data-testid":`${o}-generic-icon`,"aria-hidden":!0,children:e.jsx(be,{size:13})});return e.jsxs("button",{type:"button",onClick:f,"aria-pressed":t,"data-testid":o,className:"selectable-card flex items-center justify-between gap-3 px-3 py-2",children:[e.jsxs("span",{className:"flex min-w-0 items-center gap-3",children:[e.jsx(re,{commandPresetId:n,fallback:x,size:22}),e.jsxs("span",{className:"flex min-w-0 flex-col items-start gap-0.5",children:[e.jsx("span",{className:"truncate text-base font-medium text-pri",children:s}),e.jsxs("span",{className:"mono truncate text-xs text-ter",children:[a,l?` · ${m("addWorker.agentNotFound")}`:""]})]})]}),t?e.jsx(P,{size:14,className:"shrink-0 text-accent","aria-hidden":!0}):null]})},Le=({active:t,preset:a,onSelect:s})=>e.jsx(q,{active:t,command:a.command,displayName:a.displayName,logoPresetId:a.id,notFound:a.available===!1,testId:`agent-radio-${a.id}`,onSelect:s}),Pe=({commandPresetId:t,commandPresets:a,onPresetChange:s})=>{const{t:n}=y();return e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsx(z,{children:n("addWorker.agentCli")}),a.length===0?e.jsx("div",{className:"text-sm text-ter",children:n("addWorker.loadingPresets")}):e.jsxs("div",{className:"grid grid-cols-2 gap-2 max-md:grid-cols-1",children:[a.map(l=>e.jsx(Le,{active:t===l.id,preset:l,onSelect:()=>s(l.id)},l.id)),e.jsx(q,{active:t==="",command:n("addWorker.genericCommand"),displayName:n("addWorker.genericAgent"),testId:"agent-radio-generic",onSelect:()=>s("")})]})]})},_e=({onChange:t,value:a})=>{const{t:s,language:n}=y(),[l,o]=c.useState(!1),f=a.trim(),m=n==="zh"?"点击展开":"Click to expand";return e.jsxs("details",{onToggle:x=>o(x.currentTarget.open),className:"group flex flex-col gap-2",children:[e.jsxs("summary",{className:"flex cursor-pointer select-none items-center justify-between gap-2 list-none rounded-lg p-2 -mx-2 hover:bg-3/50 transition-colors group/summary",children:[e.jsxs("span",{className:"flex min-w-0 items-center gap-2",children:[e.jsx("span",{className:"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-3 border border-bright/30 text-ter transition-colors group-hover/summary:text-pri group-hover/summary:border-accent/40",children:e.jsx(L,{size:12,"aria-hidden":!0,className:"-rotate-90 transition-transform duration-150 group-open:rotate-0"})}),e.jsx("span",{className:"text-sm font-semibold text-pri transition-colors group-hover/summary:text-accent",children:s("addWorker.startupCommand")}),f?e.jsxs("span",{className:"truncate text-xs text-ter",children:["· ",s("addWorker.startupOverrides")]}):null]}),l?null:e.jsx("span",{className:"text-[10px] text-ter font-semibold tracking-wider uppercase opacity-60 group-hover/summary:opacity-100 group-hover/summary:text-accent transition-all pr-1",children:m})]}),e.jsxs("div",{className:"flex flex-col gap-2 rounded border bg-2 p-3",style:{borderColor:"var(--border)"},children:[e.jsx("input",{"aria-label":"Startup command",value:a,onChange:x=>t(x.currentTarget.value),placeholder:"qwen --model qwen3-coder",className:"input mono text-sm",spellCheck:!1}),e.jsx("p",{className:"text-sm leading-5 text-ter",children:s("addWorker.startupHelp",{example:"claude --resume <session-id>"})})]})]})},Ae=c.lazy(()=>fe(()=>import("./MarketplaceDrawer-DAWF9idk.js"),__vite__mapDeps([0,1,2,3])).then(t=>({default:t.MarketplaceDrawer}))),Te=({avatar:t,commandPresets:a,commandPresetId:s,creating:n=!1,customTemplates:l,onApplyMarketplaceImport:o,onAvatarChange:f,onClose:m,onDeleteTemplate:x,onNameChange:u,onPresetChange:b,onRandomName:j,onRoleDescriptionChange:N,onRoleDescriptionReset:C,onRoleChange:v,onSaveAsTemplate:r,onStartupCommandChange:p,onSubmit:g,onTemplateChange:i,roleDescription:S,roleDescriptionDefault:D,selectedTemplateId:_,startupCommand:A,templateBusy:H,workerName:E,workerRole:W,writeDisabledReason:k})=>{const{t:d}=y(),O=se(),w=ne(),[K,$]=c.useState(!1),[V,Q]=c.useState(!1),X=c.useMemo(()=>new Set(l.map(h=>h.name)),[l]),G=h=>{o(h),O.show({kind:"success",message:d("marketplace.imported",{name:h.name})})},J=h=>{h||m()},U=S!==D,T=a.find(h=>h.id===s),F=A.trim(),Y=()=>k||(E.trim()?!s&&!F?d("addWorker.pickCliOrStartup"):T?.available===!1&&!F?d("addWorker.unavailable",{name:T.displayName}):S.trim()?null:d("addWorker.emptyInstructions"):d("addWorker.enterName")),Z=h=>{const I=Y();if(I){h.preventDefault(),O.show({kind:"warning",message:I});return}g(h)};return e.jsxs(oe,{open:!0,onOpenChange:J,children:[e.jsxs(le,{children:[e.jsx(ie,{"data-testid":"add-worker-overlay",className:"app-overlay fixed inset-0 z-40"}),e.jsx("div",{className:"pointer-events-none fixed inset-0 z-50 grid place-items-center p-4 max-md:items-end max-md:p-0",children:e.jsx(de,{"data-testid":"add-worker-content","data-mobile":w||void 0,className:`${w?"dialog-slide-up add-worker-sheet":"dialog-scale-pop"} elev-2 pointer-events-auto flex max-h-[calc(100vh-32px)] w-[560px] max-w-full flex-col overflow-hidden rounded-lg border pointer-coarse:max-h-[85dvh] max-md:w-full max-md:rounded-b-none max-md:rounded-t-xl`,style:{background:"var(--bg-elevated)",borderColor:"var(--border-bright)",...w?{height:"85dvh"}:{}},children:e.jsxs("form",{onSubmit:Z,"aria-label":d("addWorker.title"),className:"flex min-h-0 flex-1 flex-col overflow-hidden max-h-full",children:[e.jsxs("div",{className:"flex shrink-0 items-start justify-between gap-3 border-b px-5 py-4 max-md:px-4",style:{borderColor:"var(--border)"},children:[e.jsxs("div",{className:"flex min-w-0 flex-col gap-0.5",children:[e.jsx(ce,{className:"text-lg font-semibold text-pri",children:d("addWorker.title")}),e.jsx(me,{className:"text-sm text-ter",children:d("addWorker.description",{command:"team send"})})]}),w?e.jsx(xe,{asChild:!0,children:e.jsx("button",{type:"button","aria-label":d("common.closeDialog"),"data-testid":"add-worker-close",className:"-mr-1 flex h-10 w-10 shrink-0 items-center justify-center rounded-md text-sec",style:{background:"var(--bg-2)"},children:e.jsx(ue,{size:18,"aria-hidden":!0})})}):null]}),e.jsxs("div",{className:"flex flex-1 min-h-0 flex-col gap-4 overflow-y-auto px-5 py-4 max-md:gap-5 max-md:px-4",children:[e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsx(z,{children:d("addWorker.name")}),e.jsxs("div",{className:"relative flex items-center",children:[e.jsx("input",{autoFocus:!w,value:E,onChange:h=>u(h.target.value),placeholder:d("addWorker.namePlaceholder"),className:"input w-full pr-24",style:{borderRadius:"10px"}}),e.jsx("div",{className:"absolute right-1.5 top-1/2 -translate-y-1/2",children:e.jsx(pe,{label:d("addWorker.randomTooltip"),children:e.jsxs("button",{type:"button","aria-label":d("addWorker.randomAria"),className:"flex h-7 items-center gap-1.5 rounded-lg border border-bright/10 bg-3 px-2.5 text-xs font-semibold text-sec hover:text-pri hover:bg-4 active:scale-95 transition-all outline-none",onClick:j,"data-testid":"random-worker-name",children:[e.jsx(ye,{size:13,"aria-hidden":!0}),e.jsx("span",{children:d("addWorker.random")})]})})})]})]}),e.jsx(Se,{workerRole:W,onRoleChange:v}),e.jsxs("button",{type:"button",onClick:()=>{Q(!0),$(!0)},"data-testid":"open-marketplace",className:"marketplace-browse-btn flex cursor-pointer items-center gap-2 self-start rounded-lg border px-3 py-2 text-xs font-semibold text-sec outline-none transition-all duration-200 hover:text-pri hover:-translate-y-0.5 active:scale-98 shadow-sm hover:shadow-md",style:{background:"linear-gradient(to bottom, var(--bg-1), var(--bg-0))",borderColor:"var(--border-bright)","--tw-ring-color":"color-mix(in oklab, var(--accent) 45%, transparent)"},children:[e.jsx(Ce,{size:14,"aria-hidden":!0,className:"text-accent"}),e.jsx("span",{children:d("marketplace.openFromAddWorker")})]}),W==="custom"?e.jsx(ze,{customTemplates:l,onDeleteTemplate:x,onSelect:i,selectedTemplateId:_,...k?{disabledReason:k}:{}}):null,e.jsx(Me,{canSaveAsTemplate:W==="custom"&&!_&&S.trim().length>0,modified:U,onChange:N,onReset:C,onSaveAsTemplate:r,roleDescription:S,templateBusy:H,workerRole:W,...k?{writeDisabledReason:k}:{}}),e.jsx(he,{avatar:t,commandPresetId:s||void 0,disabled:!!k,onChange:f,workerRole:W,showStatus:!0}),e.jsx(Pe,{commandPresetId:s,commandPresets:a,onPresetChange:b}),e.jsx(_e,{value:A,onChange:p})]}),e.jsxs("div",{className:"flex shrink-0 items-center justify-end gap-2 border-t px-5 py-3 max-md:px-4 max-md:pb-[max(12px,env(safe-area-inset-bottom))]",style:{borderColor:"var(--border)",background:"var(--bg-2)"},children:[e.jsx("button",{type:"button",onClick:m,className:`icon-btn border border-bright/20 rounded-lg hover:bg-3 hover:text-pri transition-all active:scale-95 ${w?"flex-1":""}`,"data-testid":"add-worker-cancel",children:d("addWorker.cancel")}),e.jsx("button",{type:"submit",disabled:n||!!k,title:k??void 0,className:`icon-btn icon-btn--primary rounded-lg font-bold shadow-md hover:shadow-lg transition-all active:scale-[0.97] hover:-translate-y-0.5 ${w?"flex-[2]":""}`,"data-testid":"add-worker-submit",children:d(n?"addWorker.creating":"addWorker.create")})]})]})})})]}),V?e.jsx(c.Suspense,{fallback:null,children:e.jsx(Ae,{open:K,onClose:()=>$(!1),onImport:G,importedNames:X})}):null]})};export{Te as AddWorkerDialog};