@hyperdrive.bot/paseo-protocol 0.3.63 → 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.
- package/dist/tag-color.d.ts +84 -0
- package/dist/tag-color.js +122 -0
- package/package.json +1 -1
|
@@ -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
|