@anchrd/intel-ui 0.17.0 → 0.19.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 -9
- package/src/app/app-tree/app-tree.tsx +59 -147
- package/src/app/settings-dialog/settings-dialog.tsx +10 -32
- package/src/data/intel-data-provider/intel-data-provider.ts +0 -163
- package/src/data/intel-data-provider/intel-data-provider.types.ts +0 -100
- package/src/entry-picker/entry-picker.tsx +6 -6
- package/src/i18n/de.json +6 -213
- package/src/i18n/en.json +6 -213
- package/src/i18n/es.json +6 -213
- package/src/import-bundle/import-bundle.tsx +152 -0
- package/src/kind-icon.ts +1 -15
- package/src/main.tsx +7 -10
- package/src/node-table/node-table.tsx +5 -4
- package/src/nodes/nodes.tsx +0 -34
- package/src/resource-menu/resource-menu.tsx +68 -20
- package/src/section-hint/section-hint.tsx +2 -2
- package/src/title-row/title-row.tsx +5 -22
- package/src/tools/tools.tsx +0 -2
- package/vite.config.ts +0 -4
- package/src/agent/agent-avatar/agent-avatar.tsx +0 -34
- package/src/agent/agent-calendar/agent-calendar.tsx +0 -135
- package/src/agent/agent-chat/agent-chat.tsx +0 -118
- package/src/agent/agent-costs/agent-costs.ts +0 -95
- package/src/agent/agent-cron/agent-cron.ts +0 -68
- package/src/agent/agent-definition/agent-definition.ts +0 -96
- package/src/agent/agent-delegation-notice/agent-delegation-notice.ts +0 -48
- package/src/agent/agent-entry-title/agent-entry-title.ts +0 -72
- package/src/agent/agent-log/agent-log.tsx +0 -308
- package/src/agent/agent-models/agent-models.ts +0 -166
- package/src/agent/agent-models/agent-models.types.ts +0 -24
- package/src/agent/agent-profile/agent-profile.tsx +0 -988
- package/src/agent/agent-state/agent-state.ts +0 -85
- package/src/agent/agent-status-dot/agent-status-dot.ts +0 -73
- package/src/agent/agent.tsx +0 -482
- package/src/board/board-calendar/board-calendar.tsx +0 -89
- package/src/board/board-card/board-card.tsx +0 -106
- package/src/board/board-data/board-data.ts +0 -241
- package/src/board/board-data/board-data.types.ts +0 -63
- package/src/board/board-detail/board-detail.tsx +0 -629
- package/src/board/board-gantt/board-gantt.ts +0 -545
- package/src/board/board-gantt/board-gantt.tsx +0 -286
- package/src/board/board-graph/board-graph.ts +0 -174
- package/src/board/board-graph/board-graph.tsx +0 -168
- package/src/board/board-items/board-items.ts +0 -183
- package/src/board/board-kanban/board-kanban.ts +0 -97
- package/src/board/board-kanban/board-kanban.tsx +0 -211
- package/src/board/board-status/board-status.ts +0 -59
- package/src/board/board-statuses/board-statuses.ts +0 -63
- package/src/board/board-statuses/board-statuses.tsx +0 -228
- package/src/board/board-table/board-table.ts +0 -33
- package/src/board/board-table/board-table.tsx +0 -413
- package/src/board/board-views/board-views.tsx +0 -68
- package/src/board/board-views/board-views.types.ts +0 -29
- package/src/board/board.tsx +0 -251
- package/src/components/ui/item-calendar.tsx +0 -181
- package/src/components/ui/item-gantt.tsx +0 -463
- package/src/components/ui/kanban.tsx +0 -245
- package/src/data/agent-runtime/agent-runtime.ts +0 -131
- package/src/hooks/use-capabilities.ts +0 -22
- package/src/hooks/use-model-catalog.ts +0 -26
- package/src/timezone/timezone-combobox/timezone-combobox.tsx +0 -149
- package/src/timezone/timezone-context.tsx +0 -65
- package/src/timezone/timezone.ts +0 -174
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Announcements,
|
|
3
|
-
closestCorners,
|
|
4
|
-
DndContext,
|
|
5
|
-
DragOverlay,
|
|
6
|
-
type DragStartEvent,
|
|
7
|
-
KeyboardSensor,
|
|
8
|
-
PointerSensor,
|
|
9
|
-
useDroppable,
|
|
10
|
-
useSensor,
|
|
11
|
-
useSensors,
|
|
12
|
-
} from "@dnd-kit/core";
|
|
13
|
-
import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
14
|
-
import { CSS } from "@dnd-kit/utilities";
|
|
15
|
-
import { GripVertical } from "lucide-react";
|
|
16
|
-
import { type ReactNode, useState } from "react";
|
|
17
|
-
import { createPortal } from "react-dom";
|
|
18
|
-
import { cn } from "@/lib/utils";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* A kanban board on dnd-kit, from the Kibo UI registry (MIT) and changed where it had to be.
|
|
22
|
-
*
|
|
23
|
-
* Registry code is our code, and four things about the original do not survive contact with a board
|
|
24
|
-
* whose truth is on a server:
|
|
25
|
-
*
|
|
26
|
-
* ⚠️ **It reported a drop as a new ARRAY, and it reported one twice.** The original called
|
|
27
|
-
* `onDataChange` in `onDragOver` for a cross-column move and again in `onDragEnd`, and both times
|
|
28
|
-
* with a reordered copy of the whole list. Against a server that is two writes for one drag — and
|
|
29
|
-
* the second one describes a state the first already changed. This version says nothing while the
|
|
30
|
-
* card is in the air and emits ONE drop at the end, naming what was dropped and what it was dropped
|
|
31
|
-
* on. What that means is the caller's to decide (`board-kanban.ts`), which is also where the single
|
|
32
|
-
* `board_task_move` is sent from.
|
|
33
|
-
*
|
|
34
|
-
* ⚠️ **It mutated its own props** — `newData[activeIndex].column = overColumn` writes into the
|
|
35
|
-
* object the caller passed in. With TanStack Query holding that object, a drag edited the cache in
|
|
36
|
-
* place and no re-render followed.
|
|
37
|
-
*
|
|
38
|
-
* ⚠️ **`tunnel-rat` is gone.** It existed to render the dragged card into the overlay from inside
|
|
39
|
-
* the card component; `DragOverlay` already does that, and the caller draws the overlay itself.
|
|
40
|
-
* One dependency fewer for something React has a portal for.
|
|
41
|
-
*
|
|
42
|
-
* ⚠️ **The screen reader announcements are handed in.** The originals are English string literals
|
|
43
|
-
* in the component; Intel speaks three languages and its text comes from `useI18n()`.
|
|
44
|
-
*
|
|
45
|
-
* ⚠️ **The card is not itself a `role="button"`.** The original spread `useSortable().attributes`
|
|
46
|
-
* onto the card, which contributes `role="button"` and `tabIndex={0}` — and a card that also has to
|
|
47
|
-
* be OPENED then holds a real button inside that one. Two tab stops computing the same accessible
|
|
48
|
-
* name from the same contents, and an Enter that means "start dragging" on the outside and "open
|
|
49
|
-
* this" on the inside. Split instead: the card keeps `listeners`, so a pointer can still drag it
|
|
50
|
-
* from anywhere, and `attributes` moves to a handle that is the one tab stop for dragging and says
|
|
51
|
-
* so.
|
|
52
|
-
*/
|
|
53
|
-
|
|
54
|
-
export interface KanbanColumn {
|
|
55
|
-
id: string;
|
|
56
|
-
name: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface KanbanDrop {
|
|
60
|
-
activeId: string;
|
|
61
|
-
// The column or the card it was let go over. `null` means it was let go over nothing — a drag
|
|
62
|
-
// that ended outside the board, which is a cancel and not a move.
|
|
63
|
-
overId: string | null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function KanbanBoard({
|
|
67
|
-
id,
|
|
68
|
-
droppable = true,
|
|
69
|
-
children,
|
|
70
|
-
className,
|
|
71
|
-
}: {
|
|
72
|
-
id: string;
|
|
73
|
-
// A column that exists to be READ but must not be written into. It still draws and still holds
|
|
74
|
-
// cards; it simply never becomes a drop target, so nothing can be dropped where the answer would
|
|
75
|
-
// be a refusal (`board-kanban.tsx`).
|
|
76
|
-
droppable?: boolean;
|
|
77
|
-
children: ReactNode;
|
|
78
|
-
className?: string;
|
|
79
|
-
}) {
|
|
80
|
-
const { isOver, setNodeRef } = useDroppable({ id, disabled: !droppable });
|
|
81
|
-
return (
|
|
82
|
-
<div
|
|
83
|
-
ref={setNodeRef}
|
|
84
|
-
data-slot="kanban-column"
|
|
85
|
-
data-droppable={droppable}
|
|
86
|
-
className={cn(
|
|
87
|
-
"flex size-full min-h-40 flex-col divide-y overflow-hidden rounded-lg border bg-muted/40 text-xs ring-2 transition-colors",
|
|
88
|
-
isOver ? "ring-ring" : "ring-transparent",
|
|
89
|
-
className,
|
|
90
|
-
)}
|
|
91
|
-
>
|
|
92
|
-
{children}
|
|
93
|
-
</div>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function KanbanHeader({ children }: { children: ReactNode }) {
|
|
98
|
-
return <div className="flex items-center gap-2 p-2 text-sm font-semibold">{children}</div>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function KanbanCards({ id, children }: { id: string; children: ReactNode }) {
|
|
102
|
-
return (
|
|
103
|
-
<div
|
|
104
|
-
// The column scrolls, not the page: four lanes of different lengths must not make the board
|
|
105
|
-
// as tall as its longest one.
|
|
106
|
-
className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto p-2"
|
|
107
|
-
data-slot="kanban-cards"
|
|
108
|
-
data-column={id}
|
|
109
|
-
>
|
|
110
|
-
{children}
|
|
111
|
-
</div>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function KanbanCard({
|
|
116
|
-
id,
|
|
117
|
-
dragLabel,
|
|
118
|
-
children,
|
|
119
|
-
className,
|
|
120
|
-
}: {
|
|
121
|
-
id: string;
|
|
122
|
-
// What the drag handle is called. Handed in because only the caller knows what the card is of.
|
|
123
|
-
dragLabel: string;
|
|
124
|
-
children: ReactNode;
|
|
125
|
-
className?: string;
|
|
126
|
-
}) {
|
|
127
|
-
const {
|
|
128
|
-
attributes,
|
|
129
|
-
listeners,
|
|
130
|
-
setNodeRef,
|
|
131
|
-
setActivatorNodeRef,
|
|
132
|
-
transition,
|
|
133
|
-
transform,
|
|
134
|
-
isDragging,
|
|
135
|
-
} = useSortable({ id });
|
|
136
|
-
return (
|
|
137
|
-
<div
|
|
138
|
-
ref={setNodeRef}
|
|
139
|
-
style={{ transition, transform: CSS.Transform.toString(transform) }}
|
|
140
|
-
// ⚠️ `listeners` here, `attributes` NOT. The pointer sensor has to see a press anywhere on the
|
|
141
|
-
// card — a kanban whose cards can only be dragged by a grip is a kanban nobody drags — but
|
|
142
|
-
// `attributes` is what carries `role="button"` and `tabIndex`, and those on the card would
|
|
143
|
-
// wrap the control that OPENS it in a second button. Pointer drag from the whole card,
|
|
144
|
-
// keyboard drag from the handle, one tab stop each.
|
|
145
|
-
//
|
|
146
|
-
// ⚠️ The split only holds because the handle registers itself as the ACTIVATOR below. Without
|
|
147
|
-
// that, `listeners` on this element make every Enter and Space inside the card a drag: the
|
|
148
|
-
// key handler bubbles up from whatever is focused, `KeyboardSensor` accepts it and calls
|
|
149
|
-
// `preventDefault()`, and the button that opens the task never sees its own activation.
|
|
150
|
-
{...listeners}
|
|
151
|
-
className={cn(
|
|
152
|
-
"flex cursor-grab items-start gap-1 rounded-lg border bg-card p-3 text-left shadow-sm",
|
|
153
|
-
// Left where it was, faded, rather than removed: a lane that reflows while a card is over
|
|
154
|
-
// it moves the drop target out from under the pointer.
|
|
155
|
-
isDragging && "opacity-40",
|
|
156
|
-
className,
|
|
157
|
-
)}
|
|
158
|
-
>
|
|
159
|
-
{/* The keyboard half of the drag: `attributes` puts the card into the tab order exactly once,
|
|
160
|
-
here, where it has a name of its own that says what it does.
|
|
161
|
-
|
|
162
|
-
⚠️ `setActivatorNodeRef` is what makes that true rather than merely intended. dnd-kit's
|
|
163
|
-
`KeyboardSensor` opens with `if (activator && event.target !== activator) return false` —
|
|
164
|
-
a guard that only exists once something has claimed to BE the activator. Unclaimed, the
|
|
165
|
-
keyboard sensor accepts a key press from anywhere under the listeners, so Enter on the
|
|
166
|
-
card's own "open" button started a drag and swallowed the click. The pointer drag is
|
|
167
|
-
unaffected: `PointerSensor` never consults the activator node. */}
|
|
168
|
-
<button
|
|
169
|
-
type="button"
|
|
170
|
-
ref={setActivatorNodeRef}
|
|
171
|
-
aria-label={dragLabel}
|
|
172
|
-
{...listeners}
|
|
173
|
-
{...attributes}
|
|
174
|
-
className="mt-0.5 shrink-0 cursor-grab rounded text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
175
|
-
>
|
|
176
|
-
<GripVertical aria-hidden="true" className="size-4" />
|
|
177
|
-
</button>
|
|
178
|
-
<div className="min-w-0 flex-1">{children}</div>
|
|
179
|
-
</div>
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function KanbanProvider({
|
|
184
|
-
columns,
|
|
185
|
-
itemsByColumn,
|
|
186
|
-
children,
|
|
187
|
-
announcements,
|
|
188
|
-
overlay,
|
|
189
|
-
onDrop,
|
|
190
|
-
className,
|
|
191
|
-
}: {
|
|
192
|
-
columns: KanbanColumn[];
|
|
193
|
-
// Which cards stand in which column, in the order they are drawn. The provider needs it for the
|
|
194
|
-
// sortable contexts and asks for it rather than deriving it, so the caller stays the only place
|
|
195
|
-
// that knows how a board is ordered.
|
|
196
|
-
itemsByColumn: Map<string, string[]>;
|
|
197
|
-
children: (column: KanbanColumn) => ReactNode;
|
|
198
|
-
announcements: Announcements;
|
|
199
|
-
overlay(activeId: string): ReactNode;
|
|
200
|
-
onDrop(drop: KanbanDrop): void;
|
|
201
|
-
className?: string;
|
|
202
|
-
}) {
|
|
203
|
-
const [activeId, setActiveId] = useState<string | null>(null);
|
|
204
|
-
const sensors = useSensors(
|
|
205
|
-
// ⚠️ A distance before a drag starts, and it is not decoration: without it every click on a
|
|
206
|
-
// card is a drag of zero pixels, and the card can then no longer be opened by clicking it.
|
|
207
|
-
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
208
|
-
useSensor(KeyboardSensor),
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
return (
|
|
212
|
-
<DndContext
|
|
213
|
-
accessibility={{ announcements }}
|
|
214
|
-
// Corners rather than centres: a tall card over a short one wins on centre distance even
|
|
215
|
-
// when the pointer is nowhere near it.
|
|
216
|
-
collisionDetection={closestCorners}
|
|
217
|
-
sensors={sensors}
|
|
218
|
-
onDragStart={(event: DragStartEvent) => setActiveId(String(event.active.id))}
|
|
219
|
-
onDragCancel={() => setActiveId(null)}
|
|
220
|
-
onDragEnd={(event) => {
|
|
221
|
-
setActiveId(null);
|
|
222
|
-
const over = event.over === null ? null : String(event.over.id);
|
|
223
|
-
onDrop({ activeId: String(event.active.id), overId: over });
|
|
224
|
-
}}
|
|
225
|
-
>
|
|
226
|
-
<div className={cn("grid size-full auto-cols-fr grid-flow-col gap-3", className)}>
|
|
227
|
-
{columns.map((column) => (
|
|
228
|
-
<SortableContext
|
|
229
|
-
key={column.id}
|
|
230
|
-
items={itemsByColumn.get(column.id) ?? []}
|
|
231
|
-
strategy={verticalListSortingStrategy}
|
|
232
|
-
>
|
|
233
|
-
{children(column)}
|
|
234
|
-
</SortableContext>
|
|
235
|
-
))}
|
|
236
|
-
</div>
|
|
237
|
-
{typeof document === "undefined"
|
|
238
|
-
? null
|
|
239
|
-
: createPortal(
|
|
240
|
-
<DragOverlay>{activeId === null ? null : overlay(activeId)}</DragOverlay>,
|
|
241
|
-
document.body,
|
|
242
|
-
)}
|
|
243
|
-
</DndContext>
|
|
244
|
-
);
|
|
245
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* What the agent runtime answers, read back defensively.
|
|
5
|
-
*
|
|
6
|
-
* ⚠️ These shapes are NOT in `@anchrd/intel-contract`, and that is deliberate rather than an
|
|
7
|
-
* oversight: the runtime (`packages/agent`) imports nothing from the contract either, so a run
|
|
8
|
-
* record has no shared declaration anywhere. It crosses a network boundary into this package, so it
|
|
9
|
-
* is parsed here — tolerantly, in the same spirit the runtime reads a definition, because a field
|
|
10
|
-
* the runtime adds must not blank the log screen.
|
|
11
|
-
*/
|
|
12
|
-
export const AgentRunTarget = z.object({
|
|
13
|
-
kind: z.enum(["chat", "document", "flow", "mail", "task"]),
|
|
14
|
-
id: z.string().nullable(),
|
|
15
|
-
});
|
|
16
|
-
export type AgentRunTarget = z.infer<typeof AgentRunTarget>;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* What one run consumed at the provider, as the runtime recorded it (#250).
|
|
20
|
-
*
|
|
21
|
-
* ⚠️ `inputTokens` is the part that was NOT cached, not the size of the prompt — the prompt is all
|
|
22
|
-
* four fields added together. Anything that shows the first as "the prompt" will report a run that
|
|
23
|
-
* suddenly shrank to a twentieth of the previous one.
|
|
24
|
-
*
|
|
25
|
-
* ⚠️ Optional here, and the optionality is the feature. A run recorded before #250 carries no
|
|
26
|
-
* numbers at all, and that is a different fact from "used no tokens" — the row shows an empty state
|
|
27
|
-
* rather than four zeroes, because a zero reads as "cost nothing".
|
|
28
|
-
*/
|
|
29
|
-
export const AgentUsage = z.object({
|
|
30
|
-
inputTokens: z.number(),
|
|
31
|
-
outputTokens: z.number(),
|
|
32
|
-
cacheCreationInputTokens: z.number(),
|
|
33
|
-
cacheReadInputTokens: z.number(),
|
|
34
|
-
});
|
|
35
|
-
export type AgentUsage = z.infer<typeof AgentUsage>;
|
|
36
|
-
|
|
37
|
-
export const AgentRun = z.object({
|
|
38
|
-
id: z.string(),
|
|
39
|
-
trigger: z.enum(["chat", "schedule", "mail", "task", "manual"]),
|
|
40
|
-
target: AgentRunTarget,
|
|
41
|
-
status: z.enum(["running", "completed", "failed"]),
|
|
42
|
-
startedAt: z.string(),
|
|
43
|
-
finishedAt: z.string().nullable(),
|
|
44
|
-
steps: z.number(),
|
|
45
|
-
error: z.string().nullable(),
|
|
46
|
-
/**
|
|
47
|
-
* What the run answered, for the runs nobody was watching: `schedule`, `manual` and `task`.
|
|
48
|
-
*
|
|
49
|
-
* ⚠️ A `chat` or `mail` run carries none, and that is a runtime invariant rather than an accident
|
|
50
|
-
* (#268): that answer belongs to the person whose window it was streamed into or to the
|
|
51
|
-
* correspondent it was posted to. The log screen therefore claims nothing at all on those two
|
|
52
|
-
* triggers — an empty state there would read as "the answer went missing".
|
|
53
|
-
*
|
|
54
|
-
* ⚠️ `.nullish()` for the same reason as `usage` below: a runtime from before #268 sends no such
|
|
55
|
-
* field, and a required one would blank the whole Log tab over its absence.
|
|
56
|
-
*/
|
|
57
|
-
answer: z.string().nullish(),
|
|
58
|
-
// ⚠️ `.nullish()` on both, not `.nullable()`. The runtime is a separate deployment and may be a
|
|
59
|
-
// version BEHIND this page: a run list from a runtime without #250/#251 has no such field at all,
|
|
60
|
-
// and a required one would blank the whole Log tab over a missing number.
|
|
61
|
-
usage: AgentUsage.nullish(),
|
|
62
|
-
// Which model did the thinking, as `provider:model`, written into the run rather than looked up:
|
|
63
|
-
// switching the model in the profile must not relabel every past run (#251).
|
|
64
|
-
model: z.string().nullish(),
|
|
65
|
-
});
|
|
66
|
-
export type AgentRun = z.infer<typeof AgentRun>;
|
|
67
|
-
|
|
68
|
-
/** Whether the agent is switched off. Runtime state, never part of the definition (#179). */
|
|
69
|
-
export const AgentState = z.object({
|
|
70
|
-
paused: z.boolean(),
|
|
71
|
-
pausedAt: z.string().nullable(),
|
|
72
|
-
alarmAt: z.number().nullable(),
|
|
73
|
-
});
|
|
74
|
-
export type AgentState = z.infer<typeof AgentState>;
|
|
75
|
-
|
|
76
|
-
/** What "run now" answers: the run exists, and it is on its way. */
|
|
77
|
-
export const AgentManualRun = z.object({
|
|
78
|
-
runId: z.string(),
|
|
79
|
-
status: z.string(),
|
|
80
|
-
});
|
|
81
|
-
export type AgentManualRun = z.infer<typeof AgentManualRun>;
|
|
82
|
-
|
|
83
|
-
export const AgentLogEntry = z.object({
|
|
84
|
-
id: z.string(),
|
|
85
|
-
runId: z.string(),
|
|
86
|
-
at: z.string(),
|
|
87
|
-
level: z.enum(["info", "warn", "error"]),
|
|
88
|
-
event: z.string(),
|
|
89
|
-
detail: z.string(),
|
|
90
|
-
});
|
|
91
|
-
export type AgentLogEntry = z.infer<typeof AgentLogEntry>;
|
|
92
|
-
|
|
93
|
-
export const AgentRunList = z.object({ items: z.array(AgentRun) });
|
|
94
|
-
export type AgentRunList = z.infer<typeof AgentRunList>;
|
|
95
|
-
|
|
96
|
-
export const AgentRunDetail = z.object({ run: AgentRun, log: z.array(AgentLogEntry) });
|
|
97
|
-
export type AgentRunDetail = z.infer<typeof AgentRunDetail>;
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* The runtime's own address, for the one thing that is dialled from outside this page.
|
|
101
|
-
*
|
|
102
|
-
* ⚠️ Only the agent's MCP endpoint is reached here, and only as a string somebody copies. A portal
|
|
103
|
-
* or another agent dials it directly with its own credential (ADR-0005 §7), so it has to be the
|
|
104
|
-
* runtime's real hostname — a deployment that puts the runtime somewhere else sets
|
|
105
|
-
* `VITE_AGENT_RUNTIME_URL` at build time. Empty means "the origin this page came from".
|
|
106
|
-
*
|
|
107
|
-
* ⚠️ NOT the browser's path any more (#178). Everything this page fetches goes through intel, which
|
|
108
|
-
* proxies it to the runtime over a service binding; see `agentRuntimePath` below.
|
|
109
|
-
*/
|
|
110
|
-
export function agentRuntimeBaseUrl(): string {
|
|
111
|
-
return (import.meta.env.VITE_AGENT_RUNTIME_URL ?? "").replace(/\/$/, "");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function agentMcpAddress(agentId: string): string {
|
|
115
|
-
return `${agentRuntimeBaseUrl()}/agents/${encodeURIComponent(agentId)}/mcp`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Where the browser asks, which is intel and nothing else.
|
|
120
|
-
*
|
|
121
|
-
* ⚠️ One door, one origin, one credential: the page carries its intel session cookie, intel opens
|
|
122
|
-
* it, and intel forwards the call to the runtime with the Gate token of the person who asked. That
|
|
123
|
-
* is why there is no CORS here, no second hostname to configure, and above all no Gate token in
|
|
124
|
-
* browser JavaScript — the runtime still demands `agents/run`, it just hears it from intel.
|
|
125
|
-
*
|
|
126
|
-
* The path is relative on purpose. It is joined to intel's `/api/v1` by the data provider, which is
|
|
127
|
-
* the only module that knows where intel is.
|
|
128
|
-
*/
|
|
129
|
-
export function agentRuntimePath(agentId: string, route: string): string {
|
|
130
|
-
return `/agents/${encodeURIComponent(agentId)}${route}`;
|
|
131
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { IntelCapabilities } from "@anchrd/intel-contract";
|
|
2
|
-
import { type UseQueryResult, useQuery } from "@tanstack/react-query";
|
|
3
|
-
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* What this installation is equipped to do, asked once and believed for the session (#190).
|
|
7
|
-
*
|
|
8
|
-
* ⚠️ Deployment facts, not the caller's permissions — a runtime does not appear or vanish while a
|
|
9
|
-
* page is open, so the answer never goes stale and is never refetched. While the answer is still on
|
|
10
|
-
* its way, `data` is `undefined`: a caller deciding whether to OFFER something treats that as "not
|
|
11
|
-
* yet" (a menu item appearing late is fine; one that vanishes was a lie), and a caller deciding
|
|
12
|
-
* whether to take a rendered page AWAY does the opposite.
|
|
13
|
-
*/
|
|
14
|
-
export function useCapabilities(): UseQueryResult<IntelCapabilities> {
|
|
15
|
-
const { data } = useIntelRouterContext();
|
|
16
|
-
return useQuery({
|
|
17
|
-
queryKey: ["capabilities"],
|
|
18
|
-
queryFn: () => data.getCapabilities(),
|
|
19
|
-
staleTime: Number.POSITIVE_INFINITY,
|
|
20
|
-
retry: false,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { ModelCatalog } from "@anchrd/intel-contract";
|
|
2
|
-
import { type UseQueryResult, useQuery } from "@tanstack/react-query";
|
|
3
|
-
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* What the offered models cost and how much they hold, asked of intel once an hour at most (#257).
|
|
7
|
-
*
|
|
8
|
-
* ⚠️ The caching is the point of the hook and not an optimisation. Before this, the figures were a
|
|
9
|
-
* table in the bundle; the ticket's own condition for taking that away is that the replacement is
|
|
10
|
-
* not "one fetch every time somebody opens the select". Prices move a few times a year, so an hour
|
|
11
|
-
* is generous in the direction that matters.
|
|
12
|
-
*
|
|
13
|
-
* ⚠️ `retry: false`, like the tools screen's grouping query (#212). A React Query retry is PAUSED
|
|
14
|
-
* while the tab is unfocused, so a select that waited on a retried query could sit unusable for as
|
|
15
|
-
* long as the reader is looking elsewhere. This query only ENRICHES the select — the model can be
|
|
16
|
-
* chosen without it — so it fails fast and the row simply shows no figures.
|
|
17
|
-
*/
|
|
18
|
-
export function useModelCatalog(): UseQueryResult<ModelCatalog> {
|
|
19
|
-
const { data } = useIntelRouterContext();
|
|
20
|
-
return useQuery({
|
|
21
|
-
queryKey: ["model-catalog"],
|
|
22
|
-
queryFn: () => data.listModels(),
|
|
23
|
-
staleTime: 60 * 60 * 1_000,
|
|
24
|
-
retry: false,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { Check, ChevronsUpDown } from "lucide-react";
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
3
|
-
import {
|
|
4
|
-
Command,
|
|
5
|
-
CommandEmpty,
|
|
6
|
-
CommandGroup,
|
|
7
|
-
CommandInput,
|
|
8
|
-
CommandItem,
|
|
9
|
-
CommandList,
|
|
10
|
-
} from "@/components/ui/command";
|
|
11
|
-
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
12
|
-
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
13
|
-
import { cn } from "@/lib/utils";
|
|
14
|
-
import {
|
|
15
|
-
browserTimezone,
|
|
16
|
-
type TimezoneOption,
|
|
17
|
-
timezoneOption,
|
|
18
|
-
timezoneOptions,
|
|
19
|
-
} from "../timezone.ts";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Picking one of 418 timezones.
|
|
23
|
-
*
|
|
24
|
-
* ⚠️ A `Select` was the wrong shape and this replaces it (#243). Four hundred entries cannot be
|
|
25
|
-
* scrolled to, and a bare zone name does not say whether you got the right one — `Europe/Berlin`
|
|
26
|
-
* and `Europe/Madrid` are the same clock, `America/New_York` is not. So: type to search, and the
|
|
27
|
-
* current offset stands in front of every name.
|
|
28
|
-
*
|
|
29
|
-
* ⚠️ The label is computed here, at render time, and never stored. A zone's offset AND its
|
|
30
|
-
* abbreviation move with daylight saving, so a written-down label is wrong for half of every year.
|
|
31
|
-
*/
|
|
32
|
-
interface TimezoneComboboxProps {
|
|
33
|
-
id: string;
|
|
34
|
-
value: string;
|
|
35
|
-
onChange(zone: string): void;
|
|
36
|
-
/** Handed in rather than read from the clock, so a test does not have to travel in time. */
|
|
37
|
-
now?: Date;
|
|
38
|
-
describedBy?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function OptionLabel({ option }: { option: TimezoneOption }) {
|
|
42
|
-
return (
|
|
43
|
-
<>
|
|
44
|
-
{/* Fixed width, so the zone names line up into a column instead of stepping left and right
|
|
45
|
-
with every offset. Tabular figures keep "GMT+05:30" and "GMT-04:00" the same width, and
|
|
46
|
-
⚠️ `whitespace-nowrap` is not decoration: the longest label ("GMT-09:00 · HADT") wrapped
|
|
47
|
-
onto a second line and pushed its own row out of the column. */}
|
|
48
|
-
<span className="w-36 shrink-0 whitespace-nowrap text-xs tabular-nums text-muted-foreground">
|
|
49
|
-
{option.offsetLabel}
|
|
50
|
-
{option.abbreviation ? ` · ${option.abbreviation}` : ""}
|
|
51
|
-
</span>
|
|
52
|
-
<span className="truncate">{option.zone.replace(/_/g, " ")}</span>
|
|
53
|
-
</>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function TimezoneCombobox({ id, value, onChange, now, describedBy }: TimezoneComboboxProps) {
|
|
58
|
-
const i18n = useI18n();
|
|
59
|
-
const [open, setOpen] = useState(false);
|
|
60
|
-
const at = now ?? new Date();
|
|
61
|
-
// The clock is a dependency on purpose: without it in the key, a session that crossed a DST
|
|
62
|
-
// boundary would keep showing yesterday's offsets.
|
|
63
|
-
const at_ = at.getTime();
|
|
64
|
-
|
|
65
|
-
const options = useMemo(
|
|
66
|
-
() => timezoneOptions(value, new Date(at_), i18n.locale),
|
|
67
|
-
[value, at_, i18n.locale],
|
|
68
|
-
);
|
|
69
|
-
const selected = useMemo(
|
|
70
|
-
() => timezoneOption(value, new Date(at_), i18n.locale),
|
|
71
|
-
[value, at_, i18n.locale],
|
|
72
|
-
);
|
|
73
|
-
// The reader's own zone, pinned above the list: it is the answer in the overwhelming majority of
|
|
74
|
-
// cases, and finding it in a sorted list of 418 is the work this saves.
|
|
75
|
-
const own = useMemo(
|
|
76
|
-
() => timezoneOption(browserTimezone(), new Date(at_), i18n.locale),
|
|
77
|
-
[at_, i18n.locale],
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
const choose = (zone: string) => {
|
|
81
|
-
onChange(zone);
|
|
82
|
-
setOpen(false);
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
87
|
-
<PopoverTrigger asChild>
|
|
88
|
-
<button
|
|
89
|
-
id={id}
|
|
90
|
-
type="button"
|
|
91
|
-
role="combobox"
|
|
92
|
-
aria-expanded={open}
|
|
93
|
-
aria-describedby={describedBy}
|
|
94
|
-
className="flex w-full items-center gap-2 rounded-md border bg-background px-3 py-2 text-left text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
95
|
-
>
|
|
96
|
-
<OptionLabel option={selected} />
|
|
97
|
-
<ChevronsUpDown aria-hidden="true" className="ml-auto size-4 shrink-0 opacity-50" />
|
|
98
|
-
</button>
|
|
99
|
-
</PopoverTrigger>
|
|
100
|
-
<PopoverContent className="w-(--radix-popover-trigger-width) p-0" align="start">
|
|
101
|
-
{/* ⚠️ A substring filter, not cmdk's default fuzzy one. Fuzzy matches a scattered
|
|
102
|
-
subsequence, so over 418 zones "berlin" also surfaced Brisbane, Dublin and Lisbon —
|
|
103
|
-
technically a match, useless as an answer. Prefix beats contains, so typing a city puts
|
|
104
|
-
that city first. An empty search returns 1 for everything, which leaves the list in DOM
|
|
105
|
-
order: sorted west to east. */}
|
|
106
|
-
<Command
|
|
107
|
-
filter={(value, search, keywords) => {
|
|
108
|
-
const needle = search.trim().toLowerCase();
|
|
109
|
-
if (!needle) return 1;
|
|
110
|
-
const haystack = [value.toLowerCase(), ...(keywords ?? [])];
|
|
111
|
-
if (haystack.some((word) => word.startsWith(needle))) return 2;
|
|
112
|
-
return haystack.some((word) => word.includes(needle)) ? 1 : 0;
|
|
113
|
-
}}
|
|
114
|
-
>
|
|
115
|
-
<CommandInput placeholder={i18n.t("settings.timezoneSearch")} />
|
|
116
|
-
<CommandList>
|
|
117
|
-
<CommandEmpty>{i18n.t("settings.timezoneNoMatch")}</CommandEmpty>
|
|
118
|
-
{own.zone !== value ? (
|
|
119
|
-
<CommandGroup heading={i18n.t("settings.timezoneOwn")}>
|
|
120
|
-
<CommandItem value={own.zone} keywords={own.keywords} onSelect={choose}>
|
|
121
|
-
<OptionLabel option={own} />
|
|
122
|
-
</CommandItem>
|
|
123
|
-
</CommandGroup>
|
|
124
|
-
) : null}
|
|
125
|
-
<CommandGroup heading={i18n.t("settings.timezoneAll")}>
|
|
126
|
-
{options.map((option) => (
|
|
127
|
-
<CommandItem
|
|
128
|
-
key={option.zone}
|
|
129
|
-
value={option.zone}
|
|
130
|
-
keywords={option.keywords}
|
|
131
|
-
onSelect={choose}
|
|
132
|
-
>
|
|
133
|
-
<OptionLabel option={option} />
|
|
134
|
-
<Check
|
|
135
|
-
aria-hidden="true"
|
|
136
|
-
className={cn(
|
|
137
|
-
"ml-auto size-4 shrink-0",
|
|
138
|
-
option.zone === value ? "opacity-100" : "opacity-0",
|
|
139
|
-
)}
|
|
140
|
-
/>
|
|
141
|
-
</CommandItem>
|
|
142
|
-
))}
|
|
143
|
-
</CommandGroup>
|
|
144
|
-
</CommandList>
|
|
145
|
-
</Command>
|
|
146
|
-
</PopoverContent>
|
|
147
|
-
</Popover>
|
|
148
|
-
);
|
|
149
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { createContext, type ReactNode, useContext, useMemo, useState } from "react";
|
|
2
|
-
import { browserTimezone, readChosenTimezone, rememberTimezone } from "./timezone.ts";
|
|
3
|
-
|
|
4
|
-
// The React seam of the timezone preference, the same shape as i18n-context and theme-context.
|
|
5
|
-
|
|
6
|
-
export interface TimezoneSelection {
|
|
7
|
-
/** The zone to read times in: the choice, or the browser's own where there is none. */
|
|
8
|
-
current: string;
|
|
9
|
-
/**
|
|
10
|
-
* ⚠️ The zone somebody actually CHOSE — `null` while they never did (#256). `current` cannot
|
|
11
|
-
* answer this: it falls back to the browser, and a screen that treated that as a preference would
|
|
12
|
-
* annotate every schedule with a guess nobody made.
|
|
13
|
-
*/
|
|
14
|
-
chosen: string | null;
|
|
15
|
-
select(zone: string): void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const TimezoneContext = createContext<TimezoneSelection | null>(null);
|
|
19
|
-
|
|
20
|
-
interface TimezoneProviderProps {
|
|
21
|
-
store?: Pick<Storage, "getItem" | "setItem">;
|
|
22
|
-
children: ReactNode;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function TimezoneProvider({ store, children }: TimezoneProviderProps) {
|
|
26
|
-
const [chosen, setChosen] = useState(() => readChosenTimezone(store));
|
|
27
|
-
|
|
28
|
-
const selection = useMemo<TimezoneSelection>(
|
|
29
|
-
() => ({
|
|
30
|
-
current: chosen ?? browserTimezone(),
|
|
31
|
-
chosen,
|
|
32
|
-
select: (zone) => {
|
|
33
|
-
rememberTimezone(zone, store);
|
|
34
|
-
setChosen(zone);
|
|
35
|
-
},
|
|
36
|
-
}),
|
|
37
|
-
[chosen, store],
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
return <TimezoneContext value={selection}>{children}</TimezoneContext>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// `null` without a provider — the settings dialog asks whether there is anything to choose.
|
|
44
|
-
export function useTimezoneSelection(): TimezoneSelection | null {
|
|
45
|
-
return useContext(TimezoneContext);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// The zone to read times in. Never null: a component without a provider still has to format a date,
|
|
49
|
-
// and the browser's own zone is the honest answer there.
|
|
50
|
-
export function useTimezone(): string {
|
|
51
|
-
const selection = useContext(TimezoneContext);
|
|
52
|
-
return selection?.current ?? browserTimezone();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The zone the reader CHOSE, or `null` (#256).
|
|
57
|
-
*
|
|
58
|
-
* ⚠️ Not `useTimezone`. This is for screens that add the reader's own reading BESIDE a time that is
|
|
59
|
-
* already correct — the agent's calendar. Nothing may be added for somebody who never set a zone:
|
|
60
|
-
* the browser's guess presented as "your time" is a setting the reader did not make, and it would
|
|
61
|
-
* put a second time on every row of a screen that was fine as it was.
|
|
62
|
-
*/
|
|
63
|
-
export function useChosenTimezone(): string | null {
|
|
64
|
-
return useContext(TimezoneContext)?.chosen ?? null;
|
|
65
|
-
}
|