@promptbook/cli 0.114.0-21 → 0.114.0-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 (43) hide show
  1. package/esm/index.es.js +780 -103
  2. package/esm/index.es.js.map +1 -1
  3. package/esm/scripts/run-codex-prompts/prompts/buildPromptStatusDetails.d.ts +5 -4
  4. package/esm/scripts/run-codex-prompts/prompts/markPromptFailed.d.ts +3 -2
  5. package/esm/scripts/run-codex-prompts/prompts/promptRunnerAttribution.d.ts +14 -8
  6. package/esm/scripts/run-codex-prompts/runners/claude-code/ClaudeCodeRunner.d.ts +15 -0
  7. package/esm/scripts/run-codex-prompts/runners/claude-code/parseClaudeCodeOutputEvents.d.ts +3 -0
  8. package/esm/scripts/run-codex-prompts/runners/claude-code/parseClaudeCodeSubscriptionUsage.d.ts +11 -0
  9. package/esm/scripts/run-codex-prompts/runners/openai-codex/OpenAiCodexRunner.d.ts +8 -0
  10. package/esm/scripts/run-codex-prompts/runners/openai-codex/getCodexSubscriptionUsage.d.ts +19 -0
  11. package/esm/scripts/run-codex-prompts/runners/types/HarnessSubscriptionUsage.d.ts +43 -0
  12. package/esm/scripts/run-codex-prompts/ui/CoderRunUiState.d.ts +6 -0
  13. package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +2 -0
  14. package/esm/scripts/run-codex-prompts/ui/buildSubscriptionUsageSessionRows.d.ts +11 -0
  15. package/esm/scripts/run-codex-prompts/ui/refreshCoderRunUiSubscriptionUsage.d.ts +14 -0
  16. package/esm/scripts/utils/emojiTags/validSinglePictogramEmojis.d.ts +17 -0
  17. package/esm/src/utils/misc/emojis.d.ts +3 -2
  18. package/esm/src/version.d.ts +1 -1
  19. package/package.json +1 -1
  20. package/src/other/templates/getTemplatesPipelineCollection.ts +859 -822
  21. package/src/utils/misc/emojis.ts +3 -3
  22. package/src/version.ts +2 -2
  23. package/src/versions.txt +2 -0
  24. package/umd/index.umd.js +779 -102
  25. package/umd/index.umd.js.map +1 -1
  26. package/umd/scripts/run-codex-prompts/prompts/buildPromptStatusDetails.d.ts +5 -4
  27. package/umd/scripts/run-codex-prompts/prompts/markPromptFailed.d.ts +3 -2
  28. package/umd/scripts/run-codex-prompts/prompts/promptRunnerAttribution.d.ts +14 -8
  29. package/umd/scripts/run-codex-prompts/runners/claude-code/ClaudeCodeRunner.d.ts +15 -0
  30. package/umd/scripts/run-codex-prompts/runners/claude-code/parseClaudeCodeOutputEvents.d.ts +3 -0
  31. package/umd/scripts/run-codex-prompts/runners/claude-code/parseClaudeCodeSubscriptionUsage.d.ts +11 -0
  32. package/umd/scripts/run-codex-prompts/runners/openai-codex/OpenAiCodexRunner.d.ts +8 -0
  33. package/umd/scripts/run-codex-prompts/runners/openai-codex/getCodexSubscriptionUsage.d.ts +19 -0
  34. package/umd/scripts/run-codex-prompts/runners/types/HarnessSubscriptionUsage.d.ts +43 -0
  35. package/umd/scripts/run-codex-prompts/ui/CoderRunUiState.d.ts +6 -0
  36. package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +2 -0
  37. package/umd/scripts/run-codex-prompts/ui/buildSubscriptionUsageSessionRows.d.ts +11 -0
  38. package/umd/scripts/run-codex-prompts/ui/refreshCoderRunUiSubscriptionUsage.d.ts +14 -0
  39. package/umd/scripts/utils/emojiTags/validSinglePictogramEmojis.d.ts +17 -0
  40. package/umd/src/utils/misc/emojis.d.ts +3 -2
  41. package/umd/src/version.d.ts +1 -1
  42. package/esm/scripts/find-fresh-emoji-tags/utils/emojis.d.ts +0 -16
  43. package/umd/scripts/find-fresh-emoji-tags/utils/emojis.d.ts +0 -16
package/umd/index.umd.js CHANGED
@@ -58,7 +58,7 @@
58
58
  * @generated
59
59
  * @see https://github.com/webgptorg/promptbook
60
60
  */
61
- const PROMPTBOOK_ENGINE_VERSION = '0.114.0-21';
61
+ const PROMPTBOOK_ENGINE_VERSION = '0.114.0-23';
62
62
  /**
63
63
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
64
64
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -25557,6 +25557,29 @@
25557
25557
  }
25558
25558
  }
25559
25559
 
25560
+ /**
25561
+ * Merges a newly reported subset of subscription windows into the latest complete snapshot.
25562
+ *
25563
+ * Some harnesses emit an update only for the limit whose state changed. Keeping the previous windows means a
25564
+ * five-hour update cannot make a still-valid weekly limit disappear from the dashboard. Existing order is preserved
25565
+ * and newly discovered windows are appended, which keeps the terminal presentation stable between refreshes.
25566
+ *
25567
+ * @private internal utility of `ptbk coder`
25568
+ */
25569
+ function mergeHarnessSubscriptionUsage(previousSubscriptionUsage, latestSubscriptionUsage) {
25570
+ if (previousSubscriptionUsage === undefined) {
25571
+ return latestSubscriptionUsage;
25572
+ }
25573
+ const latestLimitsByLabel = new Map(latestSubscriptionUsage.limits.map((latestLimit) => [latestLimit.label, latestLimit]));
25574
+ const previousLimitLabels = new Set(previousSubscriptionUsage.limits.map((previousLimit) => previousLimit.label));
25575
+ return {
25576
+ limits: [
25577
+ ...previousSubscriptionUsage.limits.map((previousLimit) => { var _a; return (_a = latestLimitsByLabel.get(previousLimit.label)) !== null && _a !== void 0 ? _a : previousLimit; }),
25578
+ ...latestSubscriptionUsage.limits.filter((latestLimit) => !previousLimitLabels.has(latestLimit.label)),
25579
+ ],
25580
+ };
25581
+ }
25582
+
25560
25583
  /**
25561
25584
  * Base delimiter used for passing large prompts through stdin.
25562
25585
  */
@@ -25890,6 +25913,122 @@
25890
25913
  }
25891
25914
  }
25892
25915
 
25916
+ /**
25917
+ * Human-readable labels for known Claude Code rolling subscription-limit windows.
25918
+ */
25919
+ const CLAUDE_CODE_RATE_LIMIT_LABELS = {
25920
+ five_hour: '5h',
25921
+ seven_day: '7d',
25922
+ seven_day_opus: '7d Opus',
25923
+ seven_day_sonnet: '7d Sonnet',
25924
+ overage: 'Overage',
25925
+ };
25926
+ /**
25927
+ * Fallback label used when a newer Claude Code release adds a rate-limit window unknown to Promptbook.
25928
+ */
25929
+ const UNKNOWN_CLAUDE_CODE_RATE_LIMIT_LABEL = 'Limit';
25930
+ /**
25931
+ * Converts Claude Code stream-json rate-limit events into the shared subscription-usage snapshot.
25932
+ *
25933
+ * Claude Code can emit one event for each applicable limit, so the parser preserves every distinct window instead of
25934
+ * assuming the familiar 5-hour and seven-day pair. This lets a new vendor-side limit appear in the dashboard without
25935
+ * changing its renderer.
25936
+ *
25937
+ * @private internal utility of the Claude Code runner
25938
+ */
25939
+ function parseClaudeCodeSubscriptionUsage(output) {
25940
+ const limitsByType = new Map();
25941
+ for (const event of parseClaudeCodeOutputEvents(output)) {
25942
+ const limit = parseClaudeCodeSubscriptionUsageLimit(event);
25943
+ if (limit) {
25944
+ limitsByType.set(resolveClaudeCodeRateLimitType(event), limit);
25945
+ }
25946
+ }
25947
+ const limits = [...limitsByType.values()];
25948
+ return limits.length === 0 ? undefined : { limits };
25949
+ }
25950
+ /**
25951
+ * Parses one Claude Code stream event when it contains a usable subscription-limit update.
25952
+ *
25953
+ * @private helper of `parseClaudeCodeSubscriptionUsage`
25954
+ */
25955
+ function parseClaudeCodeSubscriptionUsageLimit(event) {
25956
+ if (event.type !== 'rate_limit_event' || !event.rate_limit_info) {
25957
+ return undefined;
25958
+ }
25959
+ const rateLimitType = resolveClaudeCodeRateLimitType(event);
25960
+ const usedPercentage = resolveClaudeCodeUsedPercentage(event.rate_limit_info);
25961
+ if (usedPercentage === undefined) {
25962
+ return undefined;
25963
+ }
25964
+ const resetsAt = resolveClaudeCodeResetTimestamp(event.rate_limit_info);
25965
+ return {
25966
+ label: formatClaudeCodeRateLimitTypeLabel(rateLimitType),
25967
+ usedPercentage,
25968
+ ...(resetsAt !== undefined && { resetsAt }),
25969
+ };
25970
+ }
25971
+ /**
25972
+ * Formats known and newly introduced Claude Code rate-limit types for a stable terminal label.
25973
+ *
25974
+ * A newer limit type remains distinct in the dashboard instead of making several unrelated windows all appear as
25975
+ * a generic `Limit` row.
25976
+ *
25977
+ * @private helper of `parseClaudeCodeSubscriptionUsage`
25978
+ */
25979
+ function formatClaudeCodeRateLimitTypeLabel(rateLimitType) {
25980
+ const knownLabel = CLAUDE_CODE_RATE_LIMIT_LABELS[rateLimitType];
25981
+ if (knownLabel) {
25982
+ return knownLabel;
25983
+ }
25984
+ const words = rateLimitType.split(/[\s_-]+/u).filter(Boolean);
25985
+ if (words.length === 0) {
25986
+ return UNKNOWN_CLAUDE_CODE_RATE_LIMIT_LABEL;
25987
+ }
25988
+ return words.map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join(' ');
25989
+ }
25990
+ /**
25991
+ * Resolves the stable rate-limit type key emitted by Claude Code across camel- and snake-case payload versions.
25992
+ *
25993
+ * @private helper of `parseClaudeCodeSubscriptionUsage`
25994
+ */
25995
+ function resolveClaudeCodeRateLimitType(event) {
25996
+ var _a;
25997
+ const rateLimitInfo = event.rate_limit_info;
25998
+ const rateLimitType = (_a = rateLimitInfo === null || rateLimitInfo === void 0 ? void 0 : rateLimitInfo.rateLimitType) !== null && _a !== void 0 ? _a : rateLimitInfo === null || rateLimitInfo === void 0 ? void 0 : rateLimitInfo.rate_limit_type;
25999
+ return typeof rateLimitType === 'string' && rateLimitType.trim() !== ''
26000
+ ? rateLimitType.trim()
26001
+ : UNKNOWN_CLAUDE_CODE_RATE_LIMIT_LABEL;
26002
+ }
26003
+ /**
26004
+ * Converts Claude's fractional utilization value to a displayed percentage.
26005
+ *
26006
+ * A rejected limit event may omit utilization, but it still conveys a precise zero-remaining state and is therefore
26007
+ * rendered as 100 percent consumed.
26008
+ *
26009
+ * @private helper of `parseClaudeCodeSubscriptionUsage`
26010
+ */
26011
+ function resolveClaudeCodeUsedPercentage(rateLimitInfo) {
26012
+ const utilization = rateLimitInfo.utilization;
26013
+ if (typeof utilization === 'number' && Number.isFinite(utilization)) {
26014
+ const usedPercentage = utilization <= 1 ? utilization * 100 : utilization;
26015
+ if (usedPercentage >= 0 && usedPercentage <= 100) {
26016
+ return usedPercentage;
26017
+ }
26018
+ }
26019
+ return rateLimitInfo.status === 'rejected' ? 100 : undefined;
26020
+ }
26021
+ /**
26022
+ * Reads Claude's optional Unix reset timestamp from either current or earlier stream payload naming.
26023
+ *
26024
+ * @private helper of `parseClaudeCodeSubscriptionUsage`
26025
+ */
26026
+ function resolveClaudeCodeResetTimestamp(rateLimitInfo) {
26027
+ var _a;
26028
+ const resetsAt = (_a = rateLimitInfo.resetsAt) !== null && _a !== void 0 ? _a : rateLimitInfo.resets_at;
26029
+ return typeof resetsAt === 'number' && Number.isFinite(resetsAt) && resetsAt > 0 ? resetsAt : undefined;
26030
+ }
26031
+
25893
26032
  /**
25894
26033
  * Polling interval used while waiting for Claude Code session limits to reset.
25895
26034
  */
@@ -25905,6 +26044,14 @@
25905
26044
  this.options = options;
25906
26045
  this.name = 'claude-code';
25907
26046
  }
26047
+ /**
26048
+ * Returns the latest subscription-limit snapshot emitted by this Claude Code session.
26049
+ *
26050
+ * Claude exposes these values in the normal stream after a response, so no separate quota-only model call is made.
26051
+ */
26052
+ async getSubscriptionUsage() {
26053
+ return this.subscriptionUsage;
26054
+ }
25908
26055
  /**
25909
26056
  * Runs the prompt using Claude Code and parses usage output.
25910
26057
  */
@@ -25918,6 +26065,7 @@
25918
26065
  prompt,
25919
26066
  resumeSessionId,
25920
26067
  }).catch(async (error) => {
26068
+ this.updateSubscriptionUsage(error instanceof Error ? error.message : String(error));
25921
26069
  const sessionLimit = extractClaudeCodeSessionLimitFromError(error);
25922
26070
  if (!sessionLimit) {
25923
26071
  throw error;
@@ -25932,6 +26080,7 @@
25932
26080
  continue;
25933
26081
  }
25934
26082
  const sessionLimit = extractClaudeCodeSessionLimitFromOutput(output);
26083
+ this.updateSubscriptionUsage(output);
25935
26084
  if (sessionLimit) {
25936
26085
  resurrectionCount++;
25937
26086
  await waitForClaudeCodeSessionLimitReset(sessionLimit, resurrectionCount, options);
@@ -25961,6 +26110,18 @@
25961
26110
  preserveArtifactsOnSuccess: options.preserveArtifactsOnSuccess,
25962
26111
  });
25963
26112
  }
26113
+ /**
26114
+ * Keeps the newest usable Claude subscription-limit values reported by the stream.
26115
+ *
26116
+ * A stream can omit these values for API-key users or unsupported plan types; retaining the prior snapshot avoids
26117
+ * a temporary omission erasing a still-valid dashboard value while a long queue is running.
26118
+ */
26119
+ updateSubscriptionUsage(output) {
26120
+ const subscriptionUsage = parseClaudeCodeSubscriptionUsage(output);
26121
+ if (subscriptionUsage) {
26122
+ this.subscriptionUsage = mergeHarnessSubscriptionUsage(this.subscriptionUsage, subscriptionUsage);
26123
+ }
26124
+ }
25964
26125
  }
25965
26126
  /**
25966
26127
  * Waits until the Claude Code session can be resumed, keeping terminal status clear.
@@ -26868,6 +27029,292 @@
26868
27029
  return (_c = OPENAI_MODELS.find((model) => model.modelName === CODEX_FALLBACK_PRICING_MODEL)) === null || _c === void 0 ? void 0 : _c.pricing;
26869
27030
  }
26870
27031
 
27032
+ /**
27033
+ * Arguments which start the Codex app server over JSON-RPC stdio.
27034
+ */
27035
+ const CODEX_APP_SERVER_ARGUMENTS = ['app-server', '--stdio'];
27036
+ /**
27037
+ * Maximum time spent waiting for the optional Codex account-rate-limit snapshot.
27038
+ */
27039
+ const CODEX_SUBSCRIPTION_USAGE_REQUEST_TIMEOUT_MS = 10 * 1000;
27040
+ /**
27041
+ * JSON-RPC request id used for the required app-server initialization handshake.
27042
+ */
27043
+ const INITIALIZE_REQUEST_ID = 1;
27044
+ /**
27045
+ * JSON-RPC request id used for the Codex account rate-limit snapshot.
27046
+ */
27047
+ const RATE_LIMITS_REQUEST_ID = 2;
27048
+ /**
27049
+ * Client name sent to the Codex app server while Promptbook reads subscription limits.
27050
+ */
27051
+ const PROMPTBOOK_CODEX_APP_SERVER_CLIENT_NAME = 'ptbk-coder';
27052
+ /**
27053
+ * Client version sent to the Codex app server while Promptbook reads subscription limits.
27054
+ *
27055
+ * The app-server protocol only requires a non-empty version string, and the CLI's own version is intentionally not
27056
+ * coupled to Promptbook's package version.
27057
+ */
27058
+ const PROMPTBOOK_CODEX_APP_SERVER_CLIENT_VERSION = '1';
27059
+ /**
27060
+ * Reads the current Codex subscription limit windows through its local app-server protocol.
27061
+ *
27062
+ * Codex versions without this optional protocol, API-key sessions, and transient account failures simply provide no
27063
+ * snapshot. Subscription usage is contextual UI information, so it must never make a coding prompt fail.
27064
+ *
27065
+ * @private internal utility of the OpenAI Codex runner
27066
+ */
27067
+ async function getCodexSubscriptionUsage(codexCommand) {
27068
+ try {
27069
+ return await requestCodexSubscriptionUsage(codexCommand);
27070
+ }
27071
+ catch (_a) {
27072
+ return undefined;
27073
+ }
27074
+ }
27075
+ /**
27076
+ * Performs the small Codex app-server JSON-RPC exchange which reads rate limits.
27077
+ *
27078
+ * The app server requires a completed `initialize` handshake before it accepts account methods. Closing standard input
27079
+ * after the response lets the short-lived helper exit without sharing the lifecycle of the coding process itself.
27080
+ *
27081
+ * @private helper of `getCodexSubscriptionUsage`
27082
+ */
27083
+ function requestCodexSubscriptionUsage(codexCommand) {
27084
+ return new Promise((resolve) => {
27085
+ const codexAppServerProcess = child_process.spawn(codexCommand, CODEX_APP_SERVER_ARGUMENTS, {
27086
+ shell: process.platform === 'win32',
27087
+ stdio: 'pipe',
27088
+ });
27089
+ const outputReader = readline.createInterface({ input: codexAppServerProcess.stdout });
27090
+ let requestTimeout;
27091
+ let isSettled = false;
27092
+ // Draining stderr prevents a protocol diagnostic from blocking the short-lived child process. The response is
27093
+ // deliberately not surfaced because missing usage must not distract from an otherwise healthy coding run.
27094
+ codexAppServerProcess.stderr.resume();
27095
+ codexAppServerProcess.stdin.on('error', () => undefined);
27096
+ /**
27097
+ * Stops the helper process and settles this optional request exactly once.
27098
+ */
27099
+ const settle = (subscriptionUsage) => {
27100
+ if (isSettled) {
27101
+ return;
27102
+ }
27103
+ isSettled = true;
27104
+ if (requestTimeout) {
27105
+ clearTimeout(requestTimeout);
27106
+ }
27107
+ outputReader.close();
27108
+ codexAppServerProcess.stdin.end();
27109
+ if (codexAppServerProcess.exitCode === null && !codexAppServerProcess.killed) {
27110
+ codexAppServerProcess.kill();
27111
+ }
27112
+ resolve(subscriptionUsage);
27113
+ };
27114
+ /**
27115
+ * Sends one JSON-RPC message to the Codex app server.
27116
+ */
27117
+ const sendJsonRpcMessage = (message) => {
27118
+ try {
27119
+ codexAppServerProcess.stdin.write(`${JSON.stringify(message)}\n`);
27120
+ }
27121
+ catch (_a) {
27122
+ settle(undefined);
27123
+ }
27124
+ };
27125
+ requestTimeout = setTimeout(() => settle(undefined), CODEX_SUBSCRIPTION_USAGE_REQUEST_TIMEOUT_MS);
27126
+ codexAppServerProcess.on('error', () => settle(undefined));
27127
+ codexAppServerProcess.on('close', () => settle(undefined));
27128
+ outputReader.on('line', (line) => {
27129
+ const response = parseJsonRecord(line);
27130
+ if (!response) {
27131
+ return;
27132
+ }
27133
+ if (response.id === INITIALIZE_REQUEST_ID) {
27134
+ if (!isJsonRecord(response.result)) {
27135
+ settle(undefined);
27136
+ return;
27137
+ }
27138
+ sendJsonRpcMessage({ jsonrpc: '2.0', method: 'initialized' });
27139
+ sendJsonRpcMessage({
27140
+ jsonrpc: '2.0',
27141
+ id: RATE_LIMITS_REQUEST_ID,
27142
+ method: 'account/rateLimits/read',
27143
+ });
27144
+ return;
27145
+ }
27146
+ if (response.id === RATE_LIMITS_REQUEST_ID) {
27147
+ settle(buildCodexSubscriptionUsage(response.result));
27148
+ }
27149
+ });
27150
+ sendJsonRpcMessage({
27151
+ jsonrpc: '2.0',
27152
+ id: INITIALIZE_REQUEST_ID,
27153
+ method: 'initialize',
27154
+ params: {
27155
+ clientInfo: {
27156
+ name: PROMPTBOOK_CODEX_APP_SERVER_CLIENT_NAME,
27157
+ version: PROMPTBOOK_CODEX_APP_SERVER_CLIENT_VERSION,
27158
+ },
27159
+ capabilities: {
27160
+ experimentalApi: true,
27161
+ },
27162
+ },
27163
+ });
27164
+ });
27165
+ }
27166
+ /**
27167
+ * Converts the raw Codex app-server rate-limit response into Promptbook's harness-neutral subscription snapshot.
27168
+ *
27169
+ * Modern Codex versions can report several metered buckets, while older versions expose a single compatibility
27170
+ * bucket. In both cases every primary and secondary rolling window is preserved for the terminal dashboard.
27171
+ *
27172
+ * @private internal utility of the OpenAI Codex runner
27173
+ */
27174
+ function buildCodexSubscriptionUsage(response) {
27175
+ if (!isJsonRecord(response)) {
27176
+ return undefined;
27177
+ }
27178
+ const snapshots = resolveCodexRateLimitSnapshots(response);
27179
+ const hasMultipleSnapshots = snapshots.length > 1;
27180
+ const limits = snapshots.flatMap(({ identifier, value }) => buildCodexSubscriptionUsageLimits({
27181
+ snapshot: value,
27182
+ identifier,
27183
+ hasMultipleSnapshots,
27184
+ }));
27185
+ return limits.length === 0 ? undefined : { limits };
27186
+ }
27187
+ /**
27188
+ * Resolves either Codex's multi-bucket rate-limit response or its compatible single-bucket fallback.
27189
+ *
27190
+ * @private helper of `buildCodexSubscriptionUsage`
27191
+ */
27192
+ function resolveCodexRateLimitSnapshots(response) {
27193
+ const rateLimitsByLimitId = response.rateLimitsByLimitId;
27194
+ if (isJsonRecord(rateLimitsByLimitId)) {
27195
+ const snapshots = Object.entries(rateLimitsByLimitId)
27196
+ .filter(([, value]) => isJsonRecord(value))
27197
+ .map(([identifier, value]) => ({ identifier, value: value }));
27198
+ if (snapshots.length > 0) {
27199
+ return snapshots;
27200
+ }
27201
+ }
27202
+ return isJsonRecord(response.rateLimits) ? [{ value: response.rateLimits }] : [];
27203
+ }
27204
+ /**
27205
+ * Creates every displayed rolling window from one Codex rate-limit bucket.
27206
+ *
27207
+ * @private helper of `buildCodexSubscriptionUsage`
27208
+ */
27209
+ function buildCodexSubscriptionUsageLimits(options) {
27210
+ var _a;
27211
+ const { snapshot, identifier, hasMultipleSnapshots } = options;
27212
+ const bucketLabel = hasMultipleSnapshots ? (_a = readString(snapshot.limitName)) !== null && _a !== void 0 ? _a : identifier : undefined;
27213
+ return [
27214
+ buildCodexSubscriptionUsageLimit(snapshot.primary, bucketLabel, 'Primary'),
27215
+ buildCodexSubscriptionUsageLimit(snapshot.secondary, bucketLabel, 'Secondary'),
27216
+ ].filter((limit) => limit !== undefined);
27217
+ }
27218
+ /**
27219
+ * Converts one raw Codex rate-limit window to a displayable subscription usage limit.
27220
+ *
27221
+ * @private helper of `buildCodexSubscriptionUsage`
27222
+ */
27223
+ function buildCodexSubscriptionUsageLimit(rawWindow, bucketLabel, fallbackWindowLabel) {
27224
+ var _a;
27225
+ if (!isJsonRecord(rawWindow)) {
27226
+ return undefined;
27227
+ }
27228
+ const usedPercentage = readPercentage(rawWindow.usedPercent);
27229
+ if (usedPercentage === undefined) {
27230
+ return undefined;
27231
+ }
27232
+ const windowLabel = (_a = formatCodexRateLimitWindowDuration(rawWindow.windowDurationMins)) !== null && _a !== void 0 ? _a : fallbackWindowLabel;
27233
+ const label = bucketLabel ? `${bucketLabel} ${windowLabel}` : windowLabel;
27234
+ const resetsAt = readUnixTimestamp(rawWindow.resetsAt);
27235
+ return {
27236
+ label,
27237
+ usedPercentage,
27238
+ ...(resetsAt !== undefined && { resetsAt }),
27239
+ };
27240
+ }
27241
+ /**
27242
+ * Formats the duration of one Codex rolling rate-limit window for compact terminal display.
27243
+ *
27244
+ * @private helper of `buildCodexSubscriptionUsage`
27245
+ */
27246
+ function formatCodexRateLimitWindowDuration(value) {
27247
+ const durationMinutes = readNonNegativeFiniteNumber(value);
27248
+ if (durationMinutes === undefined || durationMinutes === 0) {
27249
+ return undefined;
27250
+ }
27251
+ const MINUTES_PER_HOUR = 60;
27252
+ const MINUTES_PER_DAY = 24 * MINUTES_PER_HOUR;
27253
+ if (durationMinutes % MINUTES_PER_DAY === 0) {
27254
+ return `${durationMinutes / MINUTES_PER_DAY}d`;
27255
+ }
27256
+ if (durationMinutes % MINUTES_PER_HOUR === 0) {
27257
+ return `${durationMinutes / MINUTES_PER_HOUR}h`;
27258
+ }
27259
+ return `${durationMinutes}m`;
27260
+ }
27261
+ /**
27262
+ * Parses one JSON-RPC line when it is an object.
27263
+ *
27264
+ * @private helper of `getCodexSubscriptionUsage`
27265
+ */
27266
+ function parseJsonRecord(line) {
27267
+ try {
27268
+ const value = JSON.parse(line);
27269
+ return isJsonRecord(value) ? value : undefined;
27270
+ }
27271
+ catch (_a) {
27272
+ return undefined;
27273
+ }
27274
+ }
27275
+ /**
27276
+ * Checks whether a value is a JSON object.
27277
+ *
27278
+ * @private helper of `getCodexSubscriptionUsage`
27279
+ */
27280
+ function isJsonRecord(value) {
27281
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
27282
+ }
27283
+ /**
27284
+ * Reads one non-empty string from an untrusted JSON object.
27285
+ *
27286
+ * @private helper of `buildCodexSubscriptionUsage`
27287
+ */
27288
+ function readString(value) {
27289
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
27290
+ }
27291
+ /**
27292
+ * Reads a finite number which cannot be negative.
27293
+ *
27294
+ * @private helper of `buildCodexSubscriptionUsage`
27295
+ */
27296
+ function readNonNegativeFiniteNumber(value) {
27297
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
27298
+ }
27299
+ /**
27300
+ * Reads a valid percentage from the Codex app-server response.
27301
+ *
27302
+ * @private helper of `buildCodexSubscriptionUsage`
27303
+ */
27304
+ function readPercentage(value) {
27305
+ const percentage = readNonNegativeFiniteNumber(value);
27306
+ return percentage !== undefined && percentage <= 100 ? percentage : undefined;
27307
+ }
27308
+ /**
27309
+ * Reads a Unix timestamp in seconds from the Codex app-server response.
27310
+ *
27311
+ * @private helper of `buildCodexSubscriptionUsage`
27312
+ */
27313
+ function readUnixTimestamp(value) {
27314
+ const timestamp = readNonNegativeFiniteNumber(value);
27315
+ return timestamp !== undefined && timestamp > 0 ? timestamp : undefined;
27316
+ }
27317
+
26871
27318
  /**
26872
27319
  * Detects which login method Codex used from the captured CLI output.
26873
27320
  *
@@ -27055,6 +27502,15 @@
27055
27502
  jitterRatio: RATE_LIMIT_BACKOFF_JITTER_RATIO,
27056
27503
  });
27057
27504
  }
27505
+ /**
27506
+ * Reads the current ChatGPT subscription quota snapshot through Codex's local app server.
27507
+ *
27508
+ * API-key sessions and unsupported Codex versions simply return no snapshot, which keeps this optional dashboard
27509
+ * information from affecting the actual prompt execution.
27510
+ */
27511
+ async getSubscriptionUsage() {
27512
+ return await getCodexSubscriptionUsage(this.options.codexCommand);
27513
+ }
27058
27514
  /**
27059
27515
  * Runs the Codex prompt in a temporary script and waits for completion output.
27060
27516
  */
