@anchrd/intel-ui 0.18.0 → 0.20.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 +68 -147
- package/src/app/header-search/header-search.tsx +2 -1
- package/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
- package/src/app/settings-dialog/settings-dialog.tsx +10 -32
- package/src/app/user-footer/user-footer.tsx +19 -0
- package/src/archive/archive.tsx +2 -1
- package/src/data/intel-data-provider/intel-data-provider.ts +42 -191
- package/src/data/intel-data-provider/intel-data-provider.types.ts +36 -120
- package/src/document-link/document-link.tsx +1 -1
- package/src/entry-picker/entry-picker.tsx +7 -7
- package/src/flow-runs/flow-runs.tsx +1 -1
- package/src/flows/flows.tsx +3 -2
- package/src/flows/node-icon/node-icon.ts +1 -1
- package/src/flows/node-palette/node-palette.tsx +1 -1
- package/src/flows/node-palette/node-palette.types.ts +1 -1
- package/src/graph-pane/graph-pane.tsx +1 -1
- package/src/i18n/de.json +14 -217
- package/src/i18n/en.json +14 -217
- package/src/i18n/es.json +14 -217
- 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-editor/node-editor.tsx +1 -1
- package/src/node-graph/graph-notice.tsx +1 -1
- package/src/node-graph/node-graph.ts +1 -1
- package/src/node-graph/node-graph.tsx +1 -1
- package/src/node-table/node-table.tsx +3 -3
- package/src/nodes/nodes.tsx +1 -35
- package/src/resource-menu/resource-menu.tsx +45 -21
- package/src/section-hint/section-hint.tsx +2 -2
- package/src/title-row/title-row.tsx +5 -22
- package/src/tools/tools.tsx +1 -3
- 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 -160
- 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 -1006
- 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 -507
- 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 -137
- package/src/board/board-kanban/board-kanban.tsx +0 -257
- 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 -282
- 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,282 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Announcements,
|
|
3
|
-
closestCorners,
|
|
4
|
-
DndContext,
|
|
5
|
-
type DraggableAttributes,
|
|
6
|
-
DragOverlay,
|
|
7
|
-
type DragStartEvent,
|
|
8
|
-
KeyboardSensor,
|
|
9
|
-
PointerSensor,
|
|
10
|
-
useDroppable,
|
|
11
|
-
useSensor,
|
|
12
|
-
useSensors,
|
|
13
|
-
} from "@dnd-kit/core";
|
|
14
|
-
import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
15
|
-
import { CSS } from "@dnd-kit/utilities";
|
|
16
|
-
import { GripVertical } from "lucide-react";
|
|
17
|
-
import { type ReactNode, useState } from "react";
|
|
18
|
-
import { createPortal } from "react-dom";
|
|
19
|
-
import { cn } from "@/lib/utils";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* A kanban board on dnd-kit, from the Kibo UI registry (MIT) and changed where it had to be.
|
|
23
|
-
*
|
|
24
|
-
* Registry code is our code, and four things about the original do not survive contact with a board
|
|
25
|
-
* whose truth is on a server:
|
|
26
|
-
*
|
|
27
|
-
* ⚠️ **It reported a drop as a new ARRAY, and it reported one twice.** The original called
|
|
28
|
-
* `onDataChange` in `onDragOver` for a cross-column move and again in `onDragEnd`, and both times
|
|
29
|
-
* with a reordered copy of the whole list. Against a server that is two writes for one drag — and
|
|
30
|
-
* the second one describes a state the first already changed. This version says nothing while the
|
|
31
|
-
* card is in the air and emits ONE drop at the end, naming what was dropped and what it was dropped
|
|
32
|
-
* on. What that means is the caller's to decide (`board-kanban.ts`), which is also where the single
|
|
33
|
-
* `board_task_move` is sent from.
|
|
34
|
-
*
|
|
35
|
-
* ⚠️ **It mutated its own props** — `newData[activeIndex].column = overColumn` writes into the
|
|
36
|
-
* object the caller passed in. With TanStack Query holding that object, a drag edited the cache in
|
|
37
|
-
* place and no re-render followed.
|
|
38
|
-
*
|
|
39
|
-
* ⚠️ **`tunnel-rat` is gone.** It existed to render the dragged card into the overlay from inside
|
|
40
|
-
* the card component; `DragOverlay` already does that, and the caller draws the overlay itself.
|
|
41
|
-
* One dependency fewer for something React has a portal for.
|
|
42
|
-
*
|
|
43
|
-
* ⚠️ **The screen reader announcements are handed in.** The originals are English string literals
|
|
44
|
-
* in the component; Intel speaks three languages and its text comes from `useI18n()`.
|
|
45
|
-
*
|
|
46
|
-
* ⚠️ **The card is not itself a `role="button"`, and it holds exactly ONE tab stop.** The original
|
|
47
|
-
* spread `useSortable().attributes` onto the card, which contributes `role="button"` and
|
|
48
|
-
* `tabIndex={0}` — and a card that also has to be OPENED then holds a real button inside that one:
|
|
49
|
-
* two tab stops computing the same accessible name from the same contents, and an Enter that means
|
|
50
|
-
* "start dragging" outside and "open this" inside (anchrd/intel#286). This version keeps the
|
|
51
|
-
* `listeners` on the card so a pointer still drags it from anywhere, and hands the ACTIVATOR to the
|
|
52
|
-
* caller's own control instead of minting a second button beside it (anchrd/intel#315). What
|
|
53
|
-
* separates the two meanings is then the key, not the element: see `KanbanProvider`.
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
export interface KanbanColumn {
|
|
57
|
-
id: string;
|
|
58
|
-
name: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface KanbanDrop {
|
|
62
|
-
activeId: string;
|
|
63
|
-
// The column or the card it was let go over. `null` means it was let go over nothing — a drag
|
|
64
|
-
// that ended outside the board, which is a cancel and not a move.
|
|
65
|
-
overId: string | null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function KanbanBoard({
|
|
69
|
-
id,
|
|
70
|
-
droppable = true,
|
|
71
|
-
children,
|
|
72
|
-
className,
|
|
73
|
-
}: {
|
|
74
|
-
id: string;
|
|
75
|
-
// A column that exists to be READ but must not be written into. It still draws and still holds
|
|
76
|
-
// cards; it simply never becomes a drop target, so nothing can be dropped where the answer would
|
|
77
|
-
// be a refusal (`board-kanban.tsx`).
|
|
78
|
-
droppable?: boolean;
|
|
79
|
-
children: ReactNode;
|
|
80
|
-
className?: string;
|
|
81
|
-
}) {
|
|
82
|
-
const { isOver, setNodeRef } = useDroppable({ id, disabled: !droppable });
|
|
83
|
-
return (
|
|
84
|
-
<div
|
|
85
|
-
ref={setNodeRef}
|
|
86
|
-
data-slot="kanban-column"
|
|
87
|
-
data-droppable={droppable}
|
|
88
|
-
className={cn(
|
|
89
|
-
"flex size-full min-h-40 flex-col divide-y overflow-hidden rounded-lg border bg-muted/40 text-xs ring-2 transition-colors",
|
|
90
|
-
isOver ? "ring-ring" : "ring-transparent",
|
|
91
|
-
className,
|
|
92
|
-
)}
|
|
93
|
-
>
|
|
94
|
-
{children}
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function KanbanHeader({ children }: { children: ReactNode }) {
|
|
100
|
-
return <div className="flex items-center gap-2 p-2 text-sm font-semibold">{children}</div>;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function KanbanCards({ id, children }: { id: string; children: ReactNode }) {
|
|
104
|
-
return (
|
|
105
|
-
<div
|
|
106
|
-
// The column scrolls, not the page: four lanes of different lengths must not make the board
|
|
107
|
-
// as tall as its longest one.
|
|
108
|
-
className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto p-2"
|
|
109
|
-
data-slot="kanban-cards"
|
|
110
|
-
data-column={id}
|
|
111
|
-
>
|
|
112
|
-
{children}
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* What a card hands to the one control inside it: the activator ref and dnd-kit's ARIA attributes.
|
|
119
|
-
*
|
|
120
|
-
* ⚠️ It is spread onto a control the CALLER renders, which is the whole point — the alternative is
|
|
121
|
-
* a handle of the primitive's own beside it, and that is the second tab stop anchrd/intel#315 is
|
|
122
|
-
* about. `role` and `tabIndex` are in here and are what a native `<button>` already is; what is not
|
|
123
|
-
* already there is `aria-roledescription` and the `aria-describedby` pointing at dnd-kit's keyboard
|
|
124
|
-
* instructions, and those are the half a reader needs to be told the control can also be picked up.
|
|
125
|
-
*/
|
|
126
|
-
export type KanbanCardControl = DraggableAttributes & {
|
|
127
|
-
ref: (node: HTMLElement | null) => void;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
export function KanbanCard({
|
|
131
|
-
id,
|
|
132
|
-
roleDescription,
|
|
133
|
-
children,
|
|
134
|
-
className,
|
|
135
|
-
}: {
|
|
136
|
-
id: string;
|
|
137
|
-
// What a reader is told this card IS, past its accessible name — dnd-kit's `aria-roledescription`,
|
|
138
|
-
// whose own default is the English literal "draggable". Handed in because text comes from
|
|
139
|
-
// `useI18n()` and only the caller has it.
|
|
140
|
-
roleDescription: string;
|
|
141
|
-
// A function, not a node: the one control in a card has to receive `control`, and a primitive that
|
|
142
|
-
// rendered its own control instead would be back to two tab stops.
|
|
143
|
-
children: (control: KanbanCardControl) => ReactNode;
|
|
144
|
-
className?: string;
|
|
145
|
-
}) {
|
|
146
|
-
const {
|
|
147
|
-
attributes,
|
|
148
|
-
listeners,
|
|
149
|
-
setNodeRef,
|
|
150
|
-
setActivatorNodeRef,
|
|
151
|
-
transition,
|
|
152
|
-
transform,
|
|
153
|
-
isDragging,
|
|
154
|
-
} = useSortable({ id, attributes: { roleDescription } });
|
|
155
|
-
return (
|
|
156
|
-
<div
|
|
157
|
-
ref={setNodeRef}
|
|
158
|
-
style={{ transition, transform: CSS.Transform.toString(transform) }}
|
|
159
|
-
// ⚠️ `listeners` here, `attributes` NOT. The pointer sensor has to see a press anywhere on the
|
|
160
|
-
// card — a kanban whose cards can only be dragged by a grip is a kanban nobody drags — but
|
|
161
|
-
// `attributes` carries `role="button"` and `tabIndex`, and those on the card would wrap the
|
|
162
|
-
// control that OPENS it in a second button.
|
|
163
|
-
//
|
|
164
|
-
// ⚠️ Handing the activator down only holds because something claims to BE it. dnd-kit's
|
|
165
|
-
// `KeyboardSensor` opens with `if (activator && event.target !== activator) return false` — a
|
|
166
|
-
// guard that does not exist until `setActivatorNodeRef` has been given a node. Unclaimed, the
|
|
167
|
-
// keyboard sensor accepts a key press from anywhere under these listeners, which is how a card
|
|
168
|
-
// ended up impossible to open by keyboard (anchrd/intel#286). The pointer drag is unaffected
|
|
169
|
-
// either way: `PointerSensor` never consults the activator node.
|
|
170
|
-
{...listeners}
|
|
171
|
-
data-slot="kanban-card"
|
|
172
|
-
className={cn(
|
|
173
|
-
"flex cursor-grab items-start gap-1 rounded-lg border bg-card p-3 text-left shadow-sm",
|
|
174
|
-
// Left where it was, faded, rather than removed: a lane that reflows while a card is over
|
|
175
|
-
// it moves the drop target out from under the pointer.
|
|
176
|
-
isDragging && "opacity-40",
|
|
177
|
-
className,
|
|
178
|
-
)}
|
|
179
|
-
>
|
|
180
|
-
{/* Decoration, and deliberately not a control: it says "this can be taken hold of" to whoever
|
|
181
|
-
uses a pointer, and a pointer can take hold of the whole card anyway. As a button it was a
|
|
182
|
-
tab stop of its own on every card in the lane. */}
|
|
183
|
-
<GripVertical aria-hidden="true" className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
|
184
|
-
<div className="min-w-0 flex-1">{children({ ...attributes, ref: setActivatorNodeRef })}</div>
|
|
185
|
-
</div>
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* ⚠️ Space picks a card up, Enter does not — and that is the whole of anchrd/intel#315.
|
|
191
|
-
*
|
|
192
|
-
* dnd-kit's default is `start: ['Space', 'Enter']`, which forces the activator to be an element of
|
|
193
|
-
* its own: a control that is also the card's "open" button cannot answer both keys with two
|
|
194
|
-
* different things. With the start key narrowed to Space, the two meanings are told apart by the
|
|
195
|
-
* KEY instead of by the element, and a card costs one tab stop rather than two — fifty cards, fifty
|
|
196
|
-
* stops instead of a hundred.
|
|
197
|
-
*
|
|
198
|
-
* ⚠️ `start` is the ONLY list that is narrowed. `end` and `cancel` are dnd-kit's defaults spelled
|
|
199
|
-
* out — Space, Enter and **Tab** put a card down, Escape leaves it where it was — and dropping one
|
|
200
|
-
* of them here is not a smaller change than it looks. Tab is neither an end key nor a cancel key
|
|
201
|
-
* once it is missing from this list, and `defaultKeyboardCoordinateGetter` answers nothing for it,
|
|
202
|
-
* so nothing calls `preventDefault()`: the focus walks on to the next card while the first one is
|
|
203
|
-
* still in the air and still following the arrow keys.
|
|
204
|
-
*
|
|
205
|
-
* Enter stays an END key, and it cannot open the card while ending a move: `handleEnd` calls
|
|
206
|
-
* `preventDefault()` before the browser turns the key press into a click. Somebody holding a card
|
|
207
|
-
* who presses the key they associate with "yes" should put it down, not be told nothing happened.
|
|
208
|
-
*/
|
|
209
|
-
const kanbanKeyboardCodes = {
|
|
210
|
-
start: ["Space"],
|
|
211
|
-
cancel: ["Escape"],
|
|
212
|
-
end: ["Space", "Enter", "Tab"],
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
export function KanbanProvider({
|
|
216
|
-
columns,
|
|
217
|
-
itemsByColumn,
|
|
218
|
-
children,
|
|
219
|
-
announcements,
|
|
220
|
-
instructions,
|
|
221
|
-
overlay,
|
|
222
|
-
onDrop,
|
|
223
|
-
className,
|
|
224
|
-
}: {
|
|
225
|
-
columns: KanbanColumn[];
|
|
226
|
-
// Which cards stand in which column, in the order they are drawn. The provider needs it for the
|
|
227
|
-
// sortable contexts and asks for it rather than deriving it, so the caller stays the only place
|
|
228
|
-
// that knows how a board is ordered.
|
|
229
|
-
itemsByColumn: Map<string, string[]>;
|
|
230
|
-
children: (column: KanbanColumn) => ReactNode;
|
|
231
|
-
announcements: Announcements;
|
|
232
|
-
// ⚠️ Not optional, and not a nicety. dnd-kit's built-in text is an English literal naming the
|
|
233
|
-
// default keys, and this board answers different ones. A card whose `aria-describedby` says
|
|
234
|
-
// "press space or enter" while Enter opens it describes a widget that is not there.
|
|
235
|
-
instructions: string;
|
|
236
|
-
overlay(activeId: string): ReactNode;
|
|
237
|
-
onDrop(drop: KanbanDrop): void;
|
|
238
|
-
className?: string;
|
|
239
|
-
}) {
|
|
240
|
-
const [activeId, setActiveId] = useState<string | null>(null);
|
|
241
|
-
const sensors = useSensors(
|
|
242
|
-
// ⚠️ A distance before a drag starts, and it is not decoration: without it every click on a
|
|
243
|
-
// card is a drag of zero pixels, and the card can then no longer be opened by clicking it.
|
|
244
|
-
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
245
|
-
useSensor(KeyboardSensor, { keyboardCodes: kanbanKeyboardCodes }),
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
return (
|
|
249
|
-
<DndContext
|
|
250
|
-
accessibility={{ announcements, screenReaderInstructions: { draggable: instructions } }}
|
|
251
|
-
// Corners rather than centres: a tall card over a short one wins on centre distance even
|
|
252
|
-
// when the pointer is nowhere near it.
|
|
253
|
-
collisionDetection={closestCorners}
|
|
254
|
-
sensors={sensors}
|
|
255
|
-
onDragStart={(event: DragStartEvent) => setActiveId(String(event.active.id))}
|
|
256
|
-
onDragCancel={() => setActiveId(null)}
|
|
257
|
-
onDragEnd={(event) => {
|
|
258
|
-
setActiveId(null);
|
|
259
|
-
const over = event.over === null ? null : String(event.over.id);
|
|
260
|
-
onDrop({ activeId: String(event.active.id), overId: over });
|
|
261
|
-
}}
|
|
262
|
-
>
|
|
263
|
-
<div className={cn("grid size-full auto-cols-fr grid-flow-col gap-3", className)}>
|
|
264
|
-
{columns.map((column) => (
|
|
265
|
-
<SortableContext
|
|
266
|
-
key={column.id}
|
|
267
|
-
items={itemsByColumn.get(column.id) ?? []}
|
|
268
|
-
strategy={verticalListSortingStrategy}
|
|
269
|
-
>
|
|
270
|
-
{children(column)}
|
|
271
|
-
</SortableContext>
|
|
272
|
-
))}
|
|
273
|
-
</div>
|
|
274
|
-
{typeof document === "undefined"
|
|
275
|
-
? null
|
|
276
|
-
: createPortal(
|
|
277
|
-
<DragOverlay>{activeId === null ? null : overlay(activeId)}</DragOverlay>,
|
|
278
|
-
document.body,
|
|
279
|
-
)}
|
|
280
|
-
</DndContext>
|
|
281
|
-
);
|
|
282
|
-
}
|
|
@@ -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
|
-
}
|