@hyperdrive.bot/paseo-protocol 0.3.62 → 0.3.65

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.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * MARRA CUSTOMIZATION - per-tag colors.
3
+ *
4
+ * User tags live on `Agent.labels` as `tag:<name>=<value>` (see agent-labels
5
+ * conventions and packages/app/src/utils/agent-tags.ts). The VALUE was
6
+ * historically the literal string "true", and both existing readers - the app's
7
+ * `getAgentTags` and the server's `tagsFromLabels` - key off the label KEY and
8
+ * never interpret the value beyond the "false" tombstone.
9
+ *
10
+ * That makes the value a free storage slot: `tag:monumenta=teal` is already a
11
+ * legal, already-persisted, already-synced way to pin a tag's color, with no
12
+ * schema change, no migration and no new RPC. `agent-storage.ts` types labels as
13
+ * `z.record(z.string(), z.string())`, so there is nothing to widen.
14
+ *
15
+ * Resolution order, deliberately in that order:
16
+ * 1. An explicit, recognized color name in the label value wins.
17
+ * 2. Anything else (including the legacy "true", and any unrecognized name)
18
+ * falls through to a stable hash of the tag name.
19
+ *
20
+ * Consequence worth stating: every session stored before this shipped lights up
21
+ * correctly on first render, because the color is a function of data we already
22
+ * have. Nothing backfills, nothing rewrites "true".
23
+ *
24
+ * This module is intentionally pure and pixel-free. It resolves a color NAME.
25
+ * Mapping a name to an actual hex or an ANSI code belongs to each surface (the
26
+ * app maps through its theme palette, the CLI can map to 256-color), so the
27
+ * protocol never carries UI colors and every surface still agrees on WHICH of
28
+ * the eight a given tag gets.
29
+ */
30
+ /**
31
+ * The eight tag colors, in palette order. Eight is a deliberate ceiling: enough
32
+ * to encode a real taxonomy, few enough that a person still remembers what each
33
+ * one means by the end of the week.
34
+ *
35
+ * Order is part of the contract. Appending is safe; reordering or removing
36
+ * silently repaints every tag that hashes past the change, so do not.
37
+ */
38
+ export declare const TAG_COLOR_NAMES: readonly ["red", "orange", "amber", "green", "teal", "blue", "purple", "pink"];
39
+ export type TagColorName = (typeof TAG_COLOR_NAMES)[number];
40
+ /**
41
+ * The label-key prefix that marks a user tag. `tag:monumenta` is the tag
42
+ * "monumenta". Lives here so the app and the protocol cannot drift; the server
43
+ * keeps its own copy in agent-list-filter.ts and is deliberately left alone in
44
+ * this change, since consolidating it is a separate refactor with its own blast
45
+ * radius.
46
+ */
47
+ export declare const TAG_LABEL_PREFIX = "tag:";
48
+ /** Whether `value` names one of the eight colors, case-insensitively. */
49
+ export declare function isTagColorName(value: string | null | undefined): value is TagColorName;
50
+ /**
51
+ * FNV-1a over the tag's UTF-16 code units, folded to a palette index.
52
+ *
53
+ * Chosen because it is tiny, dependency-free and identical in every runtime that
54
+ * can index a string, which is the whole point: iOS, desktop, web and the CLI
55
+ * must land on the same swatch for the same tag without coordinating. `>>> 0`
56
+ * keeps the accumulator unsigned after Math.imul, so the index is never negative.
57
+ */
58
+ export declare function hashTagToColorIndex(tag: string): number;
59
+ /**
60
+ * The color name for a tag, given the raw label value stored alongside it.
61
+ *
62
+ * `labelValue` is whatever `labels["tag:" + tag]` holds: a color name to pin it,
63
+ * "true" for the legacy default, or anything else. Unrecognized values are NOT
64
+ * an error - they fall through to the hash, which is what keeps this readable by
65
+ * a human editing ~/.paseo/agents/*.json by hand without being able to break the
66
+ * UI.
67
+ */
68
+ export declare function resolveTagColorName(tag: string, labelValue?: string | null): TagColorName;
69
+ /** One tag paired with the color it resolves to. */
70
+ export interface ColoredTag {
71
+ tag: string;
72
+ color: TagColorName;
73
+ }
74
+ /**
75
+ * Every user tag on a label bag, with its resolved color, sorted by tag name.
76
+ *
77
+ * Sorted so a tab's stripe bands are stable across renders and across surfaces:
78
+ * an unsorted map iteration would let the same session paint its bands in a
79
+ * different order on desktop than on mobile.
80
+ */
81
+ export declare function coloredTagsFromLabels(labels: Record<string, string> | null | undefined, tagPrefix?: string): ColoredTag[];
82
+ /** Re-exported so callers can write the legacy value without hardcoding it. */
83
+ export declare const TAG_LABEL_PRESENT_VALUE = "true";
84
+ //# sourceMappingURL=tag-color.d.ts.map
@@ -0,0 +1,122 @@
1
+ /**
2
+ * MARRA CUSTOMIZATION - per-tag colors.
3
+ *
4
+ * User tags live on `Agent.labels` as `tag:<name>=<value>` (see agent-labels
5
+ * conventions and packages/app/src/utils/agent-tags.ts). The VALUE was
6
+ * historically the literal string "true", and both existing readers - the app's
7
+ * `getAgentTags` and the server's `tagsFromLabels` - key off the label KEY and
8
+ * never interpret the value beyond the "false" tombstone.
9
+ *
10
+ * That makes the value a free storage slot: `tag:monumenta=teal` is already a
11
+ * legal, already-persisted, already-synced way to pin a tag's color, with no
12
+ * schema change, no migration and no new RPC. `agent-storage.ts` types labels as
13
+ * `z.record(z.string(), z.string())`, so there is nothing to widen.
14
+ *
15
+ * Resolution order, deliberately in that order:
16
+ * 1. An explicit, recognized color name in the label value wins.
17
+ * 2. Anything else (including the legacy "true", and any unrecognized name)
18
+ * falls through to a stable hash of the tag name.
19
+ *
20
+ * Consequence worth stating: every session stored before this shipped lights up
21
+ * correctly on first render, because the color is a function of data we already
22
+ * have. Nothing backfills, nothing rewrites "true".
23
+ *
24
+ * This module is intentionally pure and pixel-free. It resolves a color NAME.
25
+ * Mapping a name to an actual hex or an ANSI code belongs to each surface (the
26
+ * app maps through its theme palette, the CLI can map to 256-color), so the
27
+ * protocol never carries UI colors and every surface still agrees on WHICH of
28
+ * the eight a given tag gets.
29
+ */
30
+ /**
31
+ * The eight tag colors, in palette order. Eight is a deliberate ceiling: enough
32
+ * to encode a real taxonomy, few enough that a person still remembers what each
33
+ * one means by the end of the week.
34
+ *
35
+ * Order is part of the contract. Appending is safe; reordering or removing
36
+ * silently repaints every tag that hashes past the change, so do not.
37
+ */
38
+ export const TAG_COLOR_NAMES = [
39
+ "red",
40
+ "orange",
41
+ "amber",
42
+ "green",
43
+ "teal",
44
+ "blue",
45
+ "purple",
46
+ "pink",
47
+ ];
48
+ /**
49
+ * The label-key prefix that marks a user tag. `tag:monumenta` is the tag
50
+ * "monumenta". Lives here so the app and the protocol cannot drift; the server
51
+ * keeps its own copy in agent-list-filter.ts and is deliberately left alone in
52
+ * this change, since consolidating it is a separate refactor with its own blast
53
+ * radius.
54
+ */
55
+ export const TAG_LABEL_PREFIX = "tag:";
56
+ /** The label value that means "tagged, no explicit color" (the legacy default). */
57
+ const LEGACY_PRESENT_VALUE = "true";
58
+ /** The label value that tombstones a tag as absent. Mirrors `getAgentTags`. */
59
+ const ABSENT_VALUE = "false";
60
+ const TAG_COLOR_NAME_SET = new Set(TAG_COLOR_NAMES);
61
+ /** Whether `value` names one of the eight colors, case-insensitively. */
62
+ export function isTagColorName(value) {
63
+ return typeof value === "string" && TAG_COLOR_NAME_SET.has(value.trim().toLowerCase());
64
+ }
65
+ /**
66
+ * FNV-1a over the tag's UTF-16 code units, folded to a palette index.
67
+ *
68
+ * Chosen because it is tiny, dependency-free and identical in every runtime that
69
+ * can index a string, which is the whole point: iOS, desktop, web and the CLI
70
+ * must land on the same swatch for the same tag without coordinating. `>>> 0`
71
+ * keeps the accumulator unsigned after Math.imul, so the index is never negative.
72
+ */
73
+ export function hashTagToColorIndex(tag) {
74
+ let hash = 0x811c9dc5;
75
+ const normalized = tag.trim().toLowerCase();
76
+ for (let i = 0; i < normalized.length; i += 1) {
77
+ hash ^= normalized.charCodeAt(i);
78
+ hash = Math.imul(hash, 0x01000193) >>> 0;
79
+ }
80
+ return hash % TAG_COLOR_NAMES.length;
81
+ }
82
+ /**
83
+ * The color name for a tag, given the raw label value stored alongside it.
84
+ *
85
+ * `labelValue` is whatever `labels["tag:" + tag]` holds: a color name to pin it,
86
+ * "true" for the legacy default, or anything else. Unrecognized values are NOT
87
+ * an error - they fall through to the hash, which is what keeps this readable by
88
+ * a human editing ~/.paseo/agents/*.json by hand without being able to break the
89
+ * UI.
90
+ */
91
+ export function resolveTagColorName(tag, labelValue) {
92
+ if (isTagColorName(labelValue)) {
93
+ return labelValue.trim().toLowerCase();
94
+ }
95
+ return TAG_COLOR_NAMES[hashTagToColorIndex(tag)];
96
+ }
97
+ /**
98
+ * Every user tag on a label bag, with its resolved color, sorted by tag name.
99
+ *
100
+ * Sorted so a tab's stripe bands are stable across renders and across surfaces:
101
+ * an unsorted map iteration would let the same session paint its bands in a
102
+ * different order on desktop than on mobile.
103
+ */
104
+ export function coloredTagsFromLabels(labels, tagPrefix = TAG_LABEL_PREFIX) {
105
+ if (!labels)
106
+ return [];
107
+ const result = [];
108
+ for (const key of Object.keys(labels)) {
109
+ if (!key.startsWith(tagPrefix) || key.length <= tagPrefix.length)
110
+ continue;
111
+ const value = labels[key];
112
+ if (value === ABSENT_VALUE)
113
+ continue;
114
+ const tag = key.slice(tagPrefix.length);
115
+ result.push({ tag, color: resolveTagColorName(tag, value) });
116
+ }
117
+ result.sort((a, b) => a.tag.localeCompare(b.tag));
118
+ return result;
119
+ }
120
+ /** Re-exported so callers can write the legacy value without hardcoding it. */
121
+ export const TAG_LABEL_PRESENT_VALUE = LEGACY_PRESENT_VALUE;
122
+ //# sourceMappingURL=tag-color.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperdrive.bot/paseo-protocol",
3
- "version": "0.3.62",
3
+ "version": "0.3.65",
4
4
  "description": "Paseo shared protocol schemas and wire types",
5
5
  "files": [
6
6
  "dist",