@@ -27634,6 +28090,90 @@
27634
28090
  }
27635
28091
  // Note: [🟡] Code for priority filtering [priorityFilter](scripts/run-codex-prompts/prompts/priorityFilter.ts) should never be published outside of `@promptbook/cli`
27636
28092
 
28093
+ /**
28094
+ * Label shown on the first subscription-usage row in the terminal Session box.
28095
+ */
28096
+ const SUBSCRIPTION_USAGE_SESSION_LABEL = 'Usage';
28097
+ /**
28098
+ * Number of milliseconds in one calendar-free day used for compact reset countdowns.
28099
+ */
28100
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
28101
+ /**
28102
+ * Builds every available subscription-limit row for the terminal Session box.
28103
+ *
28104
+ * The first row carries the `Usage` label and subsequent rows stay aligned beneath it, allowing a harness to report
28105
+ * any number of windows instead of making the dashboard assume a fixed 5-hour/weekly pair.
28106
+ *
28107
+ * @private internal utility of coder run UI
28108
+ */
28109
+ function buildSubscriptionUsageSessionRows(subscriptionUsage, currentTimeMs = Date.now()) {
28110
+ if (!subscriptionUsage || subscriptionUsage.limits.length === 0) {
28111
+ return [];
28112
+ }
28113
+ return subscriptionUsage.limits.map((limit, index) => ({
28114
+ label: index === 0 ? SUBSCRIPTION_USAGE_SESSION_LABEL : '',
28115
+ value: formatSubscriptionUsageLimit(limit, currentTimeMs),
28116
+ }));
28117
+ }
28118
+ /**
28119
+ * Formats one remaining subscription limit and its optional reset time for the terminal dashboard.
28120
+ *
28121
+ * @private helper of `buildSubscriptionUsageSessionRows`
28122
+ */
28123
+ function formatSubscriptionUsageLimit(limit, currentTimeMs) {
28124
+ const remainingPercentage = Math.round(Math.max(0, Math.min(100, 100 - limit.usedPercentage)));
28125
+ const remainingPercentageColor = resolveRemainingPercentageColor(remainingPercentage);
28126
+ const resetText = formatSubscriptionUsageReset(limit.resetsAt, currentTimeMs);
28127
+ const label = limit.label.trim() || SUBSCRIPTION_USAGE_SESSION_LABEL;
28128
+ return [
28129
+ colors__default["default"].bold(label),
28130
+ remainingPercentageColor(`${remainingPercentage}% remaining`),
28131
+ ...(resetText ? [colors__default["default"].gray(resetText)] : []),
28132
+ ].join(' · ');
28133
+ }
28134
+ /**
28135
+ * Chooses an attention color for the remaining subscription percentage.
28136
+ *
28137
+ * @private helper of `buildSubscriptionUsageSessionRows`
28138
+ */
28139
+ function resolveRemainingPercentageColor(remainingPercentage) {
28140
+ if (remainingPercentage === 0) {
28141
+ return colors__default["default"].red;
28142
+ }
28143
+ if (remainingPercentage <= 20) {
28144
+ return colors__default["default"].yellow;
28145
+ }
28146
+ return colors__default["default"].green;
28147
+ }
28148
+ /**
28149
+ * Formats the optional future reset timestamp of one subscription limit.
28150
+ *
28151
+ * @private helper of `buildSubscriptionUsageSessionRows`
28152
+ */
28153
+ function formatSubscriptionUsageReset(resetsAt, currentTimeMs) {
28154
+ if (resetsAt === undefined) {
28155
+ return undefined;
28156
+ }
28157
+ const remainingDurationMs = resetsAt * 1000 - currentTimeMs;
28158
+ if (remainingDurationMs <= 0) {
28159
+ return 'resets now';
28160
+ }
28161
+ return `resets in ${formatSubscriptionUsageDuration(remainingDurationMs)}`;
28162
+ }
28163
+ /**
28164
+ * Formats a reset countdown with days when a weekly-style limit is still several days away.
28165
+ *
28166
+ * @private helper of `buildSubscriptionUsageSessionRows`
28167
+ */
28168
+ function formatSubscriptionUsageDuration(durationMs) {
28169
+ const dayCount = Math.floor(durationMs / ONE_DAY_MS);
28170
+ if (dayCount === 0) {
28171
+ return formatDurationMs(durationMs);
28172
+ }
28173
+ const remainingDurationMs = durationMs - dayCount * ONE_DAY_MS;
28174
+ return remainingDurationMs === 0 ? `${dayCount}d` : `${dayCount}d ${formatDurationMs(remainingDurationMs)}`;
28175
+ }
28176
+
27637
28177
  /**
27638
28178
  * Minimum width used for the rich coder-run frame.
27639
28179
  */
@@ -27739,6 +28279,7 @@
27739
28279
  value: runnerParts.join(' · '),
27740
28280
  },
27741
28281
  ...configurationRows,
28282
+ ...buildSubscriptionUsageSessionRows(options.subscriptionUsage),
27742
28283
  ...buildScriptPathSessionRows(options.currentScriptPaths || [], bodyWidth),
27743
28284
  {
27744
28285
  label: 'This run',
@@ -28284,6 +28825,13 @@
28284
28825
  this.agentStatusTableRows = [...agentStatusTableRows];
28285
28826
  this.emitChange();
28286
28827
  }
28828
+ /**
28829
+ * Replaces the optional harness subscription usage shown in the Session box.
28830
+ */
28831
+ setSubscriptionUsage(subscriptionUsage) {
28832
+ this.subscriptionUsage = subscriptionUsage;
28833
+ this.emitChange();
28834
+ }
28287
28835
  /**
28288
28836
  * Sets or clears the Enter-key action label shown in the controls panel.
28289
28837
  */
@@ -29275,6 +29823,7 @@
29275
29823
  pauseTargetLabel: getPauseTargetLabel(),
29276
29824
  isEndAfterCurrentPromptRequested: getEndAfterCurrentPromptState(),
29277
29825
  config: state.config,
29826
+ subscriptionUsage: state.subscriptionUsage,
29278
29827
  agentVisual: state.agentVisual,
29279
29828
  phase: state.phase,
29280
29829
  currentPromptLabel: state.currentPromptLabel,
@@ -72051,48 +72600,6 @@
72051
72600
  }
72052
72601
  // TODO: [🧠][👵] Figure out something between rotateItems and shuffleItems which is more generic and receives a ruleset how to reordeto the array in some general way
72053
72602
 
