@anchrd/intel-ui 0.13.0 → 0.15.0
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/package.json +2 -2
- package/src/agent/agent-calendar/agent-calendar.tsx +42 -4
- package/src/agent/agent-costs/agent-costs.ts +95 -0
- package/src/agent/agent-delegation-notice/agent-delegation-notice.ts +48 -0
- package/src/agent/agent-entry-title/agent-entry-title.ts +48 -7
- package/src/agent/agent-log/agent-log.tsx +127 -11
- package/src/agent/agent-models/agent-models.ts +31 -67
- package/src/agent/agent-models/agent-models.types.ts +11 -5
- package/src/agent/agent-profile/agent-profile.tsx +238 -65
- package/src/agent/agent-state/agent-state.ts +17 -0
- package/src/agent/agent-status-dot/agent-status-dot.ts +73 -0
- package/src/agent/agent.tsx +159 -50
- package/src/app/app-tree/app-tree.tsx +9 -3
- package/src/app/settings-dialog/settings-dialog.tsx +22 -31
- package/src/components/ui/popover.tsx +41 -0
- package/src/data/agent-runtime/agent-runtime.ts +26 -0
- package/src/data/intel-data-provider/intel-data-provider.ts +20 -0
- package/src/data/intel-data-provider/intel-data-provider.types.ts +19 -0
- package/src/entry-picker/entry-picker.tsx +16 -5
- package/src/hooks/use-model-catalog.ts +26 -0
- package/src/i18n/de.json +32 -10
- package/src/i18n/en.json +32 -10
- package/src/i18n/es.json +32 -10
- package/src/section-hint/section-hint.tsx +40 -0
- package/src/timezone/timezone-combobox/timezone-combobox.tsx +149 -0
- package/src/timezone/timezone-context.tsx +25 -5
- package/src/timezone/timezone.ts +119 -5
- package/src/title-row/title-row.tsx +9 -4
- package/src/user-name/user-name.ts +41 -0
- package/tsconfig.json +7 -1
package/src/timezone/timezone.ts
CHANGED
|
@@ -39,16 +39,31 @@ export function isKnownTimezone(zone: string): boolean {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The zone the reader actually CHOSE, or `null` when they never did.
|
|
44
|
+
*
|
|
45
|
+
* ⚠️ The difference from `readTimezone` is the whole reason this exists (#256). A screen that wants
|
|
46
|
+
* to add "…and this is what it says on your clock" may only do so for a zone somebody set: falling
|
|
47
|
+
* back to the browser would put a guess on screen dressed as a preference, and the agent's calendar
|
|
48
|
+
* would start annotating every schedule for a reader who never asked for it.
|
|
49
|
+
*
|
|
50
|
+
* A stored zone the engine no longer knows counts as "never chose": IANA renames zones, and a name
|
|
51
|
+
* that stopped resolving would otherwise throw on every date the calendar draws.
|
|
52
|
+
*/
|
|
53
|
+
export function readChosenTimezone(store?: Pick<Storage, "getItem">): string | null {
|
|
45
54
|
let stored: string | null = null;
|
|
46
55
|
try {
|
|
47
56
|
stored = store?.getItem(STORE_KEY) ?? null;
|
|
48
57
|
} catch {
|
|
49
|
-
return
|
|
58
|
+
return null;
|
|
50
59
|
}
|
|
51
|
-
return stored !== null && isKnownTimezone(stored) ? stored :
|
|
60
|
+
return stored !== null && isKnownTimezone(stored) ? stored : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The zone to read times in — the choice, and the browser's own where there is none. Never null: a
|
|
64
|
+
// component that has to format a date needs an answer, and the browser's is the honest one.
|
|
65
|
+
export function readTimezone(store?: Pick<Storage, "getItem">): string {
|
|
66
|
+
return readChosenTimezone(store) ?? browserTimezone();
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
export function rememberTimezone(zone: string, store?: Pick<Storage, "setItem">): void {
|
|
@@ -58,3 +73,102 @@ export function rememberTimezone(zone: string, store?: Pick<Storage, "setItem">)
|
|
|
58
73
|
// A preference that cannot be written is still a preference for this session.
|
|
59
74
|
}
|
|
60
75
|
}
|
|
76
|
+
|
|
77
|
+
/** One zone as the picker shows it. Everything here is derived, nothing is stored. */
|
|
78
|
+
export interface TimezoneOption {
|
|
79
|
+
zone: string;
|
|
80
|
+
/** "GMT+02:00", "GMT-04:00", "GMT" — present for every zone, which is why it carries the column. */
|
|
81
|
+
offsetLabel: string;
|
|
82
|
+
/** "MESZ", "JST" … or null where the runtime only repeats the offset. */
|
|
83
|
+
abbreviation: string | null;
|
|
84
|
+
/** Minutes east of UTC at `at`, for sorting. */
|
|
85
|
+
offsetMinutes: number;
|
|
86
|
+
/** What the search matches on, lowercased. */
|
|
87
|
+
keywords: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function part(zone: string, style: "short" | "longOffset", at: Date, locale: string): string {
|
|
91
|
+
try {
|
|
92
|
+
return (
|
|
93
|
+
new Intl.DateTimeFormat(locale, { timeZone: zone, timeZoneName: style })
|
|
94
|
+
.formatToParts(at)
|
|
95
|
+
.find((piece) => piece.type === "timeZoneName")?.value ?? ""
|
|
96
|
+
);
|
|
97
|
+
} catch {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// "GMT+02:00" → 120, "GMT-04:00" → -240, "GMT" → 0. Read from the same string the label shows, so
|
|
103
|
+
// the sort order and the text can never tell different stories.
|
|
104
|
+
function minutesFrom(offsetLabel: string): number {
|
|
105
|
+
const match = /^GMT([+-])(\d{2}):(\d{2})$/.exec(offsetLabel);
|
|
106
|
+
if (!match) return 0;
|
|
107
|
+
const [, sign, hours, minutes] = match;
|
|
108
|
+
const total = Number(hours) * 60 + Number(minutes);
|
|
109
|
+
return sign === "-" ? -total : total;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The label of one zone, at a given moment.
|
|
114
|
+
*
|
|
115
|
+
* ⚠️ Computed at display time and never stored. A zone's offset AND its abbreviation move with
|
|
116
|
+
* daylight saving — Berlin is MEZ/+01:00 in January and MESZ/+02:00 in July — so a label written
|
|
117
|
+
* into a field would be wrong for half of every year.
|
|
118
|
+
*
|
|
119
|
+
* ⚠️ The abbreviation is dropped where `Intl` only echoes the offset — and WHICH zones have one
|
|
120
|
+
* depends on the READER'S LANGUAGE, not on the zone. CLDR ships short names only where a locale has
|
|
121
|
+
* its own word for that zone: a German reader gets "MEZ/MESZ" for Berlin and a bare "GMT-4" for New
|
|
122
|
+
* York; an English reader gets "EST/EDT" for New York and a bare "GMT+2" for Berlin. That is the
|
|
123
|
+
* useful way round — you get the abbreviation for the zones your language actually names — but it
|
|
124
|
+
* means this function's output is not the same in two languages, and a test has to say which one it
|
|
125
|
+
* is asserting. `Asia/Kolkata` has none in either ("GMT+5:30"), which is the common case worldwide.
|
|
126
|
+
*/
|
|
127
|
+
export function timezoneOption(zone: string, at: Date, locale: string): TimezoneOption {
|
|
128
|
+
const offsetLabel = part(zone, "longOffset", at, locale) || "GMT";
|
|
129
|
+
const short = part(zone, "short", at, locale);
|
|
130
|
+
// Dropped when it repeats the offset, and also when it repeats the zone's own name: "UTC" would
|
|
131
|
+
// otherwise render as "GMT · UTC UTC", which stutters without adding anything.
|
|
132
|
+
const abbreviation =
|
|
133
|
+
short && !short.startsWith("GMT") && short !== offsetLabel && short !== zone ? short : null;
|
|
134
|
+
const city = zone.split("/").pop()?.replace(/_/g, " ") ?? zone;
|
|
135
|
+
return {
|
|
136
|
+
zone,
|
|
137
|
+
offsetLabel,
|
|
138
|
+
abbreviation,
|
|
139
|
+
offsetMinutes: minutesFrom(offsetLabel),
|
|
140
|
+
// The offset appears twice on purpose: as written ("GMT+02:00") and as typed ("+2"), because
|
|
141
|
+
// nobody searches for the leading zero.
|
|
142
|
+
keywords: [zone, city, offsetLabel, abbreviation ?? "", shorthandOffset(offsetLabel)]
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.map((word) => word.toLowerCase()),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// "GMT+02:00" → "+2", "GMT+05:30" → "+5:30", "GMT" → "+0". What a person types.
|
|
149
|
+
function shorthandOffset(offsetLabel: string): string {
|
|
150
|
+
const match = /^GMT([+-])(\d{2}):(\d{2})$/.exec(offsetLabel);
|
|
151
|
+
if (!match) return "+0";
|
|
152
|
+
const [, sign, hours, minutes] = match;
|
|
153
|
+
return `${sign}${Number(hours)}${minutes === "00" ? "" : `:${minutes}`}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Every selectable zone, sorted west to east.
|
|
158
|
+
*
|
|
159
|
+
* ⚠️ `current` is folded in even when `supportedValuesOf` does not list it. That is not a corner
|
|
160
|
+
* case: the list holds only CANONICAL names, and "UTC" is not one of them (it is an alias of
|
|
161
|
+
* Etc/UTC) — so the zone every CI machine and many servers report would otherwise be missing from
|
|
162
|
+
* the picker that is supposed to be showing it.
|
|
163
|
+
*/
|
|
164
|
+
export function timezoneOptions(current: string, at: Date, locale: string): TimezoneOption[] {
|
|
165
|
+
const zones = availableTimezones();
|
|
166
|
+
const all = zones.includes(current) ? zones : [current, ...zones];
|
|
167
|
+
return all
|
|
168
|
+
.map((zone) => timezoneOption(zone, at, locale))
|
|
169
|
+
.sort((left, right) =>
|
|
170
|
+
left.offsetMinutes === right.offsetMinutes
|
|
171
|
+
? left.zone.localeCompare(right.zone)
|
|
172
|
+
: left.offsetMinutes - right.offsetMinutes,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -11,10 +11,11 @@ import { ResourceMenu, type ResourceTarget } from "@/resource-menu/resource-menu
|
|
|
11
11
|
* the rule could be forgotten. That is the whole point: the menu holds rename, move, share and
|
|
12
12
|
* archive, and a position one has to look for is a position one stops using.
|
|
13
13
|
*
|
|
14
|
-
* The
|
|
15
|
-
* is. `title-meta` is a quiet word beside the name (a table's row count); `title-
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* The three slots are for what belongs to this thing but lives further down the tree, where its
|
|
15
|
+
* state is. `title-meta` is a quiet word beside the name (a table's row count); `title-byline` is a
|
|
16
|
+
* quiet line under it (who made an agent, #258); `title-actions` is a button in the group (saving a
|
|
17
|
+
* document, exporting a table). All are filled through `ActionSlot`, and the two in the group sit
|
|
18
|
+
* before the menu for the same reason `children` do.
|
|
18
19
|
*/
|
|
19
20
|
export function TitleRow({
|
|
20
21
|
title,
|
|
@@ -47,6 +48,10 @@ export function TitleRow({
|
|
|
47
48
|
<h2 className="truncate text-lg font-semibold">{title}</h2>
|
|
48
49
|
<span data-slot="title-meta" className="text-sm text-muted-foreground" />
|
|
49
50
|
</div>
|
|
51
|
+
{/* ⚠️ `empty:hidden` is the whole rule for "no line rather than an empty one": nothing is
|
|
52
|
+
portalled in here for most kinds, and a slot that kept its margin would make every
|
|
53
|
+
head a few pixels taller for a line nobody wrote (#258). */}
|
|
54
|
+
<span data-slot="title-byline" className="mt-0.5 block min-w-0 empty:hidden" />
|
|
50
55
|
{description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
|
|
51
56
|
</div>
|
|
52
57
|
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SessionUser } from "@anchrd/intel-contract";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The signed-in person, from the one query the shell already makes.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ The same query key the user footer uses, so asking here costs no second request — and the two
|
|
9
|
+
* can never disagree about who is signed in.
|
|
10
|
+
*/
|
|
11
|
+
export function useSessionUser(): SessionUser | null {
|
|
12
|
+
const { data } = useIntelRouterContext();
|
|
13
|
+
const session = useQuery({ queryKey: ["session"], queryFn: () => data.getSession() });
|
|
14
|
+
return session.data ?? null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** What a person is CALLED, by the same rule the user footer follows: the name, or the address. */
|
|
18
|
+
export function displayNameOf(user: SessionUser): string {
|
|
19
|
+
return user.name ?? user.email;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The display name behind a user id — or `null` where this browser cannot say.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ Today it can say for exactly one id: the signed-in person's. Intel has no user directory —
|
|
26
|
+
* `/session` answers from the token's own subject and deliberately carries nothing else, and the
|
|
27
|
+
* Gate SDK Intel holds (`authorize`, `interfaces`) has no lookup either. So a foreign id is not
|
|
28
|
+
* "probably a person whose name we could guess": it is unresolvable, and every caller here has to
|
|
29
|
+
* treat it as such.
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ Never fall back to the id. An id under a name is not an answer to "who made this", it is the
|
|
32
|
+
* question again in smaller type — and it would leak an identifier onto a screen that has no reason
|
|
33
|
+
* to carry one. Callers show nothing instead (#258).
|
|
34
|
+
*
|
|
35
|
+
* This is the ONE place that changes when Intel gains a way to resolve a name (#261).
|
|
36
|
+
*/
|
|
37
|
+
export function useUserName(userId: string | null): string | null {
|
|
38
|
+
const me = useSessionUser();
|
|
39
|
+
if (userId === null || me === null || me.id !== userId) return null;
|
|
40
|
+
return displayNameOf(me);
|
|
41
|
+
}
|
package/tsconfig.json
CHANGED