72054
- /**
72055
- * All possible emoji chars like "🍆", "🍡", "🍤"...
72056
- * Note: this will be needed to update annually - now updated at 2022-01-19
72057
- *
72058
- * @see https://getemoji.com/
72059
- */
72060
- const EMOJIS_IN_CATEGORIES = {
72061
- Smileys: '😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 🥺 😢 😭 😤 😠 😡 🤬 🤯 😳 🥵 🥶 😱 😨 😰 😥 😓 🤗 🤔 🤭 🤫 🤥 😶 😐 😑 😬 🙄 😯 😦 😧 😮 😲 🥱 😴 🤤 😪 😵 🤐 🥴 🤢 🤮 🤧 😷 🤒 🤕 🤑 🤠 😈 👿 👹 👺 🤡 💩 👻 💀 ☠️ 👽 👾 🤖 🎃 😺 😸 😹 😻 😼 😽 🙀 😿 😾'.split(' '),
72062
- 'Gestures and Body Parts': '👋 🤚 🖐 ✋ 🖖 👌 🤌 🤏 ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍 👎 ✊ 👊 🤛 🤜 👏 🙌 👐 🤲 🤝 🙏 ✍️ 💅 🤳 💪 🦾 🦵 🦿 🦶 👣 👂 🦻 👃 🫀 🫁 🧠 🦷 🦴 👀 👁 👅 👄 💋 🩸'.split(' '),
72063
- 'People and Fantasy': '👶 👧 🧒 👦 👩 🧑 👨 👩‍🦱 🧑‍🦱 👨‍🦱 👩‍🦰 🧑‍🦰 👨‍🦰 👱‍♀️ 👱 👱‍♂️ 👩‍🦳 🧑‍🦳 👨‍🦳 👩‍🦲 🧑‍🦲 👨‍🦲 🧔 👵 🧓 👴 👲 👳‍♀️ 👳 👳‍♂️ 🧕 👮‍♀️ 👮 👮‍♂️ 👷‍♀️ 👷 👷‍♂️ 💂‍♀️ 💂 💂‍♂️ 🕵️‍♀️ 🕵️ 🕵️‍♂️ 👩‍⚕️ 🧑‍⚕️ 👨‍⚕️ 👩‍🌾 🧑‍🌾 👨‍🌾 👩‍🍳 🧑‍🍳 👨‍🍳 👩‍🎓 🧑‍🎓 👨‍🎓 👩‍🎤 🧑‍🎤 👨‍🎤 👩‍🏫 🧑‍🏫 👨‍🏫 👩‍🏭 🧑‍🏭 👨‍🏭 👩‍💻 🧑‍💻 👨‍💻 👩‍💼 🧑‍💼 👨‍💼 👩‍🔧 🧑‍🔧 👨‍🔧 👩‍🔬 🧑‍🔬 👨‍🔬 👩‍🎨 🧑‍🎨 👨‍🎨 👩‍🚒 🧑‍🚒 👨‍🚒 👩‍✈️ 🧑‍✈️ 👨‍✈️ 👩‍🚀 🧑‍🚀 👨‍🚀 👩‍⚖️ 🧑‍⚖️ 👨‍⚖️ 👰‍♀️ 👰 👰‍♂️ 🤵‍♀️ 🤵 🤵‍♂️ 👸 🤴 🥷 🦸‍♀️ 🦸 🦸‍♂️ 🦹‍♀️ 🦹 🦹‍♂️ 🤶 🧑‍🎄 🎅 🧙‍♀️ 🧙 🧙‍♂️ 🧝‍♀️ 🧝 🧝‍♂️ 🧛‍♀️ 🧛 🧛‍♂️ 🧟‍♀️ 🧟 🧟‍♂️ 🧞‍♀️ 🧞 🧞‍♂️ 🧜‍♀️ 🧜 🧜‍♂️ 🧚‍♀️ 🧚 🧚‍♂️ 👼 🤰 🤱 👩‍🍼 🧑‍🍼 👨‍🍼 🙇‍♀️ 🙇 🙇‍♂️ 💁‍♀️ 💁 💁‍♂️ 🙅‍♀️ 🙅 🙅‍♂️ 🙆‍♀️ 🙆 🙆‍♂️ 🙋‍♀️ 🙋 🙋‍♂️ 🧏‍♀️ 🧏 🧏‍♂️ 🤦‍♀️ 🤦 🤦‍♂️ 🤷‍♀️ 🤷 🤷‍♂️ 🙎‍♀️ 🙎 🙎‍♂️ 🙍‍♀️ 🙍 🙍‍♂️ 💇‍♀️ 💇 💇‍♂️ 💆‍♀️ 💆 💆‍♂️ 🧖‍♀️ 🧖 🧖‍♂️ 💅 🤳 💃 🕺 👯‍♀️ 👯 👯‍♂️ 🕴 👩‍🦽 🧑‍🦽 👨‍🦽 👩‍🦼 🧑‍🦼 👨‍🦼 🚶‍♀️ 🚶 🚶‍♂️ 👩‍🦯 🧑‍🦯 👨‍🦯 🧎‍♀️ 🧎 🧎‍♂️ 🏃‍♀️ 🏃 🏃‍♂️ 🧍‍♀️ 🧍 🧍‍♂️ 👭 🧑‍🤝‍🧑 👬 👫 👩‍❤️‍👩 💑 👨‍❤️‍👨 👩‍❤️‍👨 👩‍❤️‍💋‍👩 💏 👨‍❤️‍💋‍👨 👩‍❤️‍💋‍👨 👪 👨‍👩‍👦 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 👨‍👩‍👧‍👧 👨‍👨‍👦 👨‍👨‍👧 👨‍👨‍👧‍👦 👨‍👨‍👦‍👦 👨‍👨‍👧‍👧 👩‍👩‍👦 👩‍👩‍👧 👩‍👩‍👧‍👦 👩‍👩‍👦‍👦 👩‍👩‍👧‍👧 👨‍👦 👨‍👦‍👦 👨‍👧 👨‍👧‍👦 👨‍👧‍👧 👩‍👦 👩‍👦‍👦 👩‍👧 👩‍👧‍👦 👩‍👧‍👧 🗣 👤 👥 🫂'.split(' '),
72064
- 'Clothing and Accessories': '🧳 🌂 ☂️ 🧵 🪡 🪢 🧶 👓 🕶 🥽 🥼 🦺 👔 👕 👖 🧣 🧤 🧥 🧦 👗 👘 🥻 🩴 🩱 🩲 🩳 👙 👚 👛 👜 👝 🎒 👞 👟 🥾 🥿 👠 👡 🩰 👢 👑 👒 🎩 🎓 🧢 ⛑ 🪖 💄 💍 💼'.split(' '),
72065
- 'Pale Emojis': '👋🏻 🤚🏻 🖐🏻 ✋🏻 🖖🏻 👌🏻 🤌🏻 🤏🏻 ✌🏻 🤞🏻 🤟🏻 🤘🏻 🤙🏻 👈🏻 👉🏻 👆🏻 🖕🏻 👇🏻 ☝🏻 👍🏻 👎🏻 ✊🏻 👊🏻 🤛🏻 🤜🏻 👏🏻 🙌🏻 👐🏻 🤲🏻 🙏🏻 ✍🏻 💅🏻 🤳🏻 💪🏻 🦵🏻 🦶🏻 👂🏻 🦻🏻 👃🏻 👶🏻 👧🏻 🧒🏻 👦🏻 👩🏻 🧑🏻 👨🏻 👩🏻‍🦱 🧑🏻‍🦱 👨🏻‍🦱 👩🏻‍🦰 🧑🏻‍🦰 👨🏻‍🦰 👱🏻‍♀️ 👱🏻 👱🏻‍♂️ 👩🏻‍🦳 🧑🏻‍🦳 👨🏻‍🦳 👩🏻‍🦲 🧑🏻‍🦲 👨🏻‍🦲 🧔🏻 👵🏻 🧓🏻 👴🏻 👲🏻 👳🏻‍♀️ 👳🏻 👳🏻‍♂️ 🧕🏻 👮🏻‍♀️ 👮🏻 👮🏻‍♂️ 👷🏻‍♀️ 👷🏻 👷🏻‍♂️ 💂🏻‍♀️ 💂🏻 💂🏻‍♂️ 🕵🏻‍♀️ 🕵🏻 🕵🏻‍♂️ 👩🏻‍⚕️ 🧑🏻‍⚕️ 👨🏻‍⚕️ 👩🏻‍🌾 🧑🏻‍🌾 👨🏻‍🌾 👩🏻‍🍳 🧑🏻‍🍳 👨🏻‍🍳 👩🏻‍🎓 🧑🏻‍🎓 👨🏻‍🎓 👩🏻‍🎤 🧑🏻‍🎤 👨🏻‍🎤 👩🏻‍🏫 🧑🏻‍🏫 👨🏻‍🏫 👩🏻‍🏭 🧑🏻‍🏭 👨🏻‍🏭 👩🏻‍💻 🧑🏻‍💻 👨🏻‍💻 👩🏻‍💼 🧑🏻‍💼 👨🏻‍💼 👩🏻‍🔧 🧑🏻‍🔧 👨🏻‍🔧 👩🏻‍🔬 🧑🏻‍🔬 👨🏻‍🔬 👩🏻‍🎨 🧑🏻‍🎨 👨🏻‍🎨 👩🏻‍🚒 🧑🏻‍🚒 👨🏻‍🚒 👩🏻‍✈️ 🧑🏻‍✈️ 👨🏻‍✈️ 👩🏻‍🚀 🧑🏻‍🚀 👨🏻‍🚀 👩🏻‍⚖️ 🧑🏻‍⚖️ 👨🏻‍⚖️ 👰🏻‍♀️ 👰🏻 👰🏻‍♂️ 🤵🏻‍♀️ 🤵🏻 🤵🏻‍♂️ 👸🏻 🤴🏻 🥷🏻 🦸🏻‍♀️ 🦸🏻 🦸🏻‍♂️ 🦹🏻‍♀️ 🦹🏻 🦹🏻‍♂️ 🤶🏻 🧑🏻‍🎄 🎅🏻 🧙🏻‍♀️ 🧙🏻 🧙🏻‍♂️ 🧝🏻‍♀️ 🧝🏻 🧝🏻‍♂️ 🧛🏻‍♀️ 🧛🏻 🧛🏻‍♂️ 🧜🏻‍♀️ 🧜🏻 🧜🏻‍♂️ 🧚🏻‍♀️ 🧚🏻 🧚🏻‍♂️ 👼🏻 🤰🏻 🤱🏻 👩🏻‍🍼 🧑🏻‍🍼 👨🏻‍🍼 🙇🏻‍♀️ 🙇🏻 🙇🏻‍♂️ 💁🏻‍♀️ 💁🏻 💁🏻‍♂️ 🙅🏻‍♀️ 🙅🏻 🙅🏻‍♂️ 🙆🏻‍♀️ 🙆🏻 🙆🏻‍♂️ 🙋🏻‍♀️ 🙋🏻 🙋🏻‍♂️ 🧏🏻‍♀️ 🧏🏻 🧏🏻‍♂️ 🤦🏻‍♀️ 🤦🏻 🤦🏻‍♂️ 🤷🏻‍♀️ 🤷🏻 🤷🏻‍♂️ 🙎🏻‍♀️ 🙎🏻 🙎🏻‍♂️ 🙍🏻‍♀️ 🙍🏻 🙍🏻‍♂️ 💇🏻‍♀️ 💇🏻 💇🏻‍♂️ 💆🏻‍♀️ 💆🏻 💆🏻‍♂️ 🧖🏻‍♀️ 🧖🏻 🧖🏻‍♂️ 💃🏻 🕺🏻 🕴🏻 👩🏻‍🦽 🧑🏻‍🦽 👨🏻‍🦽 👩🏻‍🦼 🧑🏻‍🦼 👨🏻‍🦼 🚶🏻‍♀️ 🚶🏻 🚶🏻‍♂️ 👩🏻‍🦯 🧑🏻‍🦯 👨🏻‍🦯 🧎🏻‍♀️ 🧎🏻 🧎🏻‍♂️ 🏃🏻‍♀️ 🏃🏻 🏃🏻‍♂️ 🧍🏻‍♀️ 🧍🏻 🧍🏻‍♂️ 👭🏻 🧑🏻‍🤝‍🧑🏻 👬🏻 👫🏻 🧗🏻‍♀️ 🧗🏻 🧗🏻‍♂️ 🏇🏻 🏂🏻 🏌🏻‍♀️ 🏌🏻 🏌🏻‍♂️ 🏄🏻‍♀️ 🏄🏻 🏄🏻‍♂️ 🚣🏻‍♀️ 🚣🏻 🚣🏻‍♂️ 🏊🏻‍♀️ 🏊🏻 🏊🏻‍♂️ ⛹🏻‍♀️ ⛹🏻 ⛹🏻‍♂️ 🏋🏻‍♀️ 🏋🏻 🏋🏻‍♂️ 🚴🏻‍♀️ 🚴🏻 🚴🏻‍♂️ 🚵🏻‍♀️ 🚵🏻 🚵🏻‍♂️ 🤸🏻‍♀️ 🤸🏻 🤸🏻‍♂️ 🤽🏻‍♀️ 🤽🏻 🤽🏻‍♂️ 🤾🏻‍♀️ 🤾🏻 🤾🏻‍♂️ 🤹🏻‍♀️ 🤹🏻 🤹🏻‍♂️ 🧘🏻‍♀️ 🧘🏻 🧘🏻‍♂️ 🛀🏻 🛌'.split(' '),
72066
- 'Cream White Emojis': '👋🏼 🤚🏼 🖐🏼 ✋🏼 🖖🏼 👌🏼 🤌🏼 🤏🏼 ✌🏼 🤞🏼 🤟🏼 🤘🏼 🤙🏼 👈🏼 👉🏼 👆🏼 🖕🏼 👇🏼 ☝🏼 👍🏼 👎🏼 ✊🏼 👊🏼 🤛🏼 🤜🏼 👏🏼 🙌🏼 👐🏼 🤲🏼 🙏🏼 ✍🏼 💅🏼 🤳🏼 💪🏼 🦵🏼 🦶🏼 👂🏼 🦻🏼 👃🏼 👶🏼 👧🏼 🧒🏼 👦🏼 👩🏼 🧑🏼 👨🏼 👩🏼‍🦱 🧑🏼‍🦱 👨🏼‍🦱 👩🏼‍🦰 🧑🏼‍🦰 👨🏼‍🦰 👱🏼‍♀️ 👱🏼 👱🏼‍♂️ 👩🏼‍🦳 🧑🏼‍🦳 👨🏼‍🦳 👩🏼‍🦲 🧑🏼‍🦲 👨🏼‍🦲 🧔🏼 👵🏼 🧓🏼 👴🏼 👲🏼 👳🏼‍♀️ 👳🏼 👳🏼‍♂️ 🧕🏼 👮🏼‍♀️ 👮🏼 👮🏼‍♂️ 👷🏼‍♀️ 👷🏼 👷🏼‍♂️ 💂🏼‍♀️ 💂🏼 💂🏼‍♂️ 🕵🏼‍♀️ 🕵🏼 🕵🏼‍♂️ 👩🏼‍⚕️ 🧑🏼‍⚕️ 👨🏼‍⚕️ 👩🏼‍🌾 🧑🏼‍🌾 👨🏼‍🌾 👩🏼‍🍳 🧑🏼‍🍳 👨🏼‍🍳 👩🏼‍🎓 🧑🏼‍🎓 👨🏼‍🎓 👩🏼‍🎤 🧑🏼‍🎤 👨🏼‍🎤 👩🏼‍🏫 🧑🏼‍🏫 👨🏼‍🏫 👩🏼‍🏭 🧑🏼‍🏭 👨🏼‍🏭 👩🏼‍💻 🧑🏼‍💻 👨🏼‍💻 👩🏼‍💼 🧑🏼‍💼 👨🏼‍💼 👩🏼‍🔧 🧑🏼‍🔧 👨🏼‍🔧 👩🏼‍🔬 🧑🏼‍🔬 👨🏼‍🔬 👩🏼‍🎨 🧑🏼‍🎨 👨🏼‍🎨 👩🏼‍🚒 🧑🏼‍🚒 👨🏼‍🚒 👩🏼‍✈️ 🧑🏼‍✈️ 👨🏼‍✈️ 👩🏼‍🚀 🧑🏼‍🚀 👨🏼‍🚀 👩🏼‍⚖️ 🧑🏼‍⚖️ 👨🏼‍⚖️ 👰🏼‍♀️ 👰🏼 👰🏼‍♂️ 🤵🏼‍♀️ 🤵🏼 🤵🏼‍♂️ 👸🏼 🤴🏼 🥷🏼 🦸🏼‍♀️ 🦸🏼 🦸🏼‍♂️ 🦹🏼‍♀️ 🦹🏼 🦹🏼‍♂️ 🤶🏼 🧑🏼‍🎄 🎅🏼 🧙🏼‍♀️ 🧙🏼 🧙🏼‍♂️ 🧝🏼‍♀️ 🧝🏼 🧝🏼‍♂️ 🧛🏼‍♀️ 🧛🏼 🧛🏼‍♂️ 🧜🏼‍♀️ 🧜🏼 🧜🏼‍♂️ 🧚🏼‍♀️ 🧚🏼 🧚🏼‍♂️ 👼🏼 🤰🏼 🤱🏼 👩🏼‍🍼 🧑🏼‍🍼 👨🏼‍🍼 🙇🏼‍♀️ 🙇🏼 🙇🏼‍♂️ 💁🏼‍♀️ 💁🏼 💁🏼‍♂️ 🙅🏼‍♀️ 🙅🏼 🙅🏼‍♂️ 🙆🏼‍♀️ 🙆🏼 🙆🏼‍♂️ 🙋🏼‍♀️ 🙋🏼 🙋🏼‍♂️ 🧏🏼‍♀️ 🧏🏼 🧏🏼‍♂️ 🤦🏼‍♀️ 🤦🏼 🤦🏼‍♂️ 🤷🏼‍♀️ 🤷🏼 🤷🏼‍♂️ 🙎🏼‍♀️ 🙎🏼 🙎🏼‍♂️ 🙍🏼‍♀️ 🙍🏼 🙍🏼‍♂️ 💇🏼‍♀️ 💇🏼 💇🏼‍♂️ 💆🏼‍♀️ 💆🏼 💆🏼‍♂️ 🧖🏼‍♀️ 🧖🏼 🧖🏼‍♂️ 💃🏼 🕺🏼 🕴🏼 👩🏼‍🦽 🧑🏼‍🦽 👨🏼‍🦽 👩🏼‍🦼 🧑🏼‍🦼 👨🏼‍🦼 🚶🏼‍♀️ 🚶🏼 🚶🏼‍♂️ 👩🏼‍🦯 🧑🏼‍🦯 👨🏼‍🦯 🧎🏼‍♀️ 🧎🏼 🧎🏼‍♂️ 🏃🏼‍♀️ 🏃🏼 🏃🏼‍♂️ 🧍🏼‍♀️ 🧍🏼 🧍🏼‍♂️ 👭🏼 🧑🏼‍🤝‍🧑🏼 👬🏼 👫🏼 🧗🏼‍♀️ 🧗🏼 🧗🏼‍♂️ 🏇🏼 🏂🏼 🏌🏼‍♀️ 🏌🏼 🏌🏼‍♂️ 🏄🏼‍♀️ 🏄🏼 🏄🏼‍♂️ 🚣🏼‍♀️ 🚣🏼 🚣🏼‍♂️ 🏊🏼‍♀️ 🏊🏼 🏊🏼‍♂️ ⛹🏼‍♀️ ⛹🏼 ⛹🏼‍♂️ 🏋🏼‍♀️ 🏋🏼 🏋🏼‍♂️ 🚴🏼‍♀️ 🚴🏼 🚴🏼‍♂️ 🚵🏼‍♀️ 🚵🏼 🚵🏼‍♂️ 🤸🏼‍♀️ 🤸🏼 🤸🏼‍♂️ 🤽🏼‍♀️ 🤽🏼 🤽🏼‍♂️ 🤾🏼‍♀️ 🤾🏼 🤾🏼‍♂️ 🤹🏼‍♀️ 🤹🏼 🤹🏼‍♂️ 🧘🏼‍♀️ 🧘🏼 🧘🏼‍♂️ 🛀🏼 🛌'.split(' '),
72067
- 'Brown Emojis': '👋🏽 🤚🏽 🖐🏽 ✋🏽 🖖🏽 👌🏽 🤌🏽 🤏🏽 ✌🏽 🤞🏽 🤟🏽 🤘🏽 🤙🏽 👈🏽 👉🏽 👆🏽 🖕🏽 👇🏽 ☝🏽 👍🏽 👎🏽 ✊🏽 👊🏽 🤛🏽 🤜🏽 👏🏽 🙌🏽 👐🏽 🤲🏽 🙏🏽 ✍🏽 💅🏽 🤳🏽 💪🏽 🦵🏽 🦶🏽 👂🏽 🦻🏽 👃🏽 👶🏽 👧🏽 🧒🏽 👦🏽 👩🏽 🧑🏽 👨🏽 👩🏽‍🦱 🧑🏽‍🦱 👨🏽‍🦱 👩🏽‍🦰 🧑🏽‍🦰 👨🏽‍🦰 👱🏽‍♀️ 👱🏽 👱🏽‍♂️ 👩🏽‍🦳 🧑🏽‍🦳 👨🏽‍🦳 👩🏽‍🦲 🧑🏽‍🦲 👨🏽‍🦲 🧔🏽 👵🏽 🧓🏽 👴🏽 👲🏽 👳🏽‍♀️ 👳🏽 👳🏽‍♂️ 🧕🏽 👮🏽‍♀️ 👮🏽 👮🏽‍♂️ 👷🏽‍♀️ 👷🏽 👷🏽‍♂️ 💂🏽‍♀️ 💂🏽 💂🏽‍♂️ 🕵🏽‍♀️ 🕵🏽 🕵🏽‍♂️ 👩🏽‍⚕️ 🧑🏽‍⚕️ 👨🏽‍⚕️ 👩🏽‍🌾 🧑🏽‍🌾 👨🏽‍🌾 👩🏽‍🍳 🧑🏽‍🍳 👨🏽‍🍳 👩🏽‍🎓 🧑🏽‍🎓 👨🏽‍🎓 👩🏽‍🎤 🧑🏽‍🎤 👨🏽‍🎤 👩🏽‍🏫 🧑🏽‍🏫 👨🏽‍🏫 👩🏽‍🏭 🧑🏽‍🏭 👨🏽‍🏭 👩🏽‍💻 🧑🏽‍💻 👨🏽‍💻 👩🏽‍💼 🧑🏽‍💼 👨🏽‍💼 👩🏽‍🔧 🧑🏽‍🔧 👨🏽‍🔧 👩🏽‍🔬 🧑🏽‍🔬 👨🏽‍🔬 👩🏽‍🎨 🧑🏽‍🎨 👨🏽‍🎨 👩🏽‍🚒 🧑🏽‍🚒 👨🏽‍🚒 👩🏽‍✈️ 🧑🏽‍✈️ 👨🏽‍✈️ 👩🏽‍🚀 🧑🏽‍🚀 👨🏽‍🚀 👩🏽‍⚖️ 🧑🏽‍⚖️ 👨🏽‍⚖️ 👰🏽‍♀️ 👰🏽 👰🏽‍♂️ 🤵🏽‍♀️ 🤵🏽 🤵🏽‍♂️ 👸🏽 🤴🏽 🥷🏽 🦸🏽‍♀️ 🦸🏽 🦸🏽‍♂️ 🦹🏽‍♀️ 🦹🏽 🦹🏽‍♂️ 🤶🏽 🧑🏽‍🎄 🎅🏽 🧙🏽‍♀️ 🧙🏽 🧙🏽‍♂️ 🧝🏽‍♀️ 🧝🏽 🧝🏽‍♂️ 🧛🏽‍♀️ 🧛🏽 🧛🏽‍♂️ 🧜🏽‍♀️ 🧜🏽 🧜🏽‍♂️ 🧚🏽‍♀️ 🧚🏽 🧚🏽‍♂️ 👼🏽 🤰🏽 🤱🏽 👩🏽‍🍼 🧑🏽‍🍼 👨🏽‍🍼 🙇🏽‍♀️ 🙇🏽 🙇🏽‍♂️ 💁🏽‍♀️ 💁🏽 💁🏽‍♂️ 🙅🏽‍♀️ 🙅🏽 🙅🏽‍♂️ 🙆🏽‍♀️ 🙆🏽 🙆🏽‍♂️ 🙋🏽‍♀️ 🙋🏽 🙋🏽‍♂️ 🧏🏽‍♀️ 🧏🏽 🧏🏽‍♂️ 🤦🏽‍♀️ 🤦🏽 🤦🏽‍♂️ 🤷🏽‍♀️ 🤷🏽 🤷🏽‍♂️ 🙎🏽‍♀️ 🙎🏽 🙎🏽‍♂️ 🙍🏽‍♀️ 🙍🏽 🙍🏽‍♂️ 💇🏽‍♀️ 💇🏽 💇🏽‍♂️ 💆🏽‍♀️ 💆🏽 💆🏽‍♂️ 🧖🏽‍♀️ 🧖🏽 🧖🏽‍♂️ 💃🏽 🕺🏽 🕴🏽 👩🏽‍🦽 🧑🏽‍🦽 👨🏽‍🦽 👩🏽‍🦼 🧑🏽‍🦼 👨🏽‍🦼 🚶🏽‍♀️ 🚶🏽 🚶🏽‍♂️ 👩🏽‍🦯 🧑🏽‍🦯 👨🏽‍🦯 🧎🏽‍♀️ 🧎🏽 🧎🏽‍♂️ 🏃🏽‍♀️ 🏃🏽 🏃🏽‍♂️ 🧍🏽‍♀️ 🧍🏽 🧍🏽‍♂️ 👭🏽 🧑🏽‍🤝‍🧑🏽 👬🏽 👫🏽 🧗🏽‍♀️ 🧗🏽 🧗🏽‍♂️ 🏇🏽 🏂🏽 🏌🏽‍♀️ 🏌🏽 🏌🏽‍♂️ 🏄🏽‍♀️ 🏄🏽 🏄🏽‍♂️ 🚣🏽‍♀️ 🚣🏽 🚣🏽‍♂️ 🏊🏽‍♀️ 🏊🏽 🏊🏽‍♂️ ⛹🏽‍♀️ ⛹🏽 ⛹🏽‍♂️ 🏋🏽‍♀️ 🏋🏽 🏋🏽‍♂️ 🚴🏽‍♀️ 🚴🏽 🚴🏽‍♂️ 🚵🏽‍♀️ 🚵🏽 🚵🏽‍♂️ 🤸🏽‍♀️ 🤸🏽 🤸🏽‍♂️ 🤽🏽‍♀️ 🤽🏽 🤽🏽‍♂️ 🤾🏽‍♀️ 🤾🏽 🤾🏽‍♂️ 🤹🏽‍♀️ 🤹🏽 🤹🏽‍♂️ 🧘🏽‍♀️ 🧘🏽 🧘🏽‍♂️ 🛀🏽 🛌'.split(' '),
72068
- 'Dark Brown Emojis': '👋🏾 🤚🏾 🖐🏾 ✋🏾 🖖🏾 👌🏾 🤌🏾 🤏🏾 ✌🏾 🤞🏾 🤟🏾 🤘🏾 🤙🏾 👈🏾 👉🏾 👆🏾 🖕🏾 👇🏾 ☝🏾 👍🏾 👎🏾 ✊🏾 👊🏾 🤛🏾 🤜🏾 👏🏾 🙌🏾 👐🏾 🤲🏾 🙏🏾 ✍🏾 💅🏾 🤳🏾 💪🏾 🦵🏾 🦶🏾 👂🏾 🦻🏾 👃🏾 👶🏾 👧🏾 🧒🏾 👦🏾 👩🏾 🧑🏾 👨🏾 👩🏾‍🦱 🧑🏾‍🦱 👨🏾‍🦱 👩🏾‍🦰 🧑🏾‍🦰 👨🏾‍🦰 👱🏾‍♀️ 👱🏾 👱🏾‍♂️ 👩🏾‍🦳 🧑🏾‍🦳 👨🏾‍🦳 👩🏾‍🦲 🧑🏾‍🦲 👨🏾‍🦲 🧔🏾 👵🏾 🧓🏾 👴🏾 👲🏾 👳🏾‍♀️ 👳🏾 👳🏾‍♂️ 🧕🏾 👮🏾‍♀️ 👮🏾 👮🏾‍♂️ 👷🏾‍♀️ 👷🏾 👷🏾‍♂️ 💂🏾‍♀️ 💂🏾 💂🏾‍♂️ 🕵🏾‍♀️ 🕵🏾 🕵🏾‍♂️ 👩🏾‍⚕️ 🧑🏾‍⚕️ 👨🏾‍⚕️ 👩🏾‍🌾 🧑🏾‍🌾 👨🏾‍🌾 👩🏾‍🍳 🧑🏾‍🍳 👨🏾‍🍳 👩🏾‍🎓 🧑🏾‍🎓 👨🏾‍🎓 👩🏾‍🎤 🧑🏾‍🎤 👨🏾‍🎤 👩🏾‍🏫 🧑🏾‍🏫 👨🏾‍🏫 👩🏾‍🏭 🧑🏾‍🏭 👨🏾‍🏭 👩🏾‍💻 🧑🏾‍💻 👨🏾‍💻 👩🏾‍💼 🧑🏾‍💼 👨🏾‍💼 👩🏾‍🔧 🧑🏾‍🔧 👨🏾‍🔧 👩🏾‍🔬 🧑🏾‍🔬 👨🏾‍🔬 👩🏾‍🎨 🧑🏾‍🎨 👨🏾‍🎨 👩🏾‍🚒 🧑🏾‍🚒 👨🏾‍🚒 👩🏾‍✈️ 🧑🏾‍✈️ 👨🏾‍✈️ 👩🏾‍🚀 🧑🏾‍🚀 👨🏾‍🚀 👩🏾‍⚖️ 🧑🏾‍⚖️ 👨🏾‍⚖️ 👰🏾‍♀️ 👰🏾 👰🏾‍♂️ 🤵🏾‍♀️ 🤵🏾 🤵🏾‍♂️ 👸🏾 🤴🏾 🥷🏾 🦸🏾‍♀️ 🦸🏾 🦸🏾‍♂️ 🦹🏾‍♀️ 🦹🏾 🦹🏾‍♂️ 🤶🏾 🧑🏾‍🎄 🎅🏾 🧙🏾‍♀️ 🧙🏾 🧙🏾‍♂️ 🧝🏾‍♀️ 🧝🏾 🧝🏾‍♂️ 🧛🏾‍♀️ 🧛🏾 🧛🏾‍♂️ 🧜🏾‍♀️ 🧜🏾 🧜🏾‍♂️ 🧚🏾‍♀️ 🧚🏾 🧚🏾‍♂️ 👼🏾 🤰🏾 🤱🏾 👩🏾‍🍼 🧑🏾‍🍼 👨🏾‍🍼 🙇🏾‍♀️ 🙇🏾 🙇🏾‍♂️ 💁🏾‍♀️ 💁🏾 💁🏾‍♂️ 🙅🏾‍♀️ 🙅🏾 🙅🏾‍♂️ 🙆🏾‍♀️ 🙆🏾 🙆🏾‍♂️ 🙋🏾‍♀️ 🙋🏾 🙋🏾‍♂️ 🧏🏾‍♀️ 🧏🏾 🧏🏾‍♂️ 🤦🏾‍♀️ 🤦🏾 🤦🏾‍♂️ 🤷🏾‍♀️ 🤷🏾 🤷🏾‍♂️ 🙎🏾‍♀️ 🙎🏾 🙎🏾‍♂️ 🙍🏾‍♀️ 🙍🏾 🙍🏾‍♂️ 💇🏾‍♀️ 💇🏾 💇🏾‍♂️ 💆🏾‍♀️ 💆🏾 💆🏾‍♂️ 🧖🏾‍♀️ 🧖🏾 🧖🏾‍♂️ 💃🏾 🕺🏾 🕴🏿 👩🏾‍🦽 🧑🏾‍🦽 👨🏾‍🦽 👩🏾‍🦼 🧑🏾‍🦼 👨🏾‍🦼 🚶🏾‍♀️ 🚶🏾 🚶🏾‍♂️ 👩🏾‍🦯 🧑🏾‍🦯 👨🏾‍🦯 🧎🏾‍♀️ 🧎🏾 🧎🏾‍♂️ 🏃🏾‍♀️ 🏃🏾 🏃🏾‍♂️ 🧍🏾‍♀️ 🧍🏾 🧍🏾‍♂️ 👭🏾 🧑🏾‍🤝‍🧑🏾 👬🏾 👫🏾 🧗🏾‍♀️ 🧗🏾 🧗🏾‍♂️ 🏇🏾 🏂🏾 🏌🏾‍♀️ 🏌🏾 🏌🏾‍♂️ 🏄🏾‍♀️ 🏄🏾 🏄🏾‍♂️ 🚣🏾‍♀️ 🚣🏾 🚣🏾‍♂️ 🏊🏾‍♀️ 🏊🏾 🏊🏾‍♂️ ⛹🏾‍♀️ ⛹🏾 ⛹🏾‍♂️ 🏋🏾‍♀️ 🏋🏾 🏋🏾‍♂️ 🚴🏾‍♀️ 🚴🏾 🚴🏾‍♂️ 🚵🏾‍♀️ 🚵🏾 🚵🏾‍♂️ 🤸🏾‍♀️ 🤸🏾 🤸🏾‍♂️ 🤽🏾‍♀️ 🤽🏾 🤽🏾‍♂️ 🤾🏾‍♀️ 🤾🏾 🤾🏾‍♂️ 🤹🏾‍♀️ 🤹🏾 🤹🏾‍♂️ 🧘🏾‍♀️ 🧘🏾 🧘🏾‍♂️ 🛀🏾 🛌'.split(' '),
72069
- 'Black Emojis': '👋🏿 🤚🏿 🖐🏿 ✋🏿 🖖🏿 👌🏿 🤌🏿 🤏🏿 ✌🏿 🤞🏿 🤟🏿 🤘🏿 🤙🏿 👈🏿 👉🏿 👆🏿 🖕🏿 👇🏿 ☝🏿 👍🏿 👎🏿 ✊🏿 👊🏿 🤛🏿 🤜🏿 👏🏿 🙌🏿 👐🏿 🤲🏿 🙏🏿 ✍🏿 💅🏿 🤳🏿 💪🏿 🦵🏿 🦶🏿 👂🏿 🦻🏿 👃🏿 👶🏿 👧🏿 🧒🏿 👦🏿 👩🏿 🧑🏿 👨🏿 👩🏿‍🦱 🧑🏿‍🦱 👨🏿‍🦱 👩🏿‍🦰 🧑🏿‍🦰 👨🏿‍🦰 👱🏿‍♀️ 👱🏿 👱🏿‍♂️ 👩🏿‍🦳 🧑🏿‍🦳 👨🏿‍🦳 👩🏿‍🦲 🧑🏿‍🦲 👨🏿‍🦲 🧔🏿 👵🏿 🧓🏿 👴🏿 👲🏿 👳🏿‍♀️ 👳🏿 👳🏿‍♂️ 🧕🏿 👮🏿‍♀️ 👮🏿 👮🏿‍♂️ 👷🏿‍♀️ 👷🏿 👷🏿‍♂️ 💂🏿‍♀️ 💂🏿 💂🏿‍♂️ 🕵🏿‍♀️ 🕵🏿 🕵🏿‍♂️ 👩🏿‍⚕️ 🧑🏿‍⚕️ 👨🏿‍⚕️ 👩🏿‍🌾 🧑🏿‍🌾 👨🏿‍🌾 👩🏿‍🍳 🧑🏿‍🍳 👨🏿‍🍳 👩🏿‍🎓 🧑🏿‍🎓 👨🏿‍🎓 👩🏿‍🎤 🧑🏿‍🎤 👨🏿‍🎤 👩🏿‍🏫 🧑🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 🧑🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 🧑🏿‍💻 👨🏿‍💻 👩🏿‍💼 🧑🏿‍💼 👨🏿‍💼 👩🏿‍🔧 🧑🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 🧑🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 🧑🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 🧑🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 🧑🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 🧑🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 🧑🏿‍⚖️ 👨🏿‍⚖️ 👰🏿‍♀️ 👰🏿 👰🏿‍♂️ 🤵🏿‍♀️ 🤵🏿 🤵🏿‍♂️ 👸🏿 🤴🏿 🥷🏿 🦸🏿‍♀️ 🦸🏿 🦸🏿‍♂️ 🦹🏿‍♀️ 🦹🏿 🦹🏿‍♂️ 🤶🏿 🧑🏿‍🎄 🎅🏿 🧙🏿‍♀️ 🧙🏿 🧙🏿‍♂️ 🧝🏿‍♀️ 🧝🏿 🧝🏿‍♂️ 🧛🏿‍♀️ 🧛🏿 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿 🧜🏿‍♂️ 🧚🏿‍♀️ 🧚🏿 🧚🏿‍♂️ 👼🏿 🤰🏿 🤱🏿 👩🏿‍🍼 🧑🏿‍🍼 👨🏿‍🍼 🙇🏿‍♀️ 🙇🏿 🙇🏿‍♂️ 💁🏿‍♀️ 💁🏿 💁🏿‍♂️ 🙅🏿‍♀️ 🙅🏿 🙅🏿‍♂️ 🙆🏿‍♀️ 🙆🏿 🙆🏿‍♂️ 🙋🏿‍♀️ 🙋🏿 🙋🏿‍♂️ 🧏🏿‍♀️ 🧏🏿 🧏🏿‍♂️ 🤦🏿‍♀️ 🤦🏿 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿 🤷🏿‍♂️ 🙎🏿‍♀️ 🙎🏿 🙎🏿‍♂️ 🙍🏿‍♀️ 🙍🏿 🙍🏿‍♂️ 💇🏿‍♀️ 💇🏿 💇🏿‍♂️ 💆🏿‍♀️ 💆🏿 💆🏿‍♂️ 🧖🏿‍♀️ 🧖🏿 🧖🏿‍♂️ 💃🏿 🕺🏿 🕴🏿 👩🏿‍🦽 🧑🏿‍🦽 👨🏿‍🦽 👩🏿‍🦼 🧑🏿‍🦼 👨🏿‍🦼 🚶🏿‍♀️ 🚶🏿 🚶🏿‍♂️ 👩🏿‍🦯 🧑🏿‍🦯 👨🏿‍🦯 🧎🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🏃🏿‍♀️ 🏃🏿 🏃🏿‍♂️ 🧍🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 👭🏿 🧑🏿‍🤝‍🧑🏿 👬🏿 👫🏿 🧗🏿‍♀️ 🧗🏿 🧗🏿‍♂️ 🏇🏿 🏂🏿 🏌🏿‍♀️ 🏌🏿 🏌🏿‍♂️ 🏄🏿‍♀️ 🏄🏿 🏄🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🚣🏿‍♂️ 🏊🏿‍♀️ 🏊🏿 🏊🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 ⛹🏿‍♂️ 🏋🏿‍♀️ 🏋🏿 🏋🏿‍♂️ 🚴🏿‍♀️ 🚴🏿 🚴🏿‍♂️ 🚵🏿‍♀️ 🚵🏿 🚵🏿‍♂️ 🤸🏿‍♀️ 🤸🏿 🤸🏿‍♂️ 🤽🏿‍♀️ 🤽🏿 🤽🏿‍♂️ 🤾🏿‍♀️ 🤾🏿 🤾🏿‍♂️ 🤹🏿‍♀️ 🤹🏿 🤹🏿‍♂️ 🧘🏿‍♀️ 🧘🏿 🧘🏿‍♂️ 🛀🏿 🛌🏿'.split(' '),
72070
- 'Animals & Nature': '🐶 🐱 🐭 🐹 🐰 🦊 🐻 🐼 🐻‍❄️ 🐨 🐯 🦁 🐮 🐷 🐽 🐸 🐵 🙈 🙉 🙊 🐒 🐔 🐧 🐦 🐤 🐣 🐥 🦆 🦅 🦉 🦇 🐺 🐗 🐴 🦄 🐝 🪱 🐛 🦋 🐌 🐞 🐜 🪰 🪲 🪳 🦟 🦗 🕷 🕸 🦂 🐢 🐍 🦎 🦖 🦕 🐙 🦑 🦐 🦞 🦀 🐡 🐠 🐟 🐬 🐳 🐋 🦈 🐊 🐅 🐆 🦓 🦍 🦧 🦣 🐘 🦛 🦏 🐪 🐫 🦒 🦘 🦬 🐃 🐂 🐄 🐎 🐖 🐏 🐑 🦙 🐐 🦌 🐕 🐩 🦮 🐕‍🦺 🐈 🐈‍⬛ 🪶 🐓 🦃 🦤 🦚 🦜 🦢 🦩 🕊 🐇 🦝 🦨 🦡 🦫 🦦 🦥 🐁 🐀 🐿 🦔 🐾 🐉 🐲 🌵 🎄 🌲 🌳 🌴 🪵 🌱 🌿 ☘️ 🍀 🎍 🪴 🎋 🍃 🍂 🍁 🍄 🐚 🪨 🌾 💐 🌷 🌹 🥀 🌺 🌸 🌼 🌻 🌞 🌝 🌛 🌜 🌚 🌕 🌖 🌗 🌘 🌑 🌒 🌓 🌔 🌙 🌎 🌍 🌏 🪐 💫 ⭐️ 🌟 ✨ ⚡️ ☄️ 💥 🔥 🌪 🌈 ☀️ 🌤 ⛅️ 🌥 ☁️ 🌦 🌧 ⛈ 🌩 🌨 ❄️ ☃️ ⛄️ 🌬 💨 💧 💦 ☔️ ☂️ 🌊 🌫'.split(' '),
72071
- 'Food & Drink': '🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🫐 🍈 🍒 🍑 🥭 🍍 🥥 🥝 🍅 🍆 🥑 🥦 🥬 🥒 🌶 🫑 🌽 🥕 🫒 🧄 🧅 🥔 🍠 🥐 🥯 🍞 🥖 🥨 🧀 🥚 🍳 🧈 🥞 🧇 🥓 🥩 🍗 🍖 🦴 🌭 🍔 🍟 🍕 🫓 🥪 🥙 🧆 🌮 🌯 🫔 🥗 🥘 🫕 🥫 🍝 🍜 🍲 🍛 🍣 🍱 🥟 🦪 🍤 🍙 🍚 🍘 🍥 🥠 🥮 🍢 🍡 🍧 🍨 🍦 🥧 🧁 🍰 🎂 🍮 🍭 🍬 🍫 🍿 🍩 🍪 🌰 🥜 🍯 🥛 🍼 🫖 ☕️ 🍵 🧃 🥤 🧋 🍶 🍺 🍻 🥂 🍷 🥃 🍸 🍹 🧉 🍾 🧊 🥄 🍴 🍽 🥣 🥡 🥢 🧂'.split(' '),
72072
- 'Activity and Sports': '⚽️ 🏀 🏈 ⚾️ 🥎 🎾 🏐 🏉 🥏 🎱 🪀 🏓 🏸 🏒 🏑 🥍 🏏 🪃 🥅 ⛳️ 🪁 🏹 🎣 🤿 🥊 🥋 🎽 🛹 🛼 🛷 ⛸ 🥌 🎿 ⛷ 🏂 🪂 🏋️‍♀️ 🏋️ 🏋️‍♂️ 🤼‍♀️ 🤼 🤼‍♂️ 🤸‍♀️ 🤸 🤸‍♂️ ⛹️‍♀️ ⛹️ ⛹️‍♂️ 🤺 🤾‍♀️ 🤾 🤾‍♂️ 🏌️‍♀️ 🏌️ 🏌️‍♂️ 🏇 🧘‍♀️ 🧘 🧘‍♂️ 🏄‍♀️ 🏄 🏄‍♂️ 🏊‍♀️ 🏊 🏊‍♂️ 🤽‍♀️ 🤽 🤽‍♂️ 🚣‍♀️ 🚣 🚣‍♂️ 🧗‍♀️ 🧗 🧗‍♂️ 🚵‍♀️ 🚵 🚵‍♂️ 🚴‍♀️ 🚴 🚴‍♂️ 🏆 🥇 🥈 🥉 🏅 🎖 🏵 🎗 🎫 🎟 🎪 🤹 🤹‍♂️ 🤹‍♀️ 🎭 🩰 🎨 🎬 🎤 🎧 🎼 🎹 🥁 🪘 🎷 🎺 🪗 🎸 🪕 🎻 🎲 ♟ 🎯 🎳 🎮 🎰 🧩'.split(' '),
72073
- 'Travel & Places': '🚗 🚕 🚙 🚌 🚎 🏎 🚓 🚑 🚒 🚐 🛻 🚚 🚛 🚜 🦯 🦽 🦼 🛴 🚲 🛵 🏍 🛺 🚨 🚔 🚍 🚘 🚖 🚡 🚠 🚟 🚃 🚋 🚞 🚝 🚄 🚅 🚈 🚂 🚆 🚇 🚊 🚉 ✈️ 🛫 🛬 🛩 💺 🛰 🚀 🛸 🚁 🛶 ⛵️ 🚤 🛥 🛳 ⛴ 🚢 ⚓️ 🪝 ⛽️ 🚧 🚦 🚥 🚏 🗺 🗿 🗽 🗼 🏰 🏯 🏟 🎡 🎢 🎠 ⛲️ ⛱ 🏖 🏝 🏜 🌋 ⛰ 🏔 🗻 🏕 ⛺️ 🛖 🏠 🏡 🏘 🏚 🏗 🏭 🏢 🏬 🏣 🏤 🏥 🏦 🏨 🏪 🏫 🏩 💒 🏛 ⛪️ 🕌 🕍 🛕 🕋 ⛩ 🛤 🛣 🗾 🎑 🏞 🌅 🌄 🌠 🎇 🎆 🌇 🌆 🏙 🌃 🌌 🌉 🌁'.split(' '),
72074
- Objects: '⌚️ 📱 📲 💻 ⌨️ 🖥 🖨 🖱 🖲 🕹 🗜 💽 💾 💿 📀 📼 📷 📸 📹 🎥 📽 🎞 📞 ☎️ 📟 📠 📺 📻 🎙 🎚 🎛 🧭 ⏱ ⏲ ⏰ 🕰 ⌛️ ⏳ 📡 🔋 🔌 💡 🔦 🕯 🪔 🧯 🛢 💸 💵 💴 💶 💷 🪙 💰 💳 💎 ⚖️ 🪜 🧰 🪛 🔧 🔨 ⚒ 🛠 ⛏ 🪚 🔩 ⚙️ 🪤 🧱 ⛓ 🧲 🔫 💣 🧨 🪓 🔪 🗡 ⚔️ 🛡 🚬 ⚰️ 🪦 ⚱️ 🏺 🔮 📿 🧿 💈 ⚗️ 🔭 🔬 🕳 🩹 🩺 💊 💉 🩸 🧬 🦠 🧫 🧪 🌡 🧹 🪠 🧺 🧻 🚽 🚰 🚿 🛁 🛀 🧼 🪥 🪒 🧽 🪣 🧴 🛎 🔑 🗝 🚪 🪑 🛋 🛏 🛌 🧸 🪆 🖼 🪞 🪟 🛍 🛒 🎁 🎈 🎏 🎀 🪄 🪅 🎊 🎉 🎎 🏮 🎐 🧧 ✉️ 📩 📨 📧 💌 📥 📤 📦 🏷 🪧 📪 📫 📬 📭 📮 📯 📜 📃 📄 📑 🧾 📊 📈 📉 🗒 🗓 📆 📅 🗑 📇 🗃 🗳 🗄 📋 📁 📂 🗂 🗞 📰 📓 📔 📒 📕 📗 📘 📙 📚 📖 🔖 🧷 🔗 📎 🖇 📐 📏 🧮 📌 📍 ✂️ 🖊 🖋 ✒️ 🖌 🖍 📝 ✏️ 🔍 🔎 🔏 🔐 🔒 🔓'.split(' '),
72075
- Symbols: '❤️ 🧡 💛 💚 💙 💜 🖤 🤍 🤎 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🛗 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 ⚧ 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ⏏️ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ ♾ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 🔜 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ 🟥 🟧 🟨 🟩 🟦 🟪 ⬛️ ⬜️ 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 🕟 🕠 🕡 🕢 🕣 🕤 🕥 🕦 🕧'.split(' '),
72076
- 'Non-Emoji Symbols': '✢ ✣ ✤ ✥ ✦ ✧ ★ ☆ ✯ ✡︎ ✩ ✪ ✫ ✬ ✭ ✮ ✶ ✷ ✵ ✸ ✹ → ⇒ ⟹ ⇨ ⇾ ➾ ⇢ ☛ ☞ ➔ ➜ ➙ ➛ ➝ ➞ ♠︎ ♣︎ ♥︎ ♦︎ ♤ ♧ ♡ ♢ ♚ ♛ ♜ ♝ ♞ ♟ ♔ ♕ ♖ ♗ ♘ ♙ ⚀ ⚁ ⚂ ⚃ ⚄ ⚅ 🂠 ⚈ ⚉ ⚆ ⚇ 𓀀 𓀁 𓀂 𓀃 𓀄 𓀅 𓀆 𓀇 𓀈 𓀉 𓀊 𓀋 𓀌 𓀍 𓀎 𓀏 𓀐 𓀑 𓀒 𓀓 𓀔 𓀕 𓀖 𓀗 𓀘 𓀙 𓀚 𓀛 𓀜 𓀝'.split(' '),
72077
- Flags: '🏳️ 🏴 🏁 🚩 🏳️‍🌈 🏳️‍⚧️ 🏴‍☠️'.split(' '),
72078
- Countries: '🇦🇫 🇦🇽 🇦🇱 🇩🇿 🇦🇸 🇦🇩 🇦🇴 🇦🇮 🇦🇶 🇦🇬 🇦🇷 🇦🇲 🇦🇼 🇦🇺 🇦🇹 🇦🇿 🇧🇸 🇧🇭 🇧🇩 🇧🇧 🇧🇾 🇧🇪 🇧🇿 🇧🇯 🇧🇲 🇧🇹 🇧🇴 🇧🇦 🇧🇼 🇧🇷 🇮🇴 🇻🇬 🇧🇳 🇧🇬 🇧🇫 🇧🇮 🇰🇭 🇨🇲 🇨🇦 🇮🇨 🇨🇻 🇧🇶 🇰🇾 🇨🇫 🇹🇩 🇨🇱 🇨🇳 🇨🇽 🇨🇨 🇨🇴 🇰🇲 🇨🇬 🇨🇩 🇨🇰 🇨🇷 🇨🇮 🇭🇷 🇨🇺 🇨🇼 🇨🇾 🇨🇿 🇩🇰 🇩🇯 🇩🇲 🇩🇴 🇪🇨 🇪🇬 🇸🇻 🇬🇶 🇪🇷 🇪🇪 🇪🇹 🇪🇺 🇫🇰 🇫🇴 🇫🇯 🇫🇮 🇫🇷 🇬🇫 🇵🇫 🇹🇫 🇬🇦 🇬🇲 🇬🇪 🇩🇪 🇬🇭 🇬🇮 🇬🇷 🇬🇱 🇬🇩 🇬🇵 🇬🇺 🇬🇹 🇬🇬 🇬🇳 🇬🇼 🇬🇾 🇭🇹 🇭🇳 🇭🇰 🇭🇺 🇮🇸 🇮🇳 🇮🇩 🇮🇷 🇮🇶 🇮🇪 🇮🇲 🇮🇱 🇮🇹 🇯🇲 🇯🇵 🎌 🇯🇪 🇯🇴 🇰🇿 🇰🇪 🇰🇮 🇽🇰 🇰🇼 🇰🇬 🇱🇦 🇱🇻 🇱🇧 🇱🇸 🇱🇷 🇱🇾 🇱🇮 🇱🇹 🇱🇺 🇲🇴 🇲🇰 🇲🇬 🇲🇼 🇲🇾 🇲🇻 🇲🇱 🇲🇹 🇲🇭 🇲🇶 🇲🇷 🇲🇺 🇾🇹 🇲🇽 🇫🇲 🇲🇩 🇲🇨 🇲🇳 🇲🇪 🇲🇸 🇲🇦 🇲🇿 🇲🇲 🇳🇦 🇳🇷 🇳🇵 🇳🇱 🇳🇨 🇳🇿 🇳🇮 🇳🇪 🇳🇬 🇳🇺 🇳🇫 🇰🇵 🇲🇵 🇳🇴 🇴🇲 🇵🇰 🇵🇼 🇵🇸 🇵🇦 🇵🇬 🇵🇾 🇵🇪 🇵🇭 🇵🇳 🇵🇱 🇵🇹 🇵🇷 🇶🇦 🇷🇪 🇷🇴 🇷🇺 🇷🇼 🇼🇸 🇸🇲 🇸🇦 🇸🇳 🇷🇸 🇸🇨 🇸🇱 🇸🇬 🇸🇽 🇸🇰 🇸🇮 🇬🇸 🇸🇧 🇸🇴 🇿🇦 🇰🇷 🇸🇸 🇪🇸 🇱🇰 🇧🇱 🇸🇭 🇰🇳 🇱🇨 🇵🇲 🇻🇨 🇸🇩 🇸🇷 🇸🇿 🇸🇪 🇨🇭 🇸🇾 🇹🇼 🇹🇯 🇹🇿 🇹🇭 🇹🇱 🇹🇬 🇹🇰 🇹🇴 🇹🇹 🇹🇳 🇹🇷 🇹🇲 🇹🇨 🇹🇻 🇻🇮 🇺🇬 🇺🇦 🇦🇪 🇬🇧 🇺🇳 🇺🇸 🇺🇾 🇺🇿 🇻🇺 🇻🇦 🇻🇪 🇻🇳 🇼🇫 🇪🇭 🇾🇪 🇿🇲 🇿🇼'.split(' '),
72079
- 'New Emojis 2020': '🥲 🥸 🤌 🤌🏻 🤌🏼 🤌🏽 🤌🏾 🤌🏿 🫀 🫁 🥷 🤵‍♀️ 🤵🏻‍♀️ 🤵🏼‍♀️ 🤵🏽‍♀️ 🤵🏾‍♀️ 🤵🏿‍♀️ 🤵‍♂️ 🤵🏻‍♂️ 🤵🏼‍♂️ 🤵🏽‍♂️ 🤵🏾‍♂️ 🤵🏿‍♂️ 👰‍♀️ 👰🏻‍♀️ 👰🏼‍♀️ 👰🏽‍♀️ 👰🏾‍♀️ 👰🏿‍♀️ 👰‍♂️ 👰🏻‍♂️ 👰🏼‍♂️ 👰🏽‍♂️ 👰🏾‍♂️ 👰🏿‍♂️ 👩‍🍼 👩🏻‍🍼 👩🏼‍🍼 👩🏽‍🍼 👩🏾‍🍼 👩🏿‍🍼 🧑‍🍼 🧑🏻‍🍼 🧑🏼‍🍼 🧑🏽‍🍼 🧑🏾‍🍼 🧑🏿‍🍼 👨‍🍼 👨🏻‍🍼 👨🏼‍🍼 👨🏽‍🍼 👨🏾‍🍼 👨🏿‍🍼 🧑‍🎄 🧑🏻‍🎄 🧑🏼‍🎄 🧑🏽‍🎄 🧑🏾‍🎄 🧑🏿‍🎄 🫂 🐈‍⬛ 🦬 🦣 🦫 🐻‍❄️ 🦤 🪶 🦭 🪲 🪳 🪰 🪱 🪴 🫐 🫒 🫑 🫓 🫔 🫕 🫖 🧋 🪨 🪵 🛖 🛻 🛼 🪄 🪅 🪆 🪡 🪢 🩴 🪖 🪗 🪘 🪙 🪃 🪚 🪛 🪝 🪜 🛗 🪞 🪟 🪠 🪤 🪣 🪥 🪦 🪧 🏳️‍⚧️'.split(' '),
72080
- 'New Emoji 2021': '😮‍💨 😵‍💫 😶‍🌫️ ❤️‍🔥 ❤️‍🩹 🧔‍♀️ 🧔🏻‍♀️ 🧔🏼‍♀️ 🧔🏽‍♀️ 🧔🏾‍♀️ 🧔🏿‍♀️ 🧔‍♂️ 🧔🏻‍♂️ 🧔🏼‍♂️ 🧔🏽‍♂️ 🧔🏾‍♂️ 🧔🏿‍♂️ 💑🏻 💑🏼 💑🏽 💑🏾 💑🏿 💏🏻 💏🏼 💏🏽 💏🏾 💏🏿 👨🏻‍❤️‍👨🏻 👨🏻‍❤️‍👨🏼 👨🏻‍❤️‍👨🏽 👨🏻‍❤️‍👨🏾 👨🏻‍❤️‍👨🏿 👨🏼‍❤️‍👨🏻 👨🏼‍❤️‍👨🏼 👨🏼‍❤️‍👨🏽 👨🏼‍❤️‍👨🏾 👨🏼‍❤️‍👨🏿 👨🏽‍❤️‍👨🏻 👨🏽‍❤️‍👨🏼 👨🏽‍❤️‍👨🏽 👨🏽‍❤️‍👨🏾 👨🏽‍❤️‍👨🏿 👨🏾‍❤️‍👨🏻 👨🏾‍❤️‍👨🏼 👨🏾‍❤️‍👨🏽 👨🏾‍❤️‍👨🏾 👨🏾‍❤️‍👨🏿 👨🏿‍❤️‍👨🏻 👨🏿‍❤️‍👨🏼 👨🏿‍❤️‍👨🏽 👨🏿‍❤️‍👨🏾 👨🏿‍❤️‍👨🏿 👩🏻‍❤️‍👨🏻 👩🏻‍❤️‍👨🏼 👩🏻‍❤️‍👨🏽 👩🏻‍❤️‍👨🏾 👩🏻‍❤️‍👨🏿 👩🏻‍❤️‍👩🏻 👩🏻‍❤️‍👩🏼 👩🏻‍❤️‍👩🏽 👩🏻‍❤️‍👩🏾 👩🏻‍❤️‍👩🏿 👩🏼‍❤️‍👨🏻 👩🏼‍❤️‍👨🏼 👩🏼‍❤️‍👨🏽 👩🏼‍❤️‍👨🏾 👩🏼‍❤️‍👨🏿 👩🏼‍❤️‍👩🏻 👩🏼‍❤️‍👩🏼 👩🏼‍❤️‍👩🏽 👩🏼‍❤️‍👩🏾 👩🏼‍❤️‍👩🏿 👩🏽‍❤️‍👨🏻 👩🏽‍❤️‍👨🏼 👩🏽‍❤️‍👨🏽 👩🏽‍❤️‍👨🏾 👩🏽‍❤️‍👨🏿 👩🏽‍❤️‍👩🏻 👩🏽‍❤️‍👩🏼 👩🏽‍❤️‍👩🏽 👩🏽‍❤️‍👩🏾 👩🏽‍❤️‍👩🏿 👩🏾‍❤️‍👨🏻 👩🏾‍❤️‍👨🏼 👩🏾‍❤️‍👨🏽 👩🏾‍❤️‍👨🏾 👩🏾‍❤️‍👨🏿 👩🏾‍❤️‍👩🏻 👩🏾‍❤️‍👩🏼 👩🏾‍❤️‍👩🏽 👩🏾‍❤️‍👩🏾 👩🏾‍❤️‍👩🏿 👩🏿‍❤️‍👨🏻 👩🏿‍❤️‍👨🏼 👩🏿‍❤️‍👨🏽 👩🏿‍❤️‍👨🏾 👩🏿‍❤️‍👨🏿 👩🏿‍❤️‍👩🏻 👩🏿‍❤️‍👩🏼 👩🏿‍❤️‍👩🏽 👩🏿‍❤️‍👩🏾 👩🏿‍❤️‍👩🏿 🧑🏻‍❤️‍🧑🏼 🧑🏻‍❤️‍🧑🏽 🧑🏻‍❤️‍🧑🏾 🧑🏻‍❤️‍🧑🏿 🧑🏼‍❤️‍🧑🏻 🧑🏼‍❤️‍🧑🏽 🧑🏼‍❤️‍🧑🏾 🧑🏼‍❤️‍🧑🏿 🧑🏽‍❤️‍🧑🏻 🧑🏽‍❤️‍🧑🏼 🧑🏽‍❤️‍🧑🏾 🧑🏽‍❤️‍🧑🏿 🧑🏾‍❤️‍🧑🏻 🧑🏾‍❤️‍🧑🏼 🧑🏾‍❤️‍🧑🏽 🧑🏾‍❤️‍🧑🏿 🧑🏿‍❤️‍🧑🏻 🧑🏿‍❤️‍🧑🏼 🧑🏿‍❤️‍🧑🏽 🧑🏿‍❤️‍🧑🏾 👨🏻‍❤️‍💋‍👨🏻 👨🏻‍❤️‍💋‍👨🏼 👨🏻‍❤️‍💋‍👨🏽 👨🏻‍❤️‍💋‍👨🏾 👨🏻‍❤️‍💋‍👨🏿 👨🏼‍❤️‍💋‍👨🏻 👨🏼‍❤️‍💋‍👨🏼 👨🏼‍❤️‍💋‍👨🏽 👨🏼‍❤️‍💋‍👨🏾 👨🏼‍❤️‍💋‍👨🏿 👨🏽‍❤️‍💋‍👨🏻 👨🏽‍❤️‍💋‍👨🏼 👨🏽‍❤️‍💋‍👨🏽 👨🏽‍❤️‍💋‍👨🏾 👨🏽‍❤️‍💋‍👨🏿 👨🏾‍❤️‍💋‍👨🏻 👨🏾‍❤️‍💋‍👨🏼 👨🏾‍❤️‍💋‍👨🏽 👨🏾‍❤️‍💋‍👨🏾 👨🏾‍❤️‍💋‍👨🏿 👨🏿‍❤️‍💋‍👨🏻 👨🏿‍❤️‍💋‍👨🏼 👨🏿‍❤️‍💋‍👨🏽 👨🏿‍❤️‍💋‍👨🏾 👨🏿‍❤️‍💋‍👨🏿 👩🏻‍❤️‍💋‍👨🏻 👩🏻‍❤️‍💋‍👨🏼 👩🏻‍❤️‍💋‍👨🏽 👩🏻‍❤️‍💋‍👨🏾 👩🏻‍❤️‍💋‍👨🏿 👩🏻‍❤️‍💋‍👩🏻 👩🏻‍❤️‍💋‍👩🏼 👩🏻‍❤️‍💋‍👩🏽 👩🏻‍❤️‍💋‍👩🏾 👩🏻‍❤️‍💋‍👩🏿 👩🏼‍❤️‍💋‍👨🏻 👩🏼‍❤️‍💋‍👨🏼 👩🏼‍❤️‍💋‍👨🏽 👩🏼‍❤️‍💋‍👨🏾 👩🏼‍❤️‍💋‍👨🏿 👩🏼‍❤️‍💋‍👩🏻 👩🏼‍❤️‍💋‍👩🏼 👩🏼‍❤️‍💋‍👩🏽 👩🏼‍❤️‍💋‍👩🏾 👩🏼‍❤️‍💋‍👩🏿 👩🏽‍❤️‍💋‍👨🏻 👩🏽‍❤️‍💋‍👨🏼 👩🏽‍❤️‍💋‍👨🏽 👩🏽‍❤️‍💋‍👨🏾 👩🏽‍❤️‍💋‍👨🏿 👩🏽‍❤️‍💋‍👩🏻 👩🏽‍❤️‍💋‍👩🏼 👩🏽‍❤️‍💋‍👩🏽 👩🏽‍❤️‍💋‍👩🏾 👩🏽‍❤️‍💋‍👩🏿 👩🏾‍❤️‍💋‍👨🏻 👩🏾‍❤️‍💋‍👨🏼 👩🏾‍❤️‍💋‍👨🏽 👩🏾‍❤️‍💋‍👨🏾 👩🏾‍❤️‍💋‍👨🏿 👩🏾‍❤️‍💋‍👩🏻 👩🏾‍❤️‍💋‍👩🏼 👩🏾‍❤️‍💋‍👩🏽 👩🏾‍❤️‍💋‍👩🏾 👩🏾‍❤️‍💋‍👩🏿 👩🏿‍❤️‍💋‍👨🏻 👩🏿‍❤️‍💋‍👨🏼 👩🏿‍❤️‍💋‍👨🏽 👩🏿‍❤️‍💋‍👨🏾 👩🏿‍❤️‍💋‍👨🏿 👩🏿‍❤️‍💋‍👩🏻 👩🏿‍❤️‍💋‍👩🏼 👩🏿‍❤️‍💋‍👩🏽 👩🏿‍❤️‍💋‍👩🏾 👩🏿‍❤️‍💋‍👩🏿 🧑🏻‍❤️‍💋‍🧑🏼 🧑🏻‍❤️‍💋‍🧑🏽 🧑🏻‍❤️‍💋‍🧑🏾 🧑🏻‍❤️‍💋‍🧑🏿 🧑🏼‍❤️‍💋‍🧑🏻 🧑🏼‍❤️‍💋‍🧑🏽 🧑🏼‍❤️‍💋‍🧑🏾 🧑🏼‍❤️‍💋‍🧑🏿 🧑🏽‍❤️‍💋‍🧑🏻 🧑🏽‍❤️‍💋‍🧑🏼 🧑🏽‍❤️‍💋‍🧑🏾 🧑🏽‍❤️‍💋‍🧑🏿 🧑🏾‍❤️‍💋‍🧑🏻 🧑🏾‍❤️‍💋‍🧑🏼 🧑🏾‍❤️‍💋‍🧑🏽 🧑🏾‍❤️‍💋‍🧑🏿 🧑🏿‍❤️‍💋‍🧑🏻 🧑🏿‍❤️‍💋‍🧑🏼 🧑🏿‍❤️‍💋‍🧑🏽 🧑🏿‍❤️‍💋‍🧑🏾'.split(' '),
72081
- 'New Emojis (Discorered randomly)': '🖌️'.split(' '),
72082
- 'Emojis used in WebGPT': ['▶', '❤'],
72083
- };
72084
- /**
72085
- * All possible emoji chars like "🍆", "🍡", "🍤", "🇨🇼", "🧑🏿‍❤️‍💋‍🧑🏻"...
72086
- */
72087
- const EMOJIS = new Set(Object.values(EMOJIS_IN_CATEGORIES).flat());
72088
- /**
72089
- * Emojis which are single pictograms like "🍆", "🍡", "🍤"...
72090
- */
72091
- const EMOJIS_OF_SINGLE_PICTOGRAM = new Set(Array.from(EMOJIS).filter((emoji) => emoji.length <= 2));
72092
- // TODO: [💴] DRY - just one version of emojis.ts
72093
- // TODO: Mirror from Collboard or some common package
72094
- // Note: [💞] Ignore a discrepancy between file name and entity name
72095
-
72096
72603
  /**
72097
72604
  * Default file globs scanned for emoji tags.
72098
72605
  */
@@ -72112,7 +72619,7 @@
72112
72619
  /**
72113
72620
  * Current schema version of the persisted emoji-tag scan cache.
72114
72621
  */
72115
- const EMOJI_TAG_SCAN_CACHE_VERSION = 1;
72622
+ const EMOJI_TAG_SCAN_CACHE_VERSION = 2;
72116
72623
  /**
72117
72624
  * Scans repository files for bracketed emoji tags while reusing per-file cache entries for unchanged files.
72118
72625
  */
@@ -72124,8 +72631,9 @@
72124
72631
  const tagPrefix = (_d = options.tagPrefix) !== null && _d !== void 0 ? _d : '';
72125
72632
  const filesToScan = await findFilesToScan(rootDir, includeGlobs, ignoreGlobs);
72126
72633
  const matcher = buildEmojiTagMatcher(options.candidateEmojis, tagPrefix);
72634
+ const candidateEmojisFingerprint = createCandidateEmojisFingerprint(options.candidateEmojis);
72127
72635
  const usedEmojis = new Set();
72128
- const existingCache = await readEmojiTagScanCache(rootDir);
72636
+ const existingCache = await readEmojiTagScanCache(rootDir, candidateEmojisFingerprint);
72129
72637
  const nextCacheFiles = { ...existingCache.files };
72130
72638
  let isCacheDirty = false;
72131
72639
  let scannedFileCount = 0;
@@ -72159,6 +72667,7 @@
72159
72667
  if (isCacheDirty) {
72160
72668
  await writeEmojiTagScanCache(rootDir, {
72161
72669
  version: EMOJI_TAG_SCAN_CACHE_VERSION,
72670
+ candidateEmojisFingerprint,
72162
72671
  files: nextCacheFiles,
72163
72672
  });
72164
72673
  }
@@ -72241,13 +72750,13 @@
72241
72750
  /**
72242
72751
  * Loads the persisted emoji-tag scan cache and falls back to an empty cache when it is missing or invalid.
72243
72752
  */
72244
- async function readEmojiTagScanCache(rootDir) {
72753
+ async function readEmojiTagScanCache(rootDir, candidateEmojisFingerprint) {
72245
72754
  try {
72246
72755
  const cacheContent = await promises.readFile(path.join(rootDir, EMOJI_TAG_SCAN_CACHE_FILE_PATH), 'utf-8');
72247
- return normalizeEmojiTagScanCache(JSON.parse(cacheContent));
72756
+ return normalizeEmojiTagScanCache(JSON.parse(cacheContent), candidateEmojisFingerprint);
72248
72757
  }
72249
72758
  catch (_a) {
72250
- return createEmptyEmojiTagScanCache();
72759
+ return createEmptyEmojiTagScanCache(candidateEmojisFingerprint);
72251
72760
  }
72252
72761
  }
72253
72762
  /**
@@ -72265,9 +72774,12 @@
72265
72774
  /**
72266
72775
  * Normalizes one parsed cache payload into the current typed cache shape.
72267
72776
  */
72268
- function normalizeEmojiTagScanCache(value) {
72269
- if (!isPlainObject(value) || value.version !== EMOJI_TAG_SCAN_CACHE_VERSION || !isPlainObject(value.files)) {
72270
- return createEmptyEmojiTagScanCache();
72777
+ function normalizeEmojiTagScanCache(value, candidateEmojisFingerprint) {
72778
+ if (!isPlainObject(value) ||
72779
+ value.version !== EMOJI_TAG_SCAN_CACHE_VERSION ||
72780
+ value.candidateEmojisFingerprint !== candidateEmojisFingerprint ||
72781
+ !isPlainObject(value.files)) {
72782
+ return createEmptyEmojiTagScanCache(candidateEmojisFingerprint);
72271
72783
  }
72272
72784
  const files = {};
72273
72785
  for (const [filePath, cachedValue] of Object.entries(value.files)) {
@@ -72293,18 +72805,33 @@
72293
72805
  }
72294
72806
  return {
72295
72807
  version: EMOJI_TAG_SCAN_CACHE_VERSION,
72808
+ candidateEmojisFingerprint,
72296
72809
  files,
72297
72810
  };
72298
72811
  }
72299
72812
  /**
72300
72813
  * Creates an empty cache payload for emoji-tag scans.
72301
72814
  */
72302
- function createEmptyEmojiTagScanCache() {
72815
+ function createEmptyEmojiTagScanCache(candidateEmojisFingerprint) {
72303
72816
  return {
72304
72817
  version: EMOJI_TAG_SCAN_CACHE_VERSION,
72818
+ candidateEmojisFingerprint,
72305
72819
  files: {},
72306
72820
  };
72307
72821
  }
72822
+ /**
72823
+ * Creates a stable fingerprint for the complete candidate emoji set.
72824
+ *
72825
+ * A cached scan is only valid when it was created from the exact same candidates.
72826
+ * Otherwise a tag that was not part of an older catalogue could be mistaken for a
72827
+ * fresh tag after the catalogue grows.
72828
+ *
72829
+ * @private internal utility of `scanEmojiTagUsage`
72830
+ */
72831
+ function createCandidateEmojisFingerprint(candidateEmojis) {
72832
+ const normalizedCandidateEmojis = Array.from(candidateEmojis).sort().join('\u0000');
72833
+ return crypto.createHash('sha256').update(normalizedCandidateEmojis, 'utf-8').digest('hex');
72834
+ }
72308
72835
  /**
72309
72836
  * Converts an absolute file path into a stable cache key relative to the scanned root.
72310
72837
  */
@@ -72333,6 +72860,77 @@
72333
72860
  }
72334
72861
  // Note: [?] Code in this file should never be published in any package
72335
72862
 
72863
+ /**
72864
+ * All possible emoji chars like "🍆", "🍡", "🍤"...
72865
+ * Note: this will be needed to update annually - now updated at 2022-01-19
72866
+ *
72867
+ * @see https://getemoji.com/
72868
+ * @deprecated Use /\p{Extended_Pictographic}/ instead
72869
+ *
72870
+ * @private within the repository
72871
+ */
72872
+ const EMOJIS_IN_CATEGORIES = {
72873
+ Smileys: '😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 🥺 😢 😭 😤 😠 😡 🤬 🤯 😳 🥵 🥶 😱 😨 😰 😥 😓 🤗 🤔 🤭 🤫 🤥 😶 😐 😑 😬 🙄 😯 😦 😧 😮 😲 🥱 😴 🤤 😪 😵 🤐 🥴 🤢 🤮 🤧 😷 🤒 🤕 🤑 🤠 😈 👿 👹 👺 🤡 💩 👻 💀 ☠️ 👽 👾 🤖 🎃 😺 😸 😹 😻 😼 😽 🙀 😿 😾'.split(' '),
72874
+ 'Gestures and Body Parts': '👋 🤚 🖐 ✋ 🖖 👌 🤌 🤏 ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍 👎 ✊ 👊 🤛 🤜 👏 🙌 👐 🤲 🤝 🙏 ✍️ 💅 🤳 💪 🦾 🦵 🦿 🦶 👣 👂 🦻 👃 🫀 🫁 🧠 🦷 🦴 👀 👁 👅 👄 💋 🩸'.split(' '),
72875
+ 'People and Fantasy': '👶 👧 🧒 👦 👩 🧑 👨 👩‍🦱 🧑‍🦱 👨‍🦱 👩‍🦰 🧑‍🦰 👨‍🦰 👱‍♀️ 👱 👱‍♂️ 👩‍🦳 🧑‍🦳 👨‍🦳 👩‍🦲 🧑‍🦲 👨‍🦲 🧔 👵 🧓 👴 👲 👳‍♀️ 👳 👳‍♂️ 🧕 👮‍♀️ 👮 👮‍♂️ 👷‍♀️ 👷 👷‍♂️ 💂‍♀️ 💂 💂‍♂️ 🕵️‍♀️ 🕵️ 🕵️‍♂️ 👩‍⚕️ 🧑‍⚕️ 👨‍⚕️ 👩‍🌾 🧑‍🌾 👨‍🌾 👩‍🍳 🧑‍🍳 👨‍🍳 👩‍🎓 🧑‍🎓 👨‍🎓 👩‍🎤 🧑‍🎤 👨‍🎤 👩‍🏫 🧑‍🏫 👨‍🏫 👩‍🏭 🧑‍🏭 👨‍🏭 👩‍💻 🧑‍💻 👨‍💻 👩‍💼 🧑‍💼 👨‍💼 👩‍🔧 🧑‍🔧 👨‍🔧 👩‍🔬 🧑‍🔬 👨‍🔬 👩‍🎨 🧑‍🎨 👨‍🎨 👩‍🚒 🧑‍🚒 👨‍🚒 👩‍✈️ 🧑‍✈️ 👨‍✈️ 👩‍🚀 🧑‍🚀 👨‍🚀 👩‍⚖️ 🧑‍⚖️ 👨‍⚖️ 👰‍♀️ 👰 👰‍♂️ 🤵‍♀️ 🤵 🤵‍♂️ 👸 🤴 🥷 🦸‍♀️ 🦸 🦸‍♂️ 🦹‍♀️ 🦹 🦹‍♂️ 🤶 🧑‍🎄 🎅 🧙‍♀️ 🧙 🧙‍♂️ 🧝‍♀️ 🧝 🧝‍♂️ 🧛‍♀️ 🧛 🧛‍♂️ 🧟‍♀️ 🧟 🧟‍♂️ 🧞‍♀️ 🧞 🧞‍♂️ 🧜‍♀️ 🧜 🧜‍♂️ 🧚‍♀️ 🧚 🧚‍♂️ 👼 🤰 🤱 👩‍🍼 🧑‍🍼 👨‍🍼 🙇‍♀️ 🙇 🙇‍♂️ 💁‍♀️ 💁 💁‍♂️ 🙅‍♀️ 🙅 🙅‍♂️ 🙆‍♀️ 🙆 🙆‍♂️ 🙋‍♀️ 🙋 🙋‍♂️ 🧏‍♀️ 🧏 🧏‍♂️ 🤦‍♀️ 🤦 🤦‍♂️ 🤷‍♀️ 🤷 🤷‍♂️ 🙎‍♀️ 🙎 🙎‍♂️ 🙍‍♀️ 🙍 🙍‍♂️ 💇‍♀️ 💇 💇‍♂️ 💆‍♀️ 💆 💆‍♂️ 🧖‍♀️ 🧖 🧖‍♂️ 💅 🤳 💃 🕺 👯‍♀️ 👯 👯‍♂️ 🕴 👩‍🦽 🧑‍🦽 👨‍🦽 👩‍🦼 🧑‍🦼 👨‍🦼 🚶‍♀️ 🚶 🚶‍♂️ 👩‍🦯 🧑‍🦯 👨‍🦯 🧎‍♀️ 🧎 🧎‍♂️ 🏃‍♀️ 🏃 🏃‍♂️ 🧍‍♀️ 🧍 🧍‍♂️ 👭 🧑‍🤝‍🧑 👬 👫 👩‍❤️‍👩 💑 👨‍❤️‍👨 👩‍❤️‍👨 👩‍❤️‍💋‍👩 💏 👨‍❤️‍💋‍👨 👩‍❤️‍💋‍👨 👪 👨‍👩‍👦 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 👨‍👩‍👧‍👧 👨‍👨‍👦 👨‍👨‍👧 👨‍👨‍👧‍👦 👨‍👨‍👦‍👦 👨‍👨‍👧‍👧 👩‍👩‍👦 👩‍👩‍👧 👩‍👩‍👧‍👦 👩‍👩‍👦‍👦 👩‍👩‍👧‍👧 👨‍👦 👨‍👦‍👦 👨‍👧 👨‍👧‍👦 👨‍👧‍👧 👩‍👦 👩‍👦‍👦 👩‍👧 👩‍👧‍👦 👩‍👧‍👧 🗣 👤 👥 🫂'.split(' '),
72876
+ 'Clothing and Accessories': '🧳 🌂 ☂️ 🧵 🪡 🪢 🧶 👓 🕶 🥽 🥼 🦺 👔 👕 👖 🧣 🧤 🧥 🧦 👗 👘 🥻 🩴 🩱 🩲 🩳 👙 👚 👛 👜 👝 🎒 👞 👟 🥾 🥿 👠 👡 🩰 👢 👑 👒 🎩 🎓 🧢 ⛑ 🪖 💄 💍 💼'.split(' '),
72877
+ 'Pale Emojis': '👋🏻 🤚🏻 🖐🏻 ✋🏻 🖖🏻 👌🏻 🤌🏻 🤏🏻 ✌🏻 🤞🏻 🤟🏻 🤘🏻 🤙🏻 👈🏻 👉🏻 👆🏻 🖕🏻 👇🏻 ☝🏻 👍🏻 👎🏻 ✊🏻 👊🏻 🤛🏻 🤜🏻 👏🏻 🙌🏻 👐🏻 🤲🏻 🙏🏻 ✍🏻 💅🏻 🤳🏻 💪🏻 🦵🏻 🦶🏻 👂🏻 🦻🏻 👃🏻 👶🏻 👧🏻 🧒🏻 👦🏻 👩🏻 🧑🏻 👨🏻 👩🏻‍🦱 🧑🏻‍🦱 👨🏻‍🦱 👩🏻‍🦰 🧑🏻‍🦰 👨🏻‍🦰 👱🏻‍♀️ 👱🏻 👱🏻‍♂️ 👩🏻‍🦳 🧑🏻‍🦳 👨🏻‍🦳 👩🏻‍🦲 🧑🏻‍🦲 👨🏻‍🦲 🧔🏻 👵🏻 🧓🏻 👴🏻 👲🏻 👳🏻‍♀️ 👳🏻 👳🏻‍♂️ 🧕🏻 👮🏻‍♀️ 👮🏻 👮🏻‍♂️ 👷🏻‍♀️ 👷🏻 👷🏻‍♂️ 💂🏻‍♀️ 💂🏻 💂🏻‍♂️ 🕵🏻‍♀️ 🕵🏻 🕵🏻‍♂️ 👩🏻‍⚕️ 🧑🏻‍⚕️ 👨🏻‍⚕️ 👩🏻‍🌾 🧑🏻‍🌾 👨🏻‍🌾 👩🏻‍🍳 🧑🏻‍🍳 👨🏻‍🍳 👩🏻‍🎓 🧑🏻‍🎓 👨🏻‍🎓 👩🏻‍🎤 🧑🏻‍🎤 👨🏻‍🎤 👩🏻‍🏫 🧑🏻‍🏫 👨🏻‍🏫 👩🏻‍🏭 🧑🏻‍🏭 👨🏻‍🏭 👩🏻‍💻 🧑🏻‍💻 👨🏻‍💻 👩🏻‍💼 🧑🏻‍💼 👨🏻‍💼 👩🏻‍🔧 🧑🏻‍🔧 👨🏻‍🔧 👩🏻‍🔬 🧑🏻‍🔬 👨🏻‍🔬 👩🏻‍🎨 🧑🏻‍🎨 👨🏻‍🎨 👩🏻‍🚒 🧑🏻‍🚒 👨🏻‍🚒 👩🏻‍✈️ 🧑🏻‍✈️ 👨🏻‍✈️ 👩🏻‍🚀 🧑🏻‍🚀 👨🏻‍🚀 👩🏻‍⚖️ 🧑🏻‍⚖️ 👨🏻‍⚖️ 👰🏻‍♀️ 👰🏻 👰🏻‍♂️ 🤵🏻‍♀️ 🤵🏻 🤵🏻‍♂️ 👸🏻 🤴🏻 🥷🏻 🦸🏻‍♀️ 🦸🏻 🦸🏻‍♂️ 🦹🏻‍♀️ 🦹🏻 🦹🏻‍♂️ 🤶🏻 🧑🏻‍🎄 🎅🏻 🧙🏻‍♀️ 🧙🏻 🧙🏻‍♂️ 🧝🏻‍♀️ 🧝🏻 🧝🏻‍♂️ 🧛🏻‍♀️ 🧛🏻 🧛🏻‍♂️ 🧜🏻‍♀️ 🧜🏻 🧜🏻‍♂️ 🧚🏻‍♀️ 🧚🏻 🧚🏻‍♂️ 👼🏻 🤰🏻 🤱🏻 👩🏻‍🍼 🧑🏻‍🍼 👨🏻‍🍼 🙇🏻‍♀️ 🙇🏻 🙇🏻‍♂️ 💁🏻‍♀️ 💁🏻 💁🏻‍♂️ 🙅🏻‍♀️ 🙅🏻 🙅🏻‍♂️ 🙆🏻‍♀️ 🙆🏻 🙆🏻‍♂️ 🙋🏻‍♀️ 🙋🏻 🙋🏻‍♂️ 🧏🏻‍♀️ 🧏🏻 🧏🏻‍♂️ 🤦🏻‍♀️ 🤦🏻 🤦🏻‍♂️ 🤷🏻‍♀️ 🤷🏻 🤷🏻‍♂️ 🙎🏻‍♀️ 🙎🏻 🙎🏻‍♂️ 🙍🏻‍♀️ 🙍🏻 🙍🏻‍♂️ 💇🏻‍♀️ 💇🏻 💇🏻‍♂️ 💆🏻‍♀️ 💆🏻 💆🏻‍♂️ 🧖🏻‍♀️ 🧖🏻 🧖🏻‍♂️ 💃🏻 🕺🏻 🕴🏻 👩🏻‍🦽 🧑🏻‍🦽 👨🏻‍🦽 👩🏻‍🦼 🧑🏻‍🦼 👨🏻‍🦼 🚶🏻‍♀️ 🚶🏻 🚶🏻‍♂️ 👩🏻‍🦯 🧑🏻‍🦯 👨🏻‍🦯 🧎🏻‍♀️ 🧎🏻 🧎🏻‍♂️ 🏃🏻‍♀️ 🏃🏻 🏃🏻‍♂️ 🧍🏻‍♀️ 🧍🏻 🧍🏻‍♂️ 👭🏻 🧑🏻‍🤝‍🧑🏻 👬🏻 👫🏻 🧗🏻‍♀️ 🧗🏻 🧗🏻‍♂️ 🏇🏻 🏂🏻 🏌🏻‍♀️ 🏌🏻 🏌🏻‍♂️ 🏄🏻‍♀️ 🏄🏻 🏄🏻‍♂️ 🚣🏻‍♀️ 🚣🏻 🚣🏻‍♂️ 🏊🏻‍♀️ 🏊🏻 🏊🏻‍♂️ ⛹🏻‍♀️ ⛹🏻 ⛹🏻‍♂️ 🏋🏻‍♀️ 🏋🏻 🏋🏻‍♂️ 🚴🏻‍♀️ 🚴🏻 🚴🏻‍♂️ 🚵🏻‍♀️ 🚵🏻 🚵🏻‍♂️ 🤸🏻‍♀️ 🤸🏻 🤸🏻‍♂️ 🤽🏻‍♀️ 🤽🏻 🤽🏻‍♂️ 🤾🏻‍♀️ 🤾🏻 🤾🏻‍♂️ 🤹🏻‍♀️ 🤹🏻 🤹🏻‍♂️ 🧘🏻‍♀️ 🧘🏻 🧘🏻‍♂️ 🛀🏻 🛌'.split(' '),
72878
+ 'Cream White Emojis': '👋🏼 🤚🏼 🖐🏼 ✋🏼 🖖🏼 👌🏼 🤌🏼 🤏🏼 ✌🏼 🤞🏼 🤟🏼 🤘🏼 🤙🏼 👈🏼 👉🏼 👆🏼 🖕🏼 👇🏼 ☝🏼 👍🏼 👎🏼 ✊🏼 👊🏼 🤛🏼 🤜🏼 👏🏼 🙌🏼 👐🏼 🤲🏼 🙏🏼 ✍🏼 💅🏼 🤳🏼 💪🏼 🦵🏼 🦶🏼 👂🏼 🦻🏼 👃🏼 👶🏼 👧🏼 🧒🏼 👦🏼 👩🏼 🧑🏼 👨🏼 👩🏼‍🦱 🧑🏼‍🦱 👨🏼‍🦱 👩🏼‍🦰 🧑🏼‍🦰 👨🏼‍🦰 👱🏼‍♀️ 👱🏼 👱🏼‍♂️ 👩🏼‍🦳 🧑🏼‍🦳 👨🏼‍🦳 👩🏼‍🦲 🧑🏼‍🦲 👨🏼‍🦲 🧔🏼 👵🏼 🧓🏼 👴🏼 👲🏼 👳🏼‍♀️ 👳🏼 👳🏼‍♂️ 🧕🏼 👮🏼‍♀️ 👮🏼 👮🏼‍♂️ 👷🏼‍♀️ 👷🏼 👷🏼‍♂️ 💂🏼‍♀️ 💂🏼 💂🏼‍♂️ 🕵🏼‍♀️ 🕵🏼 🕵🏼‍♂️ 👩🏼‍⚕️ 🧑🏼‍⚕️ 👨🏼‍⚕️ 👩🏼‍🌾 🧑🏼‍🌾 👨🏼‍🌾 👩🏼‍🍳 🧑🏼‍🍳 👨🏼‍🍳 👩🏼‍🎓 🧑🏼‍🎓 👨🏼‍🎓 👩🏼‍🎤 🧑🏼‍🎤 👨🏼‍🎤 👩🏼‍🏫 🧑🏼‍🏫 👨🏼‍🏫 👩🏼‍🏭 🧑🏼‍🏭 👨🏼‍🏭 👩🏼‍💻 🧑🏼‍💻 👨🏼‍💻 👩🏼‍💼 🧑🏼‍💼 👨🏼‍💼 👩🏼‍🔧 🧑🏼‍🔧 👨🏼‍🔧 👩🏼‍🔬 🧑🏼‍🔬 👨🏼‍🔬 👩🏼‍🎨 🧑🏼‍🎨 👨🏼‍🎨 👩🏼‍🚒 🧑🏼‍🚒 👨🏼‍🚒 👩🏼‍✈️ 🧑🏼‍✈️ 👨🏼‍✈️ 👩🏼‍🚀 🧑🏼‍🚀 👨🏼‍🚀 👩🏼‍⚖️ 🧑🏼‍⚖️ 👨🏼‍⚖️ 👰🏼‍♀️ 👰🏼 👰🏼‍♂️ 🤵🏼‍♀️ 🤵🏼 🤵🏼‍♂️ 👸🏼 🤴🏼 🥷🏼 🦸🏼‍♀️ 🦸🏼 🦸🏼‍♂️ 🦹🏼‍♀️ 🦹🏼 🦹🏼‍♂️ 🤶🏼 🧑🏼‍🎄 🎅🏼 🧙🏼‍♀️ 🧙🏼 🧙🏼‍♂️ 🧝🏼‍♀️ 🧝🏼 🧝🏼‍♂️ 🧛🏼‍♀️ 🧛🏼 🧛🏼‍♂️ 🧜🏼‍♀️ 🧜🏼 🧜🏼‍♂️ 🧚🏼‍♀️ 🧚🏼 🧚🏼‍♂️ 👼🏼 🤰🏼 🤱🏼 👩🏼‍🍼 🧑🏼‍🍼 👨🏼‍🍼 🙇🏼‍♀️ 🙇🏼 🙇🏼‍♂️ 💁🏼‍♀️ 💁🏼 💁🏼‍♂️ 🙅🏼‍♀️ 🙅🏼 🙅🏼‍♂️ 🙆🏼‍♀️ 🙆🏼 🙆🏼‍♂️ 🙋🏼‍♀️ 🙋🏼 🙋🏼‍♂️ 🧏🏼‍♀️ 🧏🏼 🧏🏼‍♂️ 🤦🏼‍♀️ 🤦🏼 🤦🏼‍♂️ 🤷🏼‍♀️ 🤷🏼 🤷🏼‍♂️ 🙎🏼‍♀️ 🙎🏼 🙎🏼‍♂️ 🙍🏼‍♀️ 🙍🏼 🙍🏼‍♂️ 💇🏼‍♀️ 💇🏼 💇🏼‍♂️ 💆🏼‍♀️ 💆🏼 💆🏼‍♂️ 🧖🏼‍♀️ 🧖🏼 🧖🏼‍♂️ 💃🏼 🕺🏼 🕴🏼 👩🏼‍🦽 🧑🏼‍🦽 👨🏼‍🦽 👩🏼‍🦼 🧑🏼‍🦼 👨🏼‍🦼 🚶🏼‍♀️ 🚶🏼 🚶🏼‍♂️ 👩🏼‍🦯 🧑🏼‍🦯 👨🏼‍🦯 🧎🏼‍♀️ 🧎🏼 🧎🏼‍♂️ 🏃🏼‍♀️ 🏃🏼 🏃🏼‍♂️ 🧍🏼‍♀️ 🧍🏼 🧍🏼‍♂️ 👭🏼 🧑🏼‍🤝‍🧑🏼 👬🏼 👫🏼 🧗🏼‍♀️ 🧗🏼 🧗🏼‍♂️ 🏇🏼 🏂🏼 🏌🏼‍♀️ 🏌🏼 🏌🏼‍♂️ 🏄🏼‍♀️ 🏄🏼 🏄🏼‍♂️ 🚣🏼‍♀️ 🚣🏼 🚣🏼‍♂️ 🏊🏼‍♀️ 🏊🏼 🏊🏼‍♂️ ⛹🏼‍♀️ ⛹🏼 ⛹🏼‍♂️ 🏋🏼‍♀️ 🏋🏼 🏋🏼‍♂️ 🚴🏼‍♀️ 🚴🏼 🚴🏼‍♂️ 🚵🏼‍♀️ 🚵🏼 🚵🏼‍♂️ 🤸🏼‍♀️ 🤸🏼 🤸🏼‍♂️ 🤽🏼‍♀️ 🤽🏼 🤽🏼‍♂️ 🤾🏼‍♀️ 🤾🏼 🤾🏼‍♂️ 🤹🏼‍♀️ 🤹🏼 🤹🏼‍♂️ 🧘🏼‍♀️ 🧘🏼 🧘🏼‍♂️ 🛀🏼 🛌'.split(' '),
72879
+ 'Brown Emojis': '👋🏽 🤚🏽 🖐🏽 ✋🏽 🖖🏽 👌🏽 🤌🏽 🤏🏽 ✌🏽 🤞🏽 🤟🏽 🤘🏽 🤙🏽 👈🏽 👉🏽 👆🏽 🖕🏽 👇🏽 ☝🏽 👍🏽 👎🏽 ✊🏽 👊🏽 🤛🏽 🤜🏽 👏🏽 🙌🏽 👐🏽 🤲🏽 🙏🏽 ✍🏽 💅🏽 🤳🏽 💪🏽 🦵🏽 🦶🏽 👂🏽 🦻🏽 👃🏽 👶🏽 👧🏽 🧒🏽 👦🏽 👩🏽 🧑🏽 👨🏽 👩🏽‍🦱 🧑🏽‍🦱 👨🏽‍🦱 👩🏽‍🦰 🧑🏽‍🦰 👨🏽‍🦰 👱🏽‍♀️ 👱🏽 👱🏽‍♂️ 👩🏽‍🦳 🧑🏽‍🦳 👨🏽‍🦳 👩🏽‍🦲 🧑🏽‍🦲 👨🏽‍🦲 🧔🏽 👵🏽 🧓🏽 👴🏽 👲🏽 👳🏽‍♀️ 👳🏽 👳🏽‍♂️ 🧕🏽 👮🏽‍♀️ 👮🏽 👮🏽‍♂️ 👷🏽‍♀️ 👷🏽 👷🏽‍♂️ 💂🏽‍♀️ 💂🏽 💂🏽‍♂️ 🕵🏽‍♀️ 🕵🏽 🕵🏽‍♂️ 👩🏽‍⚕️ 🧑🏽‍⚕️ 👨🏽‍⚕️ 👩🏽‍🌾 🧑🏽‍🌾 👨🏽‍🌾 👩🏽‍🍳 🧑🏽‍🍳 👨🏽‍🍳 👩🏽‍🎓 🧑🏽‍🎓 👨🏽‍🎓 👩🏽‍🎤 🧑🏽‍🎤 👨🏽‍🎤 👩🏽‍🏫 🧑🏽‍🏫 👨🏽‍🏫 👩🏽‍🏭 🧑🏽‍🏭 👨🏽‍🏭 👩🏽‍💻 🧑🏽‍💻 👨🏽‍💻 👩🏽‍💼 🧑🏽‍💼 👨🏽‍💼 👩🏽‍🔧 🧑🏽‍🔧 👨🏽‍🔧 👩🏽‍🔬 🧑🏽‍🔬 👨🏽‍🔬 👩🏽‍🎨 🧑🏽‍🎨 👨🏽‍🎨 👩🏽‍🚒 🧑🏽‍🚒 👨🏽‍🚒 👩🏽‍✈️ 🧑🏽‍✈️ 👨🏽‍✈️ 👩🏽‍🚀 🧑🏽‍🚀 👨🏽‍🚀 👩🏽‍⚖️ 🧑🏽‍⚖️ 👨🏽‍⚖️ 👰🏽‍♀️ 👰🏽 👰🏽‍♂️ 🤵🏽‍♀️ 🤵🏽 🤵🏽‍♂️ 👸🏽 🤴🏽 🥷🏽 🦸🏽‍♀️ 🦸🏽 🦸🏽‍♂️ 🦹🏽‍♀️ 🦹🏽 🦹🏽‍♂️ 🤶🏽 🧑🏽‍🎄 🎅🏽 🧙🏽‍♀️ 🧙🏽 🧙🏽‍♂️ 🧝🏽‍♀️ 🧝🏽 🧝🏽‍♂️ 🧛🏽‍♀️ 🧛🏽 🧛🏽‍♂️ 🧜🏽‍♀️ 🧜🏽 🧜🏽‍♂️ 🧚🏽‍♀️ 🧚🏽 🧚🏽‍♂️ 👼🏽 🤰🏽 🤱🏽 👩🏽‍🍼 🧑🏽‍🍼 👨🏽‍🍼 🙇🏽‍♀️ 🙇🏽 🙇🏽‍♂️ 💁🏽‍♀️ 💁🏽 💁🏽‍♂️ 🙅🏽‍♀️ 🙅🏽 🙅🏽‍♂️ 🙆🏽‍♀️ 🙆🏽 🙆🏽‍♂️ 🙋🏽‍♀️ 🙋🏽 🙋🏽‍♂️ 🧏🏽‍♀️ 🧏🏽 🧏🏽‍♂️ 🤦🏽‍♀️ 🤦🏽 🤦🏽‍♂️ 🤷🏽‍♀️ 🤷🏽 🤷🏽‍♂️ 🙎🏽‍♀️ 🙎🏽 🙎🏽‍♂️ 🙍🏽‍♀️ 🙍🏽 🙍🏽‍♂️ 💇🏽‍♀️ 💇🏽 💇🏽‍♂️ 💆🏽‍♀️ 💆🏽 💆🏽‍♂️ 🧖🏽‍♀️ 🧖🏽 🧖🏽‍♂️ 💃🏽 🕺🏽 🕴🏽 👩🏽‍🦽 🧑🏽‍🦽 👨🏽‍🦽 👩🏽‍🦼 🧑🏽‍🦼 👨🏽‍🦼 🚶🏽‍♀️ 🚶🏽 🚶🏽‍♂️ 👩🏽‍🦯 🧑🏽‍🦯 👨🏽‍🦯 🧎🏽‍♀️ 🧎🏽 🧎🏽‍♂️ 🏃🏽‍♀️ 🏃🏽 🏃🏽‍♂️ 🧍🏽‍♀️ 🧍🏽 🧍🏽‍♂️ 👭🏽 🧑🏽‍🤝‍🧑🏽 👬🏽 👫🏽 🧗🏽‍♀️ 🧗🏽 🧗🏽‍♂️ 🏇🏽 🏂🏽 🏌🏽‍♀️ 🏌🏽 🏌🏽‍♂️ 🏄🏽‍♀️ 🏄🏽 🏄🏽‍♂️ 🚣🏽‍♀️ 🚣🏽 🚣🏽‍♂️ 🏊🏽‍♀️ 🏊🏽 🏊🏽‍♂️ ⛹🏽‍♀️ ⛹🏽 ⛹🏽‍♂️ 🏋🏽‍♀️ 🏋🏽 🏋🏽‍♂️ 🚴🏽‍♀️ 🚴🏽 🚴🏽‍♂️ 🚵🏽‍♀️ 🚵🏽 🚵🏽‍♂️ 🤸🏽‍♀️ 🤸🏽 🤸🏽‍♂️ 🤽🏽‍♀️ 🤽🏽 🤽🏽‍♂️ 🤾🏽‍♀️ 🤾🏽 🤾🏽‍♂️ 🤹🏽‍♀️ 🤹🏽 🤹🏽‍♂️ 🧘🏽‍♀️ 🧘🏽 🧘🏽‍♂️ 🛀🏽 🛌'.split(' '),
72880
+ 'Dark Brown Emojis': '👋🏾 🤚🏾 🖐🏾 ✋🏾 🖖🏾 👌🏾 🤌🏾 🤏🏾 ✌🏾 🤞🏾 🤟🏾 🤘🏾 🤙🏾 👈🏾 👉🏾 👆🏾 🖕🏾 👇🏾 ☝🏾 👍🏾 👎🏾 ✊🏾 👊🏾 🤛🏾 🤜🏾 👏🏾 🙌🏾 👐🏾 🤲🏾 🙏🏾 ✍🏾 💅🏾 🤳🏾 💪🏾 🦵🏾 🦶🏾 👂🏾 🦻🏾 👃🏾 👶🏾 👧🏾 🧒🏾 👦🏾 👩🏾 🧑🏾 👨🏾 👩🏾‍🦱 🧑🏾‍🦱 👨🏾‍🦱 👩🏾‍🦰 🧑🏾‍🦰 👨🏾‍🦰 👱🏾‍♀️ 👱🏾 👱🏾‍♂️ 👩🏾‍🦳 🧑🏾‍🦳 👨🏾‍🦳 👩🏾‍🦲 🧑🏾‍🦲 👨🏾‍🦲 🧔🏾 👵🏾 🧓🏾 👴🏾 👲🏾 👳🏾‍♀️ 👳🏾 👳🏾‍♂️ 🧕🏾 👮🏾‍♀️ 👮🏾 👮🏾‍♂️ 👷🏾‍♀️ 👷🏾 👷🏾‍♂️ 💂🏾‍♀️ 💂🏾 💂🏾‍♂️ 🕵🏾‍♀️ 🕵🏾 🕵🏾‍♂️ 👩🏾‍⚕️ 🧑🏾‍⚕️ 👨🏾‍⚕️ 👩🏾‍🌾 🧑🏾‍🌾 👨🏾‍🌾 👩🏾‍🍳 🧑🏾‍🍳 👨🏾‍🍳 👩🏾‍🎓 🧑🏾‍🎓 👨🏾‍🎓 👩🏾‍🎤 🧑🏾‍🎤 👨🏾‍🎤 👩🏾‍🏫 🧑🏾‍🏫 👨🏾‍🏫 👩🏾‍🏭 🧑🏾‍🏭 👨🏾‍🏭 👩🏾‍💻 🧑🏾‍💻 👨🏾‍💻 👩🏾‍💼 🧑🏾‍💼 👨🏾‍💼 👩🏾‍🔧 🧑🏾‍🔧 👨🏾‍🔧 👩🏾‍🔬 🧑🏾‍🔬 👨🏾‍🔬 👩🏾‍🎨 🧑🏾‍🎨 👨🏾‍🎨 👩🏾‍🚒 🧑🏾‍🚒 👨🏾‍🚒 👩🏾‍✈️ 🧑🏾‍✈️ 👨🏾‍✈️ 👩🏾‍🚀 🧑🏾‍🚀 👨🏾‍🚀 👩🏾‍⚖️ 🧑🏾‍⚖️ 👨🏾‍⚖️ 👰🏾‍♀️ 👰🏾 👰🏾‍♂️ 🤵🏾‍♀️ 🤵🏾 🤵🏾‍♂️ 👸🏾 🤴🏾 🥷🏾 🦸🏾‍♀️ 🦸🏾 🦸🏾‍♂️ 🦹🏾‍♀️ 🦹🏾 🦹🏾‍♂️ 🤶🏾 🧑🏾‍🎄 🎅🏾 🧙🏾‍♀️ 🧙🏾 🧙🏾‍♂️ 🧝🏾‍♀️ 🧝🏾 🧝🏾‍♂️ 🧛🏾‍♀️ 🧛🏾 🧛🏾‍♂️ 🧜🏾‍♀️ 🧜🏾 🧜🏾‍♂️ 🧚🏾‍♀️ 🧚🏾 🧚🏾‍♂️ 👼🏾 🤰🏾 🤱🏾 👩🏾‍🍼 🧑🏾‍🍼 👨🏾‍🍼 🙇🏾‍♀️ 🙇🏾 🙇🏾‍♂️ 💁🏾‍♀️ 💁🏾 💁🏾‍♂️ 🙅🏾‍♀️ 🙅🏾 🙅🏾‍♂️ 🙆🏾‍♀️ 🙆🏾 🙆🏾‍♂️ 🙋🏾‍♀️ 🙋🏾 🙋🏾‍♂️ 🧏🏾‍♀️ 🧏🏾 🧏🏾‍♂️ 🤦🏾‍♀️ 🤦🏾 🤦🏾‍♂️ 🤷🏾‍♀️ 🤷🏾 🤷🏾‍♂️ 🙎🏾‍♀️ 🙎🏾 🙎🏾‍♂️ 🙍🏾‍♀️ 🙍🏾 🙍🏾‍♂️ 💇🏾‍♀️ 💇🏾 💇🏾‍♂️ 💆🏾‍♀️ 💆🏾 💆🏾‍♂️ 🧖🏾‍♀️ 🧖🏾 🧖🏾‍♂️ 💃🏾 🕺🏾 🕴🏿 👩🏾‍🦽 🧑🏾‍🦽 👨🏾‍🦽 👩🏾‍🦼 🧑🏾‍🦼 👨🏾‍🦼 🚶🏾‍♀️ 🚶🏾 🚶🏾‍♂️ 👩🏾‍🦯 🧑🏾‍🦯 👨🏾‍🦯 🧎🏾‍♀️ 🧎🏾 🧎🏾‍♂️ 🏃🏾‍♀️ 🏃🏾 🏃🏾‍♂️ 🧍🏾‍♀️ 🧍🏾 🧍🏾‍♂️ 👭🏾 🧑🏾‍🤝‍🧑🏾 👬🏾 👫🏾 🧗🏾‍♀️ 🧗🏾 🧗🏾‍♂️ 🏇🏾 🏂🏾 🏌🏾‍♀️ 🏌🏾 🏌🏾‍♂️ 🏄🏾‍♀️ 🏄🏾 🏄🏾‍♂️ 🚣🏾‍♀️ 🚣🏾 🚣🏾‍♂️ 🏊🏾‍♀️ 🏊🏾 🏊🏾‍♂️ ⛹🏾‍♀️ ⛹🏾 ⛹🏾‍♂️ 🏋🏾‍♀️ 🏋🏾 🏋🏾‍♂️ 🚴🏾‍♀️ 🚴🏾 🚴🏾‍♂️ 🚵🏾‍♀️ 🚵🏾 🚵🏾‍♂️ 🤸🏾‍♀️ 🤸🏾 🤸🏾‍♂️ 🤽🏾‍♀️ 🤽🏾 🤽🏾‍♂️ 🤾🏾‍♀️ 🤾🏾 🤾🏾‍♂️ 🤹🏾‍♀️ 🤹🏾 🤹🏾‍♂️ 🧘🏾‍♀️ 🧘🏾 🧘🏾‍♂️ 🛀🏾 🛌'.split(' '),
72881
+ 'Black Emojis': '👋🏿 🤚🏿 🖐🏿 ✋🏿 🖖🏿 👌🏿 🤌🏿 🤏🏿 ✌🏿 🤞🏿 🤟🏿 🤘🏿 🤙🏿 👈🏿 👉🏿 👆🏿 🖕🏿 👇🏿 ☝🏿 👍🏿 👎🏿 ✊🏿 👊🏿 🤛🏿 🤜🏿 👏🏿 🙌🏿 👐🏿 🤲🏿 🙏🏿 ✍🏿 💅🏿 🤳🏿 💪🏿 🦵🏿 🦶🏿 👂🏿 🦻🏿 👃🏿 👶🏿 👧🏿 🧒🏿 👦🏿 👩🏿 🧑🏿 👨🏿 👩🏿‍🦱 🧑🏿‍🦱 👨🏿‍🦱 👩🏿‍🦰 🧑🏿‍🦰 👨🏿‍🦰 👱🏿‍♀️ 👱🏿 👱🏿‍♂️ 👩🏿‍🦳 🧑🏿‍🦳 👨🏿‍🦳 👩🏿‍🦲 🧑🏿‍🦲 👨🏿‍🦲 🧔🏿 👵🏿 🧓🏿 👴🏿 👲🏿 👳🏿‍♀️ 👳🏿 👳🏿‍♂️ 🧕🏿 👮🏿‍♀️ 👮🏿 👮🏿‍♂️ 👷🏿‍♀️ 👷🏿 👷🏿‍♂️ 💂🏿‍♀️ 💂🏿 💂🏿‍♂️ 🕵🏿‍♀️ 🕵🏿 🕵🏿‍♂️ 👩🏿‍⚕️ 🧑🏿‍⚕️ 👨🏿‍⚕️ 👩🏿‍🌾 🧑🏿‍🌾 👨🏿‍🌾 👩🏿‍🍳 🧑🏿‍🍳 👨🏿‍🍳 👩🏿‍🎓 🧑🏿‍🎓 👨🏿‍🎓 👩🏿‍🎤 🧑🏿‍🎤 👨🏿‍🎤 👩🏿‍🏫 🧑🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 🧑🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 🧑🏿‍💻 👨🏿‍💻 👩🏿‍💼 🧑🏿‍💼 👨🏿‍💼 👩🏿‍🔧 🧑🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 🧑🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 🧑🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 🧑🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 🧑🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 🧑🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 🧑🏿‍⚖️ 👨🏿‍⚖️ 👰🏿‍♀️ 👰🏿 👰🏿‍♂️ 🤵🏿‍♀️ 🤵🏿 🤵🏿‍♂️ 👸🏿 🤴🏿 🥷🏿 🦸🏿‍♀️ 🦸🏿 🦸🏿‍♂️ 🦹🏿‍♀️ 🦹🏿 🦹🏿‍♂️ 🤶🏿 🧑🏿‍🎄 🎅🏿 🧙🏿‍♀️ 🧙🏿 🧙🏿‍♂️ 🧝🏿‍♀️ 🧝🏿 🧝🏿‍♂️ 🧛🏿‍♀️ 🧛🏿 🧛🏿‍♂️ 🧜🏿‍♀️ 🧜🏿 🧜🏿‍♂️ 🧚🏿‍♀️ 🧚🏿 🧚🏿‍♂️ 👼🏿 🤰🏿 🤱🏿 👩🏿‍🍼 🧑🏿‍🍼 👨🏿‍🍼 🙇🏿‍♀️ 🙇🏿 🙇🏿‍♂️ 💁🏿‍♀️ 💁🏿 💁🏿‍♂️ 🙅🏿‍♀️ 🙅🏿 🙅🏿‍♂️ 🙆🏿‍♀️ 🙆🏿 🙆🏿‍♂️ 🙋🏿‍♀️ 🙋🏿 🙋🏿‍♂️ 🧏🏿‍♀️ 🧏🏿 🧏🏿‍♂️ 🤦🏿‍♀️ 🤦🏿 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿 🤷🏿‍♂️ 🙎🏿‍♀️ 🙎🏿 🙎🏿‍♂️ 🙍🏿‍♀️ 🙍🏿 🙍🏿‍♂️ 💇🏿‍♀️ 💇🏿 💇🏿‍♂️ 💆🏿‍♀️ 💆🏿 💆🏿‍♂️ 🧖🏿‍♀️ 🧖🏿 🧖🏿‍♂️ 💃🏿 🕺🏿 🕴🏿 👩🏿‍🦽 🧑🏿‍🦽 👨🏿‍🦽 👩🏿‍🦼 🧑🏿‍🦼 👨🏿‍🦼 🚶🏿‍♀️ 🚶🏿 🚶🏿‍♂️ 👩🏿‍🦯 🧑🏿‍🦯 👨🏿‍🦯 🧎🏿‍♀️ 🧎🏿 🧎🏿‍♂️ 🏃🏿‍♀️ 🏃🏿 🏃🏿‍♂️ 🧍🏿‍♀️ 🧍🏿 🧍🏿‍♂️ 👭🏿 🧑🏿‍🤝‍🧑🏿 👬🏿 👫🏿 🧗🏿‍♀️ 🧗🏿 🧗🏿‍♂️ 🏇🏿 🏂🏿 🏌🏿‍♀️ 🏌🏿 🏌🏿‍♂️ 🏄🏿‍♀️ 🏄🏿 🏄🏿‍♂️ 🚣🏿‍♀️ 🚣🏿 🚣🏿‍♂️ 🏊🏿‍♀️ 🏊🏿 🏊🏿‍♂️ ⛹🏿‍♀️ ⛹🏿 ⛹🏿‍♂️ 🏋🏿‍♀️ 🏋🏿 🏋🏿‍♂️ 🚴🏿‍♀️ 🚴🏿 🚴🏿‍♂️ 🚵🏿‍♀️ 🚵🏿 🚵🏿‍♂️ 🤸🏿‍♀️ 🤸🏿 🤸🏿‍♂️ 🤽🏿‍♀️ 🤽🏿 🤽🏿‍♂️ 🤾🏿‍♀️ 🤾🏿 🤾🏿‍♂️ 🤹🏿‍♀️ 🤹🏿 🤹🏿‍♂️ 🧘🏿‍♀️ 🧘🏿 🧘🏿‍♂️ 🛀🏿 🛌🏿'.split(' '),
72882
+ 'Animals & Nature': '🐶 🐱 🐭 🐹 🐰 🦊 🐻 🐼 🐻‍❄️ 🐨 🐯 🦁 🐮 🐷 🐽 🐸 🐵 🙈 🙉 🙊 🐒 🐔 🐧 🐦 🐤 🐣 🐥 🦆 🦅 🦉 🦇 🐺 🐗 🐴 🦄 🐝 🪱 🐛 🦋 🐌 🐞 🐜 🪰 🪲 🪳 🦟 🦗 🕷 🕸 🦂 🐢 🐍 🦎 🦖 🦕 🐙 🦑 🦐 🦞 🦀 🐡 🐠 🐟 🐬 🐳 🐋 🦈 🐊 🐅 🐆 🦓 🦍 🦧 🦣 🐘 🦛 🦏 🐪 🐫 🦒 🦘 🦬 🐃 🐂 🐄 🐎 🐖 🐏 🐑 🦙 🐐 🦌 🐕 🐩 🦮 🐕‍🦺 🐈 🐈‍⬛ 🪶 🐓 🦃 🦤 🦚 🦜 🦢 🦩 🕊 🐇 🦝 🦨 🦡 🦫 🦦 🦥 🐁 🐀 🐿 🦔 🐾 🐉 🐲 🌵 🎄 🌲 🌳 🌴 🪵 🌱 🌿 ☘️ 🍀 🎍 🪴 🎋 🍃 🍂 🍁 🍄 🐚 🪨 🌾 💐 🌷 🌹 🥀 🌺 🌸 🌼 🌻 🌞 🌝 🌛 🌜 🌚 🌕 🌖 🌗 🌘 🌑 🌒 🌓 🌔 🌙 🌎 🌍 🌏 🪐 💫 ⭐️ 🌟 ✨ ⚡️ ☄️ 💥 🔥 🌪 🌈 ☀️ 🌤 ⛅️ 🌥 ☁️ 🌦 🌧 ⛈ 🌩 🌨 ❄️ ☃️ ⛄️ 🌬 💨 💧 💦 ☔️ ☂️ 🌊 🌫'.split(' '),
72883
+ 'Food & Drink': '🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🫐 🍈 🍒 🍑 🥭 🍍 🥥 🥝 🍅 🍆 🥑 🥦 🥬 🥒 🌶 🫑 🌽 🥕 🫒 🧄 🧅 🥔 🍠 🥐 🥯 🍞 🥖 🥨 🧀 🥚 🍳 🧈 🥞 🧇 🥓 🥩 🍗 🍖 🦴 🌭 🍔 🍟 🍕 🫓 🥪 🥙 🧆 🌮 🌯 🫔 🥗 🥘 🫕 🥫 🍝 🍜 🍲 🍛 🍣 🍱 🥟 🦪 🍤 🍙 🍚 🍘 🍥 🥠 🥮 🍢 🍡 🍧 🍨 🍦 🥧 🧁 🍰 🎂 🍮 🍭 🍬 🍫 🍿 🍩 🍪 🌰 🥜 🍯 🥛 🍼 🫖 ☕️ 🍵 🧃 🥤 🧋 🍶 🍺 🍻 🥂 🍷 🥃 🍸 🍹 🧉 🍾 🧊 🥄 🍴 🍽 🥣 🥡 🥢 🧂'.split(' '),
72884
+ 'Activity and Sports': '⚽️ 🏀 🏈 ⚾️ 🥎 🎾 🏐 🏉 🥏 🎱 🪀 🏓 🏸 🏒 🏑 🥍 🏏 🪃 🥅 ⛳️ 🪁 🏹 🎣 🤿 🥊 🥋 🎽 🛹 🛼 🛷 ⛸ 🥌 🎿 ⛷ 🏂 🪂 🏋️‍♀️ 🏋️ 🏋️‍♂️ 🤼‍♀️ 🤼 🤼‍♂️ 🤸‍♀️ 🤸 🤸‍♂️ ⛹️‍♀️ ⛹️ ⛹️‍♂️ 🤺 🤾‍♀️ 🤾 🤾‍♂️ 🏌️‍♀️ 🏌️ 🏌️‍♂️ 🏇 🧘‍♀️ 🧘 🧘‍♂️ 🏄‍♀️ 🏄 🏄‍♂️ 🏊‍♀️ 🏊 🏊‍♂️ 🤽‍♀️ 🤽 🤽‍♂️ 🚣‍♀️ 🚣 🚣‍♂️ 🧗‍♀️ 🧗 🧗‍♂️ 🚵‍♀️ 🚵 🚵‍♂️ 🚴‍♀️ 🚴 🚴‍♂️ 🏆 🥇 🥈 🥉 🏅 🎖 🏵 🎗 🎫 🎟 🎪 🤹 🤹‍♂️ 🤹‍♀️ 🎭 🩰 🎨 🎬 🎤 🎧 🎼 🎹 🥁 🪘 🎷 🎺 🪗 🎸 🪕 🎻 🎲 ♟ 🎯 🎳 🎮 🎰 🧩'.split(' '),
72885
+ 'Travel & Places': '🚗 🚕 🚙 🚌 🚎 🏎 🚓 🚑 🚒 🚐 🛻 🚚 🚛 🚜 🦯 🦽 🦼 🛴 🚲 🛵 🏍 🛺 🚨 🚔 🚍 🚘 🚖 🚡 🚠 🚟 🚃 🚋 🚞 🚝 🚄 🚅 🚈 🚂 🚆 🚇 🚊 🚉 ✈️ 🛫 🛬 🛩 💺 🛰 🚀 🛸 🚁 🛶 ⛵️ 🚤 🛥 🛳 ⛴ 🚢 ⚓️ 🪝 ⛽️ 🚧 🚦 🚥 🚏 🗺 🗿 🗽 🗼 🏰 🏯 🏟 🎡 🎢 🎠 ⛲️ ⛱ 🏖 🏝 🏜 🌋 ⛰ 🏔 🗻 🏕 ⛺️ 🛖 🏠 🏡 🏘 🏚 🏗 🏭 🏢 🏬 🏣 🏤 🏥 🏦 🏨 🏪 🏫 🏩 💒 🏛 ⛪️ 🕌 🕍 🛕 🕋 ⛩ 🛤 🛣 🗾 🎑 🏞 🌅 🌄 🌠 🎇 🎆 🌇 🌆 🏙 🌃 🌌 🌉 🌁'.split(' '),
72886
+ Objects: '⌚️ 📱 📲 💻 ⌨️ 🖥 🖨 🖱 🖲 🕹 🗜 💽 💾 💿 📀 📼 📷 📸 📹 🎥 📽 🎞 📞 ☎️ 📟 📠 📺 📻 🎙 🎚 🎛 🧭 ⏱ ⏲ ⏰ 🕰 ⌛️ ⏳ 📡 🔋 🔌 💡 🔦 🕯 🪔 🧯 🛢 💸 💵 💴 💶 💷 🪙 💰 💳 💎 ⚖️ 🪜 🧰 🪛 🔧 🔨 ⚒ 🛠 ⛏ 🪚 🔩 ⚙️ 🪤 🧱 ⛓ 🧲 🔫 💣 🧨 🪓 🔪 🗡 ⚔️ 🛡 🚬 ⚰️ 🪦 ⚱️ 🏺 🔮 📿 🧿 💈 ⚗️ 🔭 🔬 🕳 🩹 🩺 💊 💉 🩸 🧬 🦠 🧫 🧪 🌡 🧹 🪠 🧺 🧻 🚽 🚰 🚿 🛁 🛀 🧼 🪥 🪒 🧽 🪣 🧴 🛎 🔑 🗝 🚪 🪑 🛋 🛏 🛌 🧸 🪆 🖼 🪞 🪟 🛍 🛒 🎁 🎈 🎏 🎀 🪄 🪅 🎊 🎉 🎎 🏮 🎐 🧧 ✉️ 📩 📨 📧 💌 📥 📤 📦 🏷 🪧 📪 📫 📬 📭 📮 📯 📜 📃 📄 📑 🧾 📊 📈 📉 🗒 🗓 📆 📅 🗑 📇 🗃 🗳 🗄 📋 📁 📂 🗂 🗞 📰 📓 📔 📒 📕 📗 📘 📙 📚 📖 🔖 🧷 🔗 📎 🖇 📐 📏 🧮 📌 📍 ✂️ 🖊 🖋 ✒️ 🖌 🖍 📝 ✏️ 🔍 🔎 🔏 🔐 🔒 🔓'.split(' '),
72887
+ Symbols: '❤️ 🧡 💛 💚 💙 💜 🖤 🤍 🤎 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🛗 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 ⚧ 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ⏏️ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ ♾ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 🔜 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ 🟥 🟧 🟨 🟩 🟦 🟪 ⬛️ ⬜️ 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 🕟 🕠 🕡 🕢 🕣 🕤 🕥 🕦 🕧'.split(' '),
72888
+ 'Non-Emoji Symbols': '✢ ✣ ✤ ✥ ✦ ✧ ★ ☆ ✯ ✡︎ ✩ ✪ ✫ ✬ ✭ ✮ ✶ ✷ ✵ ✸ ✹ → ⇒ ⟹ ⇨ ⇾ ➾ ⇢ ☛ ☞ ➔ ➜ ➙ ➛ ➝ ➞ ♠︎ ♣︎ ♥︎ ♦︎ ♤ ♧ ♡ ♢ ♚ ♛ ♜ ♝ ♞ ♟ ♔ ♕ ♖ ♗ ♘ ♙ ⚀ ⚁ ⚂ ⚃ ⚄ ⚅ 🂠 ⚈ ⚉ ⚆ ⚇ 𓀀 𓀁 𓀂 𓀃 𓀄 𓀅 𓀆 𓀇 𓀈 𓀉 𓀊 𓀋 𓀌 𓀍 𓀎 𓀏 𓀐 𓀑 𓀒 𓀓 𓀔 𓀕 𓀖 𓀗 𓀘 𓀙 𓀚 𓀛 𓀜 𓀝'.split(' '),
72889
+ Flags: '🏳️ 🏴 🏁 🚩 🏳️‍🌈 🏳️‍⚧️ 🏴‍☠️'.split(' '),
72890
+ Countries: '🇦🇫 🇦🇽 🇦🇱 🇩🇿 🇦🇸 🇦🇩 🇦🇴 🇦🇮 🇦🇶 🇦🇬 🇦🇷 🇦🇲 🇦🇼 🇦🇺 🇦🇹 🇦🇿 🇧🇸 🇧🇭 🇧🇩 🇧🇧 🇧🇾 🇧🇪 🇧🇿 🇧🇯 🇧🇲 🇧🇹 🇧🇴 🇧🇦 🇧🇼 🇧🇷 🇮🇴 🇻🇬 🇧🇳 🇧🇬 🇧🇫 🇧🇮 🇰🇭 🇨🇲 🇨🇦 🇮🇨 🇨🇻 🇧🇶 🇰🇾 🇨🇫 🇹🇩 🇨🇱 🇨🇳 🇨🇽 🇨🇨 🇨🇴 🇰🇲 🇨🇬 🇨🇩 🇨🇰 🇨🇷 🇨🇮 🇭🇷 🇨🇺 🇨🇼 🇨🇾 🇨🇿 🇩🇰 🇩🇯 🇩🇲 🇩🇴 🇪🇨 🇪🇬 🇸🇻 🇬🇶 🇪🇷 🇪🇪 🇪🇹 🇪🇺 🇫🇰 🇫🇴 🇫🇯 🇫🇮 🇫🇷 🇬🇫 🇵🇫 🇹🇫 🇬🇦 🇬🇲 🇬🇪 🇩🇪 🇬🇭 🇬🇮 🇬🇷 🇬🇱 🇬🇩 🇬🇵 🇬🇺 🇬🇹 🇬🇬 🇬🇳 🇬🇼 🇬🇾 🇭🇹 🇭🇳 🇭🇰 🇭🇺 🇮🇸 🇮🇳 🇮🇩 🇮🇷 🇮🇶 🇮🇪 🇮🇲 🇮🇱 🇮🇹 🇯🇲 🇯🇵 🎌 🇯🇪 🇯🇴 🇰🇿 🇰🇪 🇰🇮 🇽🇰 🇰🇼 🇰🇬 🇱🇦 🇱🇻 🇱🇧 🇱🇸 🇱🇷 🇱🇾 🇱🇮 🇱🇹 🇱🇺 🇲🇴 🇲🇰 🇲🇬 🇲🇼 🇲🇾 🇲🇻 🇲🇱 🇲🇹 🇲🇭 🇲🇶 🇲🇷 🇲🇺 🇾🇹 🇲🇽 🇫🇲 🇲🇩 🇲🇨 🇲🇳 🇲🇪 🇲🇸 🇲🇦 🇲🇿 🇲🇲 🇳🇦 🇳🇷 🇳🇵 🇳🇱 🇳🇨 🇳🇿 🇳🇮 🇳🇪 🇳🇬 🇳🇺 🇳🇫 🇰🇵 🇲🇵 🇳🇴 🇴🇲 🇵🇰 🇵🇼 🇵🇸 🇵🇦 🇵🇬 🇵🇾 🇵🇪 🇵🇭 🇵🇳 🇵🇱 🇵🇹 🇵🇷 🇶🇦 🇷🇪 🇷🇴 🇷🇺 🇷🇼 🇼🇸 🇸🇲 🇸🇦 🇸🇳 🇷🇸 🇸🇨 🇸🇱 🇸🇬 🇸🇽 🇸🇰 🇸🇮 🇬🇸 🇸🇧 🇸🇴 🇿🇦 🇰🇷 🇸🇸 🇪🇸 🇱🇰 🇧🇱 🇸🇭 🇰🇳 🇱🇨 🇵🇲 🇻🇨 🇸🇩 🇸🇷 🇸🇿 🇸🇪 🇨🇭 🇸🇾 🇹🇼 🇹🇯 🇹🇿 🇹🇭 🇹🇱 🇹🇬 🇹🇰 🇹🇴 🇹🇹 🇹🇳 🇹🇷 🇹🇲 🇹🇨 🇹🇻 🇻🇮 🇺🇬 🇺🇦 🇦🇪 🇬🇧 🇺🇳 🇺🇸 🇺🇾 🇺🇿 🇻🇺 🇻🇦 🇻🇪 🇻🇳 🇼🇫 🇪🇭 🇾🇪 🇿🇲 🇿🇼'.split(' '),
72891
+ 'New Emojis 2020': '🥲 🥸 🤌 🤌🏻 🤌🏼 🤌🏽 🤌🏾 🤌🏿 🫀 🫁 🥷 🤵‍♀️ 🤵🏻‍♀️ 🤵🏼‍♀️ 🤵🏽‍♀️ 🤵🏾‍♀️ 🤵🏿‍♀️ 🤵‍♂️ 🤵🏻‍♂️ 🤵🏼‍♂️ 🤵🏽‍♂️ 🤵🏾‍♂️ 🤵🏿‍♂️ 👰‍♀️ 👰🏻‍♀️ 👰🏼‍♀️ 👰🏽‍♀️ 👰🏾‍♀️ 👰🏿‍♀️ 👰‍♂️ 👰🏻‍♂️ 👰🏼‍♂️ 👰🏽‍♂️ 👰🏾‍♂️ 👰🏿‍♂️ 👩‍🍼 👩🏻‍🍼 👩🏼‍🍼 👩🏽‍🍼 👩🏾‍🍼 👩🏿‍🍼 🧑‍🍼 🧑🏻‍🍼 🧑🏼‍🍼 🧑🏽‍🍼 🧑🏾‍🍼 🧑🏿‍🍼 👨‍🍼 👨🏻‍🍼 👨🏼‍🍼 👨🏽‍🍼 👨🏾‍🍼 👨🏿‍🍼 🧑‍🎄 🧑🏻‍🎄 🧑🏼‍🎄 🧑🏽‍🎄 🧑🏾‍🎄 🧑🏿‍🎄 🫂 🐈‍⬛ 🦬 🦣 🦫 🐻‍❄️ 🦤 🪶 🦭 🪲 🪳 🪰 🪱 🪴 🫐 🫒 🫑 🫓 🫔 🫕 🫖 🧋 🪨 🪵 🛖 🛻 🛼 🪄 🪅 🪆 🪡 🪢 🩴 🪖 🪗 🪘 🪙 🪃 🪚 🪛 🪝 🪜 🛗 🪞 🪟 🪠 🪤 🪣 🪥 🪦 🪧 🏳️‍⚧️'.split(' '),
72892
+ 'New Emoji 2021': '😮‍💨 😵‍💫 😶‍🌫️ ❤️‍🔥 ❤️‍🩹 🧔‍♀️ 🧔🏻‍♀️ 🧔🏼‍♀️ 🧔🏽‍♀️ 🧔🏾‍♀️ 🧔🏿‍♀️ 🧔‍♂️ 🧔🏻‍♂️ 🧔🏼‍♂️ 🧔🏽‍♂️ 🧔🏾‍♂️ 🧔🏿‍♂️ 💑🏻 💑🏼 💑🏽 💑🏾 💑🏿 💏🏻 💏🏼 💏🏽 💏🏾 💏🏿 👨🏻‍❤️‍👨🏻 👨🏻‍❤️‍👨🏼 👨🏻‍❤️‍👨🏽 👨🏻‍❤️‍👨🏾 👨🏻‍❤️‍👨🏿 👨🏼‍❤️‍👨🏻 👨🏼‍❤️‍👨🏼 👨🏼‍❤️‍👨🏽 👨🏼‍❤️‍👨🏾 👨🏼‍❤️‍👨🏿 👨🏽‍❤️‍👨🏻 👨🏽‍❤️‍👨🏼 👨🏽‍❤️‍👨🏽 👨🏽‍❤️‍👨🏾 👨🏽‍❤️‍👨🏿 👨🏾‍❤️‍👨🏻 👨🏾‍❤️‍👨🏼 👨🏾‍❤️‍👨🏽 👨🏾‍❤️‍👨🏾 👨🏾‍❤️‍👨🏿 👨🏿‍❤️‍👨🏻 👨🏿‍❤️‍👨🏼 👨🏿‍❤️‍👨🏽 👨🏿‍❤️‍👨🏾 👨🏿‍❤️‍👨🏿 👩🏻‍❤️‍👨🏻 👩🏻‍❤️‍👨🏼 👩🏻‍❤️‍👨🏽 👩🏻‍❤️‍👨🏾 👩🏻‍❤️‍👨🏿 👩🏻‍❤️‍👩🏻 👩🏻‍❤️‍👩🏼 👩🏻‍❤️‍👩🏽 👩🏻‍❤️‍👩🏾 👩🏻‍❤️‍👩🏿 👩🏼‍❤️‍👨🏻 👩🏼‍❤️‍👨🏼 👩🏼‍❤️‍👨🏽 👩🏼‍❤️‍👨🏾 👩🏼‍❤️‍👨🏿 👩🏼‍❤️‍👩🏻 👩🏼‍❤️‍👩🏼 👩🏼‍❤️‍👩🏽 👩🏼‍❤️‍👩🏾 👩🏼‍❤️‍👩🏿 👩🏽‍❤️‍👨🏻 👩🏽‍❤️‍👨🏼 👩🏽‍❤️‍👨🏽 👩🏽‍❤️‍👨🏾 👩🏽‍❤️‍👨🏿 👩🏽‍❤️‍👩🏻 👩🏽‍❤️‍👩🏼 👩🏽‍❤️‍👩🏽 👩🏽‍❤️‍👩🏾 👩🏽‍❤️‍👩🏿 👩🏾‍❤️‍👨🏻 👩🏾‍❤️‍👨🏼 👩🏾‍❤️‍👨🏽 👩🏾‍❤️‍👨🏾 👩🏾‍❤️‍👨🏿 👩🏾‍❤️‍👩🏻 👩🏾‍❤️‍👩🏼 👩🏾‍❤️‍👩🏽 👩🏾‍❤️‍👩🏾 👩🏾‍❤️‍👩🏿 👩🏿‍❤️‍👨🏻 👩🏿‍❤️‍👨🏼 👩🏿‍❤️‍👨🏽 👩🏿‍❤️‍👨🏾 👩🏿‍❤️‍👨🏿 👩🏿‍❤️‍👩🏻 👩🏿‍❤️‍👩🏼 👩🏿‍❤️‍👩🏽 👩🏿‍❤️‍👩🏾 👩🏿‍❤️‍👩🏿 🧑🏻‍❤️‍🧑🏼 🧑🏻‍❤️‍🧑🏽 🧑🏻‍❤️‍🧑🏾 🧑🏻‍❤️‍🧑🏿 🧑🏼‍❤️‍🧑🏻 🧑🏼‍❤️‍🧑🏽 🧑🏼‍❤️‍🧑🏾 🧑🏼‍❤️‍🧑🏿 🧑🏽‍❤️‍🧑🏻 🧑🏽‍❤️‍🧑🏼 🧑🏽‍❤️‍🧑🏾 🧑🏽‍❤️‍🧑🏿 🧑🏾‍❤️‍🧑🏻 🧑🏾‍❤️‍🧑🏼 🧑🏾‍❤️‍🧑🏽 🧑🏾‍❤️‍🧑🏿 🧑🏿‍❤️‍🧑🏻 🧑🏿‍❤️‍🧑🏼 🧑🏿‍❤️‍🧑🏽 🧑🏿‍❤️‍🧑🏾 👨🏻‍❤️‍💋‍👨🏻 👨🏻‍❤️‍💋‍👨🏼 👨🏻‍❤️‍💋‍👨🏽 👨🏻‍❤️‍💋‍👨🏾 👨🏻‍❤️‍💋‍👨🏿 👨🏼‍❤️‍💋‍👨🏻 👨🏼‍❤️‍💋‍👨🏼 👨🏼‍❤️‍💋‍👨🏽 👨🏼‍❤️‍💋‍👨🏾 👨🏼‍❤️‍💋‍👨🏿 👨🏽‍❤️‍💋‍👨🏻 👨🏽‍❤️‍💋‍👨🏼 👨🏽‍❤️‍💋‍👨🏽 👨🏽‍❤️‍💋‍👨🏾 👨🏽‍❤️‍💋‍👨🏿 👨🏾‍❤️‍💋‍👨🏻 👨🏾‍❤️‍💋‍👨🏼 👨🏾‍❤️‍💋‍👨🏽 👨🏾‍❤️‍💋‍👨🏾 👨🏾‍❤️‍💋‍👨🏿 👨🏿‍❤️‍💋‍👨🏻 👨🏿‍❤️‍💋‍👨🏼 👨🏿‍❤️‍💋‍👨🏽 👨🏿‍❤️‍💋‍👨🏾 👨🏿‍❤️‍💋‍👨🏿 👩🏻‍❤️‍💋‍👨🏻 👩🏻‍❤️‍💋‍👨🏼 👩🏻‍❤️‍💋‍👨🏽 👩🏻‍❤️‍💋‍👨🏾 👩🏻‍❤️‍💋‍👨🏿 👩🏻‍❤️‍💋‍👩🏻 👩🏻‍❤️‍💋‍👩🏼 👩🏻‍❤️‍💋‍👩🏽 👩🏻‍❤️‍💋‍👩🏾 👩🏻‍❤️‍💋‍👩🏿 👩🏼‍❤️‍💋‍👨🏻 👩🏼‍❤️‍💋‍👨🏼 👩🏼‍❤️‍💋‍👨🏽 👩🏼‍❤️‍💋‍👨🏾 👩🏼‍❤️‍💋‍👨🏿 👩🏼‍❤️‍💋‍👩🏻 👩🏼‍❤️‍💋‍👩🏼 👩🏼‍❤️‍💋‍👩🏽 👩🏼‍❤️‍💋‍👩🏾 👩🏼‍❤️‍💋‍👩🏿 👩🏽‍❤️‍💋‍👨🏻 👩🏽‍❤️‍💋‍👨🏼 👩🏽‍❤️‍💋‍👨🏽 👩🏽‍❤️‍💋‍👨🏾 👩🏽‍❤️‍💋‍👨🏿 👩🏽‍❤️‍💋‍👩🏻 👩🏽‍❤️‍💋‍👩🏼 👩🏽‍❤️‍💋‍👩🏽 👩🏽‍❤️‍💋‍👩🏾 👩🏽‍❤️‍💋‍👩🏿 👩🏾‍❤️‍💋‍👨🏻 👩🏾‍❤️‍💋‍👨🏼 👩🏾‍❤️‍💋‍👨🏽 👩🏾‍❤️‍💋‍👨🏾 👩🏾‍❤️‍💋‍👨🏿 👩🏾‍❤️‍💋‍👩🏻 👩🏾‍❤️‍💋‍👩🏼 👩🏾‍❤️‍💋‍👩🏽 👩🏾‍❤️‍💋‍👩🏾 👩🏾‍❤️‍💋‍👩🏿 👩🏿‍❤️‍💋‍👨🏻 👩🏿‍❤️‍💋‍👨🏼 👩🏿‍❤️‍💋‍👨🏽 👩🏿‍❤️‍💋‍👨🏾 👩🏿‍❤️‍💋‍👨🏿 👩🏿‍❤️‍💋‍👩🏻 👩🏿‍❤️‍💋‍👩🏼 👩🏿‍❤️‍💋‍👩🏽 👩🏿‍❤️‍💋‍👩🏾 👩🏿‍❤️‍💋‍👩🏿 🧑🏻‍❤️‍💋‍🧑🏼 🧑🏻‍❤️‍💋‍🧑🏽 🧑🏻‍❤️‍💋‍🧑🏾 🧑🏻‍❤️‍💋‍🧑🏿 🧑🏼‍❤️‍💋‍🧑🏻 🧑🏼‍❤️‍💋‍🧑🏽 🧑🏼‍❤️‍💋‍🧑🏾 🧑🏼‍❤️‍💋‍🧑🏿 🧑🏽‍❤️‍💋‍🧑🏻 🧑🏽‍❤️‍💋‍🧑🏼 🧑🏽‍❤️‍💋‍🧑🏾 🧑🏽‍❤️‍💋‍🧑🏿 🧑🏾‍❤️‍💋‍🧑🏻 🧑🏾‍❤️‍💋‍🧑🏼 🧑🏾‍❤️‍💋‍🧑🏽 🧑🏾‍❤️‍💋‍🧑🏿 🧑🏿‍❤️‍💋‍🧑🏻 🧑🏿‍❤️‍💋‍🧑🏼 🧑🏿‍❤️‍💋‍🧑🏽 🧑🏿‍❤️‍💋‍🧑🏾'.split(' '),
72893
+ 'New Emojis (Discorered randomly)': '🖌️'.split(' '),
72894
+ 'Emojis used in WebGPT': ['▶', '❤'],
72895
+ };
72896
+ /**
72897
+ * Curated repository catalogue of emoji and legacy pictographic symbols.
72898
+ *
72899
+ * Code that needs to generate a valid Unicode emoji should apply its own validation
72900
+ * to this catalogue rather than assume every entry is an emoji.
72901
+ *
72902
+ * @private within the repository
72903
+ */
72904
+ const EMOJIS = new Set(Object.values(EMOJIS_IN_CATEGORIES).flat());
72905
+ // TODO: Mirror from Collboard or some common package
72906
+ // Note: [💞] Ignore a discrepancy between file name and entity name
72907
+
72908
+ /**
72909
+ * Matches one Unicode code point that has default emoji presentation.
72910
+ *
72911
+ * @private internal utility of generated `ptbk` emoji tags
72912
+ */
72913
+ const SINGLE_PICTOGRAM_EMOJI_PATTERN = /^\p{Emoji_Presentation}$/u;
72914
+ /**
72915
+ * Valid single-pictogram emojis that `ptbk` can use in generated tags.
72916
+ *
72917
+ * The shared emoji catalogue also contains legacy symbols for other presentation
72918
+ * purposes. Restricting generated tags to Unicode emoji-presentation pictograms
72919
+ * prevents those symbols from being emitted as prompt identifiers.
72920
+ *
72921
+ * @private internal utility of generated `ptbk` emoji tags
72922
+ */
72923
+ const VALID_SINGLE_PICTOGRAM_EMOJIS = new Set(Array.from(EMOJIS).filter(isValidSinglePictogramEmoji));
72924
+ /**
72925
+ * Determines whether one catalogue entry is a valid single-pictogram emoji for a generated tag.
72926
+ *
72927
+ * @private internal utility of generated `ptbk` emoji tags
72928
+ */
72929
+ function isValidSinglePictogramEmoji(emoji) {
72930
+ return SINGLE_PICTOGRAM_EMOJI_PATTERN.test(emoji);
72931
+ }
72932
+ // Note: [🟡] Code for generated emoji tags should never be published outside of `@promptbook/cli`
72933
+
72336
72934
  /**
72337
72935
  * Emoji prefix used for prompt tags (e.g. `[prefix + emoji]`).
72338
72936
  */
@@ -72361,9 +72959,9 @@
72361
72959
  includeGlobs,
72362
72960
  ignoreGlobs,
72363
72961
  tagPrefix,
72364
- candidateEmojis: EMOJIS_OF_SINGLE_PICTOGRAM,
72962
+ candidateEmojis: VALID_SINGLE_PICTOGRAM_EMOJIS,
72365
72963
  });
72366
- const freshEmojis = new Set(Array.from(EMOJIS_OF_SINGLE_PICTOGRAM).filter((emoji) => !usedEmojis.has(emoji)));
72964
+ const freshEmojis = new Set(Array.from(VALID_SINGLE_PICTOGRAM_EMOJIS).filter((emoji) => !usedEmojis.has(emoji)));
72367
72965
  const shuffledEmojis = $shuffleItems(...Array.from(freshEmojis));
72368
72966
  const selectedEmojis = shuffledEmojis.slice(0, count);
72369
72967
  if (selectedEmojis.length < count) {
@@ -72413,7 +73011,7 @@
72413
73011
  async function findFreshEmojiTag() {
72414
73012
  initializeFindFreshEmojiTagRun();
72415
73013
  console.info(`🤪 Find fresh emoji tag`);
72416
- const allEmojis = EMOJIS_OF_SINGLE_PICTOGRAM;
73014
+ const allEmojis = VALID_SINGLE_PICTOGRAM_EMOJIS;
72417
73015
  const { usedEmojis } = await scanEmojiTagUsage({
72418
73016
  candidateEmojis: allEmojis,
72419
73017
  tagPrefix: '',
@@ -72431,8 +73029,6 @@
72431
73029
  const tag = `[${emoji}]`;
72432
73030
  console.info(colors__default["default"].bgWhite(tag));
72433
73031
  }
72434
- //========================================/
72435
- // console.info(`[ Done 🤪 Find fresh emoji tag ]`);
72436
73032
  }
72437
73033
  // Note: [🟡] Code for CLI support script [find-fresh-emoji-tags](scripts/find-fresh-emoji-tags/find-fresh-emoji-tags.ts) should never be published outside of `@promptbook/cli`
72438
73034
 
@@ -76236,12 +76832,18 @@
76236
76832
  }
76237
76833
 
76238
76834
  /**
76239
- * Separator which introduces the harness that started a prompt another harness has taken over.
76835
+ * Separator between consecutive harness reports in the chronological history of one continued prompt.
76836
+ *
76837
+ * It is used both for writing and reading, so each `--git-changes continue` invocation can extend the
76838
+ * report that the preceding invocation left in the prompt status line.
76839
+ */
76840
+ const INTERRUPTED_CONTINUED_BY_SEPARATOR = ', interrupted, continued by ';
76841
+ /**
76842
+ * Separator written by earlier versions of `ptbk coder`, whose continuation attribution was reverse-ordered.
76240
76843
  *
76241
- * It is used both for writing and for reading the attribution, so the status line of a continued prompt
76242
- * stays parseable when yet another harness continues it later.
76844
+ * Existing interrupted prompts must remain resumable after the report format becomes chronological.
76243
76845
  */
76244
- const STARTED_BY_SEPARATOR = ', started by ';
76846
+ const LEGACY_STARTED_BY_SEPARATOR = ', started by ';
76245
76847
  /**
76246
76848
  * Matches the attribution of one status line, which reaches from `by ` up to the step summary.
76247
76849
  */
@@ -76250,33 +76852,73 @@
76250
76852
  * Formats who worked on one prompt for its status line.
76251
76853
  *
76252
76854
  * Produces `` by Claude Code `claude-opus-5` `` for a prompt implemented by a single harness and
76253
- * `` by Claude Code `claude-opus-5`, started by OpenAI Codex `gpt-5.6-luna` `` for a prompt which one
76254
- * harness started and another one continued through `--git-changes continue`.
76855
+ * `` by OpenAI Codex `gpt-5.6-luna`, interrupted, continued by Claude Code `claude-opus-5` `` for a
76856
+ * prompt which one harness started and another one continued through `--git-changes continue`.
76255
76857
  */
76256
76858
  function formatPromptRunnerAttribution(options) {
76257
- const { currentRunnerSignature, startedByRunnerSignature } = options;
76258
- if (startedByRunnerSignature === undefined || startedByRunnerSignature === currentRunnerSignature) {
76859
+ const { currentRunnerSignature, previousRunnerSignatures } = options;
76860
+ const runnerSignatures = [...(previousRunnerSignatures !== null && previousRunnerSignatures !== void 0 ? previousRunnerSignatures : [])];
76861
+ const latestRunnerSignature = runnerSignatures.at(-1);
76862
+ if (latestRunnerSignature === undefined) {
76259
76863
  return `by ${currentRunnerSignature}`;
76260
76864
  }
76261
- return `by ${currentRunnerSignature}${STARTED_BY_SEPARATOR}${startedByRunnerSignature}`;
76865
+ if (isSameRunnerSignature(latestRunnerSignature, currentRunnerSignature)) {
76866
+ runnerSignatures[runnerSignatures.length - 1] = resolveLatestRunnerSignature(latestRunnerSignature, currentRunnerSignature);
76867
+ }
76868
+ else {
76869
+ runnerSignatures.push(currentRunnerSignature);
76870
+ }
76871
+ return `by ${runnerSignatures.join(INTERRUPTED_CONTINUED_BY_SEPARATOR)}`;
76262
76872
  }
76263
76873
  /**
76264
- * Reads which harness started the work recorded on one prompt status line.
76874
+ * Reads the chronological harness report recorded on one prompt status line.
76265
76875
  *
76266
- * A line written by a single harness names that harness, and a line of a prompt which was already taken
76267
- * over keeps naming the harness which started it, so the original author survives any number of takeovers.
76876
+ * A line written by a single harness produces one signature. A line which has already been continued produces
76877
+ * every signature in the order in which it worked on the prompt, so another continuation can append to it.
76268
76878
  */
76269
- function parsePromptStartedByRunnerSignature(statusLine) {
76879
+ function parsePromptRunnerAttribution(statusLine) {
76270
76880
  var _a, _b, _c;
76271
76881
  const attribution = (_c = (_b = (_a = statusLine.match(PROMPT_RUNNER_ATTRIBUTION_PATTERN)) === null || _a === void 0 ? void 0 : _a.groups) === null || _b === void 0 ? void 0 : _b.attribution) === null || _c === void 0 ? void 0 : _c.trim();
76272
76882
  if (!attribution) {
76273
76883
  return undefined;
76274
76884
  }
76275
- const startedBySeparatorIndex = attribution.indexOf(STARTED_BY_SEPARATOR);
76276
- if (startedBySeparatorIndex === -1) {
76277
- return attribution;
76885
+ const legacyStartedBySeparatorIndex = attribution.indexOf(LEGACY_STARTED_BY_SEPARATOR);
76886
+ if (legacyStartedBySeparatorIndex !== -1) {
76887
+ const currentRunnerSignature = attribution.slice(0, legacyStartedBySeparatorIndex).trim();
76888
+ const startedByRunnerSignature = attribution
76889
+ .slice(legacyStartedBySeparatorIndex + LEGACY_STARTED_BY_SEPARATOR.length)
76890
+ .trim();
76891
+ return toPromptRunnerAttribution([startedByRunnerSignature, currentRunnerSignature]);
76892
+ }
76893
+ return toPromptRunnerAttribution(attribution.split(INTERRUPTED_CONTINUED_BY_SEPARATOR));
76894
+ }
76895
+ /**
76896
+ * Keeps the authentication label that is already known when the same harness resumes its own work before
76897
+ * reporting another label, and otherwise upgrades that last report with the newly known label.
76898
+ */
76899
+ function resolveLatestRunnerSignature(latestRunnerSignature, currentRunnerSignature) {
76900
+ if (currentRunnerSignature.startsWith(`${latestRunnerSignature} (`)) {
76901
+ return currentRunnerSignature;
76278
76902
  }
76279
- return attribution.slice(startedBySeparatorIndex + STARTED_BY_SEPARATOR.length).trim() || undefined;
76903
+ return latestRunnerSignature;
76904
+ }
76905
+ /**
76906
+ * Checks whether two report signatures describe the same harness/model/thinking configuration, allowing the
76907
+ * newer signature to add its authentication label.
76908
+ */
76909
+ function isSameRunnerSignature(latestRunnerSignature, currentRunnerSignature) {
76910
+ return (latestRunnerSignature === currentRunnerSignature ||
76911
+ latestRunnerSignature.startsWith(`${currentRunnerSignature} (`) ||
76912
+ currentRunnerSignature.startsWith(`${latestRunnerSignature} (`));
76913
+ }
76914
+ /**
76915
+ * Normalizes parsed signatures and rejects malformed or empty attribution fragments.
76916
+ */
76917
+ function toPromptRunnerAttribution(runnerSignatures) {
76918
+ const normalizedRunnerSignatures = runnerSignatures
76919
+ .map((runnerSignature) => runnerSignature.trim())
76920
+ .filter(Boolean);
76921
+ return normalizedRunnerSignatures.length === 0 ? undefined : normalizedRunnerSignatures;
76280
76922
  }
76281
76923
  // Note: [💞] Ignore a discrepancy between file name and exported helper names
76282
76924
 
@@ -76287,16 +76929,20 @@
76287
76929
  * ``by OpenAI Codex `gpt-5.6-luna` thinking `max` (ChatGPT account) - Implementation ~$0.2036 10 minutes``.
76288
76930
  */
76289
76931
  function buildPromptStatusDetails(options) {
76290
- const { steps, inProgressStepKind, runnerName, modelName, startedByRunnerSignature, attemptCount, loginMethod, thinkingLevel, } = options;
76932
+ const { steps, inProgressStepKind, runnerName, modelName, previousRunnerSignatures, attemptCount, loginMethod, thinkingLevel, } = options;
76291
76933
  const runnerSignature = formatRunnerSignature(runnerName, modelName, thinkingLevel);
76292
76934
  const attemptMetadata = formatPromptAttemptMetadata('done', attemptCount);
76293
76935
  const loginMethodLabel = formatCodexLoginMethod(loginMethod);
76294
76936
  const loginMethodSuffix = loginMethodLabel ? ` (${loginMethodLabel})` : '';
76295
76937
  const attribution = formatPromptRunnerAttribution({
76296
76938
  currentRunnerSignature: `${runnerSignature}${loginMethodSuffix}`,
76297
- startedByRunnerSignature,
76939
+ previousRunnerSignatures,
76298
76940
  });
76299
- const stepsSummary = formatCoderRunSteps(steps, inProgressStepKind);
76941
+ // Note: An interrupted prompt has step measurements from an earlier process which cannot be reconstructed
76942
+ // from its status line. Do not report just the continuing process as the whole prompt; keep only its
76943
+ // current phase while it runs, and finish with the progressive harness report.
76944
+ const completedStepsToReport = previousRunnerSignatures === undefined ? steps : [];
76945
+ const stepsSummary = formatCoderRunSteps(completedStepsToReport, inProgressStepKind);
76300
76946
  const stepsSuffix = stepsSummary === '' ? '' : ` - ${stepsSummary}`;
76301
76947
  return `${attemptMetadata}${attribution}${stepsSuffix}`;
76302
76948
  }
@@ -76350,10 +76996,10 @@
76350
76996
  * Marks a prompt section as failed and records runner details.
76351
76997
  */
76352
76998
  function markPromptFailed(options) {
76353
- const { file, section, runnerName, modelName, startedByRunnerSignature, promptExecutionStartedDate, attemptCount } = options;
76999
+ const { file, section, runnerName, modelName, previousRunnerSignatures, promptExecutionStartedDate, attemptCount } = options;
76354
77000
  const attribution = formatPromptRunnerAttribution({
76355
77001
  currentRunnerSignature: formatRunnerSignature(runnerName, modelName),
76356
- startedByRunnerSignature,
77002
+ previousRunnerSignatures,
76357
77003
  });
76358
77004
  const attemptMetadata = formatPromptAttemptMetadata('failed', attemptCount);
76359
77005
  const duration = moment__default["default"]().diff(promptExecutionStartedDate);
@@ -76420,6 +77066,30 @@
76420
77066
  await promises.writeFile(file.path, content, 'utf-8');
76421
77067
  }
76422
77068
 
77069
+ /**
77070
+ * Refreshes optional harness subscription usage in the shared coder-run UI state.
77071
+ *
77072
+ * Subscription visibility is an enhancement, not a prerequisite for running prompts. A harness which cannot expose a
77073
+ * snapshot, or whose account endpoint is temporarily unavailable, therefore leaves the existing dashboard unchanged.
77074
+ *
77075
+ * @private internal utility of coder run UI
77076
+ */
77077
+ async function refreshCoderRunUiSubscriptionUsage(options) {
77078
+ const { runner, uiState } = options;
77079
+ if (!uiState || !runner.getSubscriptionUsage) {
77080
+ return;
77081
+ }
77082
+ try {
77083
+ const subscriptionUsage = await runner.getSubscriptionUsage();
77084
+ if (subscriptionUsage) {
77085
+ uiState.setSubscriptionUsage(subscriptionUsage);
77086
+ }
77087
+ }
77088
+ catch (_a) {
77089
+ // A current subscription snapshot must never interrupt the coding queue.
77090
+ }
77091
+ }
77092
+
76423
77093
  /**
76424
77094
  * Maximum number of retry attempts performed after a prompt round throws an error.
76425
77095
  * After this many retries the round is finalized as failed.
@@ -76452,12 +77122,8 @@
76452
77122
  const codexPrompt = appendCoderContext(promptWithAgent, resolvedCoderContext);
76453
77123
  // Note: Temporary scripts and runtime logs stay in the original project so they outlive an isolated worktree
76454
77124
  const scriptPath = buildScriptPath(nextPrompt.file, nextPrompt.section);
76455
- // Note: Read before the first `[^]` rewrite of this round, which would overwrite the harness recorded so far
76456
- const startedByRunnerSignature = resolveStartedByRunnerSignature({
76457
- nextPrompt,
76458
- runnerMetadata,
76459
- thinkingLevel: options.thinkingLevel,
76460
- });
77125
+ // Note: Read before the first `[^]` rewrite of this round, which would overwrite the report recorded so far
77126
+ const previousRunnerSignatures = resolvePreviousRunnerSignatures(nextPrompt);
76461
77127
  setPromptRoundRunningState({ isRichUiEnabled, promptLabel, scriptPath, uiHandle });
76462
77128
  await waitForRequestedPause({
76463
77129
  checkpointLabel: 'preparing the current prompt execution',
@@ -76490,18 +77156,22 @@
76490
77156
  onStepStarted: (progress) => recordPromptRoundInProgress({
76491
77157
  nextPrompt,
76492
77158
  runnerMetadata,
76493
- startedByRunnerSignature,
77159
+ previousRunnerSignatures,
76494
77160
  thinkingLevel: options.thinkingLevel,
76495
77161
  attemptCount,
76496
77162
  progress,
76497
77163
  }),
76498
77164
  waitForPauseCheckpoint: waitForRequestedPause,
76499
77165
  });
77166
+ await refreshCoderRunUiSubscriptionUsage({
77167
+ runner,
77168
+ uiState: uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state,
77169
+ });
76500
77170
  await finalizeSuccessfulPromptRound({
76501
77171
  options,
76502
77172
  nextPrompt,
76503
77173
  runnerMetadata,
76504
- startedByRunnerSignature,
77174
+ previousRunnerSignatures,
76505
77175
  promptExecutionStartedDate,
76506
77176
  result,
76507
77177
  commitMessage,
@@ -76535,7 +77205,7 @@
76535
77205
  await finalizeFailedPromptRound({
76536
77206
  nextPrompt,
76537
77207
  runnerMetadata,
76538
- startedByRunnerSignature,
77208
+ previousRunnerSignatures,
76539
77209
  promptExecutionStartedDate,
76540
77210
  attemptCount,
76541
77211
  error: lastError,
@@ -76549,21 +77219,17 @@
76549
77219
  }, { preserveArtifactsOnSuccess: options.preserveLogs });
76550
77220
  }
76551
77221
  /**
76552
- * Reads which harness left the prompt of this round in the middle of its implementation.
77222
+ * Reads the chronological harness report left on the prompt before this round rewrites its in-progress status.
76553
77223
  *
76554
77224
  * Only a prompt resumed through `--git-changes continue` still carries the in-progress `[^]` status when its
76555
- * round starts, so every other round reports no earlier harness. A harness which continues its own work is
76556
- * reported as no earlier harness either, because the status line would just name it twice.
77225
+ * round starts, so every other round starts a fresh report.
76557
77226
  */
76558
- function resolveStartedByRunnerSignature(options) {
76559
- const { nextPrompt, runnerMetadata, thinkingLevel } = options;
77227
+ function resolvePreviousRunnerSignatures(nextPrompt) {
76560
77228
  if (nextPrompt.section.status !== 'in-progress') {
76561
77229
  return undefined;
76562
77230
  }
76563
77231
  const { line } = resolvePromptStatusLine(nextPrompt.file, nextPrompt.section);
76564
- const startedByRunnerSignature = parsePromptStartedByRunnerSignature(line);
76565
- const currentRunnerSignature = formatRunnerSignature(runnerMetadata.runnerName, runnerMetadata.modelName, thinkingLevel);
76566
- return startedByRunnerSignature === currentRunnerSignature ? undefined : startedByRunnerSignature;
77232
+ return parsePromptRunnerAttribution(line);
76567
77233
  }
76568
77234
  /**
76569
77235
  * Records into the prompt file that the prompt is being implemented right now.
@@ -76573,7 +77239,7 @@
76573
77239
  * crashes leaves the `[^]` status behind as the signal that this task was left in the middle.
76574
77240
  */
76575
77241
  async function recordPromptRoundInProgress(options) {
76576
- const { nextPrompt, runnerMetadata, startedByRunnerSignature, thinkingLevel, attemptCount, progress } = options;
77242
+ const { nextPrompt, runnerMetadata, previousRunnerSignatures, thinkingLevel, attemptCount, progress } = options;
76577
77243
  markPromptInProgress({
76578
77244
  file: nextPrompt.file,
76579
77245
  section: nextPrompt.section,
@@ -76581,7 +77247,7 @@
76581
77247
  inProgressStepKind: progress.startedStepKind,
76582
77248
  runnerName: runnerMetadata.runnerName,
76583
77249
  modelName: runnerMetadata.modelName,
76584
- startedByRunnerSignature,
77250
+ previousRunnerSignatures,
76585
77251
  attemptCount,
76586
77252
  loginMethod: progress.loginMethod,
76587
77253
  thinkingLevel,
@@ -76639,7 +77305,7 @@
76639
77305
  * Finalizes a successful prompt round, including prompt bookkeeping and commit flow.
76640
77306
  */
76641
77307
  async function finalizeSuccessfulPromptRound(options) {
76642
- const { options: runOptions, nextPrompt, runnerMetadata, startedByRunnerSignature, promptExecutionStartedDate, result, commitMessage, logPath, roundCommitScope, isRichUiEnabled, progressDisplay, uiHandle, waitForRequestedPause, roundProjectPath, } = options;
77308
+ const { options: runOptions, nextPrompt, runnerMetadata, previousRunnerSignatures, promptExecutionStartedDate, result, commitMessage, logPath, roundCommitScope, isRichUiEnabled, progressDisplay, uiHandle, waitForRequestedPause, roundProjectPath, } = options;
76643
77309
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.stopCapturingAgentOutput();
76644
77310
  await waitForRequestedPause({
76645
77311
  checkpointLabel: 'recording the successful prompt result',
@@ -76652,7 +77318,7 @@
76652
77318
  steps: result.steps,
76653
77319
  runnerName: runnerMetadata.runnerName,
76654
77320
  modelName: runnerMetadata.modelName,
76655
- startedByRunnerSignature,
77321
+ previousRunnerSignatures,
76656
77322
  attemptCount: result.attemptCount,
76657
77323
  loginMethod: result.loginMethod,
76658
77324
  thinkingLevel: runOptions.thinkingLevel,
@@ -76707,7 +77373,7 @@
76707
77373
  * Finalizes a failed prompt round, persisting prompt failure metadata before rethrowing.
76708
77374
  */
76709
77375
  async function finalizeFailedPromptRound(options) {
76710
- const { nextPrompt, runnerMetadata, startedByRunnerSignature, promptExecutionStartedDate, attemptCount, error, options: runOptions, roundCommitScope, uiHandle, waitForRequestedPause, roundProjectPath, } = options;
77376
+ const { nextPrompt, runnerMetadata, previousRunnerSignatures, promptExecutionStartedDate, attemptCount, error, options: runOptions, roundCommitScope, uiHandle, waitForRequestedPause, roundProjectPath, } = options;
76711
77377
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.stopCapturingAgentOutput();
76712
77378
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.setPhase('error');
76713
77379
  uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state.addError(error instanceof Error ? error.message : String(error));
@@ -76721,7 +77387,7 @@
76721
77387
  section: nextPrompt.section,
76722
77388
  runnerName: runnerMetadata.runnerName,
76723
77389
  modelName: runnerMetadata.modelName,
76724
- startedByRunnerSignature,
77390
+ previousRunnerSignatures,
76725
77391
  promptExecutionStartedDate,
76726
77392
  attemptCount,
76727
77393
  });
@@ -77737,6 +78403,10 @@
77737
78403
  console.info(colors__default["default"].green(`Running prompts with ${runner.name}`));
77738
78404
  initializeRunUi(uiHandle, runner.name, actualRunnerModel, options);
77739
78405
  await initializeRunUiAgentVisual(uiHandle, resolvedCoderAgent === null || resolvedCoderAgent === void 0 ? void 0 : resolvedCoderAgent.agentSource);
78406
+ await refreshCoderRunUiSubscriptionUsage({
78407
+ runner,
78408
+ uiState: uiHandle === null || uiHandle === void 0 ? void 0 : uiHandle.state,
78409
+ });
77740
78410
  await seedCachedAveragePromptDuration({
77741
78411
  options,
77742
78412
  actualRunnerModel,
@@ -77963,6 +78633,13 @@
77963
78633
  An isolated task is implemented in a fresh temporary worktree checked out from the last commit, so the uncommitted changes of the interrupted prompt would be left behind instead of being continued.
77964
78634
  `));
77965
78635
  }
78636
+ if (options.gitChanges === 'continue' && options.testBefore === 'yes-and-fix') {
78637
+ throw new NotAllowed(_spaceTrim.spaceTrim(`
78638
+ Flag \`--git-changes continue\` cannot be combined with \`--test-before yes-and-fix\`.
78639
+
78640
+ An interrupted prompt already has changes in progress, so there is no unmodified project state for pre-coding verification to repair.
78641
+ `));
78642
+ }
77966
78643
  if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit <= 0)) {
77967
78644
  throw new NotAllowed(_spaceTrim.spaceTrim(`
77968
78645
  Flag \`--limit\` expects a positive integer.