@lotics/ui 20.0.1 → 20.1.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/LICENSE.md +39 -0
- package/docs/ai_patterns.md +10 -5
- package/package.json +5 -3
- package/src/agent_progress.tsx +9 -1
- package/src/agent_run.tsx +47 -27
- package/src/locale.tsx +79 -5
- package/src/agent_transform.test.ts +0 -96
- package/src/calendar/layout.test.ts +0 -103
- package/src/colors.test.ts +0 -45
- package/src/date_filter_presets.test.ts +0 -62
- package/src/date_picker_value.test.ts +0 -204
- package/src/date_range_selection.test.ts +0 -60
- package/src/date_segments.test.ts +0 -335
- package/src/file_badge_fit.test.ts +0 -49
- package/src/file_intake.test.ts +0 -124
- package/src/format_date.test.ts +0 -138
- package/src/format_date_negative_zone.test.ts +0 -26
- package/src/gantt/scale.test.ts +0 -47
- package/src/inline_focus.test.ts +0 -49
- package/src/matrix.test.ts +0 -54
- package/src/pending_commits.test.ts +0 -68
- package/src/popover_layers.test.ts +0 -113
- package/src/primitives_purity.test.ts +0 -73
- package/src/remark_gfm_safe.test.ts +0 -33
- package/src/share_or_download.test.ts +0 -92
- package/src/table.test.ts +0 -90
- package/src/task_metrics.test.ts +0 -48
- package/src/use_list_keyboard_nav.test.ts +0 -184
- package/src/use_option_list.test.ts +0 -232
- package/src/use_section_nav.test.ts +0 -171
- package/src/use_selection.test.ts +0 -66
- package/src/vite.test.ts +0 -41
package/LICENSE.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Copyright (c) 2026 Lotics Technology Company Limited. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software is proprietary. It is published to the public npm registry for one
|
|
4
|
+
purpose: building on the Lotics platform.
|
|
5
|
+
|
|
6
|
+
In this license, **Lotics** means Lotics Technology Company Limited, and **the
|
|
7
|
+
Lotics platform** means the hosted services Lotics operates at lotics.ai and its
|
|
8
|
+
subdomains, together with applications, packages and integrations that run on or
|
|
9
|
+
communicate with those services.
|
|
10
|
+
|
|
11
|
+
## Permitted
|
|
12
|
+
|
|
13
|
+
You may install this package and use it **solely in connection with the Lotics
|
|
14
|
+
platform** — including bundling it, in whole or in part, into an application that
|
|
15
|
+
runs on or integrates with the Lotics platform.
|
|
16
|
+
|
|
17
|
+
## Not permitted
|
|
18
|
+
|
|
19
|
+
Without prior written permission from Lotics you may not:
|
|
20
|
+
|
|
21
|
+
1. use this software, or any portion of it, independently of the Lotics platform
|
|
22
|
+
— including in any application, product or service that neither runs on nor
|
|
23
|
+
integrates with the Lotics platform;
|
|
24
|
+
2. copy, republish, redistribute or sublicense the source code, in whole or in
|
|
25
|
+
part, other than as the bundled dependency described above;
|
|
26
|
+
3. create derivative works from the source code, or incorporate any portion of it
|
|
27
|
+
into another library, product or package;
|
|
28
|
+
4. reverse engineer, decompile or disassemble any compiled artifact, except to the
|
|
29
|
+
extent that restriction is unenforceable under applicable law;
|
|
30
|
+
5. use the source code, or any portion of it, to train or fine-tune a machine
|
|
31
|
+
learning model.
|
|
32
|
+
|
|
33
|
+
## No warranty
|
|
34
|
+
|
|
35
|
+
This software is provided "as is", without warranty of any kind, express or
|
|
36
|
+
implied. In no event shall Lotics be liable for any claim, damages or other
|
|
37
|
+
liability arising from the software or its use.
|
|
38
|
+
|
|
39
|
+
For any other use, contact minh@lotics.ai.
|
package/docs/ai_patterns.md
CHANGED
|
@@ -157,9 +157,10 @@ PUT and rolls the calls out BELOW it on press. The run ALWAYS ends on the agent'
|
|
|
157
157
|
no global terminal node.
|
|
158
158
|
|
|
159
159
|
**Tool labels.** A tool part carries the RAW tool name (`update_records` from `tool-update_records`,
|
|
160
|
-
or a `dynamic-tool`'s `toolName`), resolved
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
or a `dynamic-tool`'s `toolName`), resolved through the active locale's **`agentRun.tools`** map —
|
|
161
|
+
so a `vi` provider renders Vietnamese tool rows with no per-app wiring, the same way the rest of the
|
|
162
|
+
feed's chrome already resolved. An optional **`labelForCall`** override still wins, for phrasing a
|
|
163
|
+
call by what it TARGETS rather than by which tool ran. It gets the whole call — `{ toolName, input, state }` (`AgentToolCall`) — so a caller can phrase by what the
|
|
163
164
|
call targets or by whether it's awaiting; return `undefined` to fall back. Unknown names fall back
|
|
164
165
|
to a prettified form. `stepsLabel` localizes the "{n} steps" suffix. The tool `state` folds ai's
|
|
165
166
|
7-state lifecycle to **running / awaiting / done / error**: `approval-requested` → `awaiting` (the
|
|
@@ -206,8 +207,12 @@ feed**; the starting row IS the placeholder, consistent and localized.
|
|
|
206
207
|
**Localization.** The feed's fixed chrome — the "Thinking" label, the "Input"/"Output"/"Error"
|
|
207
208
|
panel titles, the "awaiting" annotation, and the "Retry" action — resolves through the
|
|
208
209
|
`LoticsLocale` `agentRun` slice (translate once at the provider; a `vi` app gets them for free).
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
— **and so are the tool labels** (`agentRun.tools`, keyed by raw tool name). Translating a tool
|
|
211
|
+
row is no longer each app's job: an English row under a Vietnamese "Đang suy nghĩ…" was the kit
|
|
212
|
+
contradicting itself. The "{n} steps" suffix is locale-backed too (`agentRun.steps`). `labelForCall`
|
|
213
|
+
and `stepsLabel` remain as per-call-site OVERRIDES — for phrasing a row by what
|
|
214
|
+
it targets — not as the only route to a translation. A tool name in neither the locale map nor
|
|
215
|
+
`labelForCall` falls back to its prettified form.
|
|
211
216
|
|
|
212
217
|
**Answering an awaiting call — `ApprovalPrompt`.** The `awaiting` row inside `AgentRun` is
|
|
213
218
|
READ-ONLY — it only NAMES that a call is parked on a human decision. The surface that ANSWERS it
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "20.0
|
|
3
|
+
"version": "20.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./vite": {
|
|
@@ -248,7 +248,9 @@
|
|
|
248
248
|
"examples",
|
|
249
249
|
"AGENTS.md",
|
|
250
250
|
"MIGRATION.md",
|
|
251
|
-
"docs"
|
|
251
|
+
"docs",
|
|
252
|
+
"!src/**/*.test.ts",
|
|
253
|
+
"!src/**/*.test.tsx"
|
|
252
254
|
],
|
|
253
255
|
"publishConfig": {
|
|
254
256
|
"access": "public"
|
|
@@ -258,7 +260,7 @@
|
|
|
258
260
|
"url": "https://github.com/lotics/lotics.git",
|
|
259
261
|
"directory": "packages/ui"
|
|
260
262
|
},
|
|
261
|
-
"license": "
|
|
263
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
262
264
|
"dependencies": {
|
|
263
265
|
"@lotics/docx": "^0.1.0",
|
|
264
266
|
"@lotics/xlsx": "^0.1.0",
|
package/src/agent_progress.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { WaveAvatar } from "./wave_avatar";
|
|
|
6
6
|
import { FollowScroll } from "./follow_scroll";
|
|
7
7
|
import { PressableHighlight } from "./pressable_highlight";
|
|
8
8
|
import { AgentRun, resolveToolMeta, type AgentRunProps, type AgentToolCall } from "./agent_run";
|
|
9
|
+
import { useLoticsLocale } from "./locale";
|
|
9
10
|
import { toSegments, lastRunningStep, type AgentUIPart } from "./agent_transform";
|
|
10
11
|
|
|
11
12
|
export interface AgentProgressProps {
|
|
@@ -43,7 +44,14 @@ export function AgentProgress(props: AgentProgressProps) {
|
|
|
43
44
|
const [expanded, setExpanded] = useState(defaultExpanded ?? false);
|
|
44
45
|
|
|
45
46
|
const running = lastRunningStep(toSegments(parts));
|
|
46
|
-
const
|
|
47
|
+
const locale = useLoticsLocale();
|
|
48
|
+
const runningLabel = running
|
|
49
|
+
? resolveToolMeta(
|
|
50
|
+
{ toolName: running.toolName, input: running.input, state: running.status },
|
|
51
|
+
labelForCall,
|
|
52
|
+
locale.agentRun.tools,
|
|
53
|
+
).label
|
|
54
|
+
: undefined;
|
|
47
55
|
const compact = label ?? runningLabel ?? (state === "done" ? "Done" : state === "error" ? "Stopped" : "Working…");
|
|
48
56
|
const streaming = state === "streaming";
|
|
49
57
|
|
package/src/agent_run.tsx
CHANGED
|
@@ -69,19 +69,28 @@ export interface AgentRunProps {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// ── Tool → display meta ───────────────────────────────────────────────────────
|
|
72
|
-
// The
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
// The ICON per platform tool. The LABEL lives in the locale (`agentRun.tools`)
|
|
73
|
+
// because it is language, and this component already renders localized chrome
|
|
74
|
+
// around these rows — an English tool row under a Vietnamese "Đang suy nghĩ…"
|
|
75
|
+
// was the kit contradicting itself. An icon is not language, so it stays here.
|
|
76
|
+
// A tool neither map knows falls back to a prettified name + a neutral icon.
|
|
77
|
+
const TOOL_ICONS: Record<string, IconName> = {
|
|
78
|
+
query_records: "search",
|
|
79
|
+
get_record: "file-text",
|
|
80
|
+
get_records: "file-text",
|
|
81
|
+
create_records: "plus",
|
|
82
|
+
update_records: "square-pen",
|
|
83
|
+
delete_records: "trash",
|
|
84
|
+
run_app_query: "search",
|
|
85
|
+
run_app_workflow: "play",
|
|
86
|
+
grep_knowledge: "search",
|
|
87
|
+
read_knowledge: "book-open",
|
|
88
|
+
list_knowledge: "list",
|
|
89
|
+
view_files: "file-text",
|
|
90
|
+
ask_user_choice: "message-circle-question-mark",
|
|
91
|
+
generate_pdf_from_template: "file-down",
|
|
92
|
+
generate_excel_from_template: "file-spreadsheet",
|
|
93
|
+
generate_docx_from_template: "file-text",
|
|
85
94
|
};
|
|
86
95
|
|
|
87
96
|
function prettifyToolName(name: string): string {
|
|
@@ -90,26 +99,37 @@ function prettifyToolName(name: string): string {
|
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
/** Resolve a tool's display label + icon: a caller override wins on the label,
|
|
93
|
-
* then the
|
|
94
|
-
* surfaces (e.g. `AgentProgress`) render the same
|
|
102
|
+
* then the active locale's `agentRun.tools`, then a prettified fallback.
|
|
103
|
+
* Exported so other agent surfaces (e.g. `AgentProgress`) render the same
|
|
104
|
+
* labels; `toolLabels` is optional so an existing external caller keeps
|
|
105
|
+
* working (it just falls back to the prettified name). */
|
|
95
106
|
export function resolveToolMeta(
|
|
96
107
|
call: AgentToolCall,
|
|
97
108
|
labelForCall?: (call: AgentToolCall) => string | undefined,
|
|
109
|
+
toolLabels?: Record<string, string>,
|
|
98
110
|
): { label: string; icon: IconName } {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
const generated =
|
|
112
|
+
call.toolName.startsWith("generate_") && call.toolName.endsWith("_from_template");
|
|
113
|
+
const label =
|
|
114
|
+
toolLabels?.[call.toolName] ??
|
|
115
|
+
(generated ? toolLabels?.generate_docx_from_template : undefined);
|
|
104
116
|
return {
|
|
105
|
-
label: labelForCall?.(call) ??
|
|
106
|
-
icon:
|
|
117
|
+
label: labelForCall?.(call) ?? label ?? prettifyToolName(call.toolName),
|
|
118
|
+
icon: TOOL_ICONS[call.toolName] ?? (generated ? "file-text" : "list"),
|
|
107
119
|
};
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
/** Resolve a tool step's display label from its call. */
|
|
111
|
-
function stepLabel(
|
|
112
|
-
|
|
123
|
+
function stepLabel(
|
|
124
|
+
s: AgentStep,
|
|
125
|
+
labelForCall?: (call: AgentToolCall) => string | undefined,
|
|
126
|
+
toolLabels?: Record<string, string>,
|
|
127
|
+
): string {
|
|
128
|
+
return resolveToolMeta(
|
|
129
|
+
{ toolName: s.toolName, input: s.input, state: s.status },
|
|
130
|
+
labelForCall,
|
|
131
|
+
toolLabels,
|
|
132
|
+
).label;
|
|
113
133
|
}
|
|
114
134
|
|
|
115
135
|
const INK = colors.zinc[700];
|
|
@@ -124,7 +144,7 @@ const INK = colors.zinc[700];
|
|
|
124
144
|
* Pair with `Composer` + `ChangeReview`.
|
|
125
145
|
*/
|
|
126
146
|
export function AgentRun(props: AgentRunProps) {
|
|
127
|
-
const { parts, labelForCall, renderToolOutput, onRetry, stepsLabel
|
|
147
|
+
const { parts, labelForCall, renderToolOutput, onRetry, stepsLabel, accessibilityLabel } = props;
|
|
128
148
|
const locale = useLoticsLocale();
|
|
129
149
|
const segments = toSegments(parts);
|
|
130
150
|
const state = props.state ?? (anyRunning(segments) ? "streaming" : "done");
|
|
@@ -163,7 +183,7 @@ export function AgentRun(props: AgentRunProps) {
|
|
|
163
183
|
onToggle={() => toggle(seg.id)}
|
|
164
184
|
labelForCall={labelForCall}
|
|
165
185
|
renderToolOutput={renderToolOutput}
|
|
166
|
-
stepsLabel={stepsLabel}
|
|
186
|
+
stepsLabel={stepsLabel ?? locale.agentRun.steps}
|
|
167
187
|
/>
|
|
168
188
|
);
|
|
169
189
|
})}
|
|
@@ -369,7 +389,7 @@ function ToolGroup(props: {
|
|
|
369
389
|
}) {
|
|
370
390
|
const { steps, active, expanded, onToggle, labelForCall, renderToolOutput, stepsLabel } = props;
|
|
371
391
|
const locale = useLoticsLocale();
|
|
372
|
-
const resolve = (s: AgentStep) => stepLabel(s, labelForCall);
|
|
392
|
+
const resolve = (s: AgentStep) => stepLabel(s, labelForCall, locale.agentRun.tools);
|
|
373
393
|
|
|
374
394
|
// ACTIVE — one pulsing row whose label swaps in place as each call fires (the
|
|
375
395
|
// label is keyed by the current step's id, so a new call rises + fades into the
|
package/src/locale.tsx
CHANGED
|
@@ -132,9 +132,27 @@ export interface LoticsLocale {
|
|
|
132
132
|
/** `AgentRun`: the reasoning disclosure's label (settled / streaming), the
|
|
133
133
|
* auto-built tool peek's Input / Error / Output panel titles, the `awaiting`
|
|
134
134
|
* annotation on a call parked on a human decision, and the terminal error's
|
|
135
|
-
* `retry` action
|
|
136
|
-
*
|
|
137
|
-
|
|
135
|
+
* `retry` action, the per-tool labels (`tools`) and the "{n} steps" suffix
|
|
136
|
+
* (`steps`). `labelForCall` / `stepsLabel` remain as per-call-site
|
|
137
|
+
* OVERRIDES, not as the only way to get a translation. */
|
|
138
|
+
/** `AgentRun` / `AgentProgress` chrome, plus `tools` — the display label per
|
|
139
|
+
* PLATFORM tool name. The tool set is bounded and kit-known, so localizing it
|
|
140
|
+
* here means every app inherits it; leaving it to each call site's
|
|
141
|
+
* `labelForCall` made a Vietnamese surface render Vietnamese chrome around
|
|
142
|
+
* English tool rows. A name absent from the map still falls back to its
|
|
143
|
+
* prettified form, and `labelForCall` still overrides per call. */
|
|
144
|
+
agentRun: {
|
|
145
|
+
starting: string;
|
|
146
|
+
thinking: string;
|
|
147
|
+
thinkingStreaming: string;
|
|
148
|
+
input: string;
|
|
149
|
+
error: string;
|
|
150
|
+
output: string;
|
|
151
|
+
awaiting: string;
|
|
152
|
+
retry: string;
|
|
153
|
+
tools: Record<string, string>;
|
|
154
|
+
steps: (n: number) => string;
|
|
155
|
+
};
|
|
138
156
|
/** `ApprovalPrompt`: the default prompt line (overridable per instance) and
|
|
139
157
|
* the Approve / Deny button labels — the surface that ANSWERS `AgentRun`'s
|
|
140
158
|
* read-only `awaiting` row (approve/deny = the ai-sdk approval vocabulary). */
|
|
@@ -237,7 +255,35 @@ export const en: LoticsLocale = {
|
|
|
237
255
|
matrix: { total: "Total", less: "Less", more: "More" },
|
|
238
256
|
scrollToBottom: { tooltip: "Scroll to bottom" },
|
|
239
257
|
textInputField: { clear: "Clear" },
|
|
240
|
-
agentRun: {
|
|
258
|
+
agentRun: {
|
|
259
|
+
starting: "Starting…",
|
|
260
|
+
thinking: "Thinking",
|
|
261
|
+
thinkingStreaming: "Thinking…",
|
|
262
|
+
input: "Input",
|
|
263
|
+
error: "Error",
|
|
264
|
+
output: "Output",
|
|
265
|
+
awaiting: "Awaiting",
|
|
266
|
+
retry: "Retry",
|
|
267
|
+
tools: {
|
|
268
|
+
query_records: "Searching records",
|
|
269
|
+
get_record: "Reading a record",
|
|
270
|
+
get_records: "Reading records",
|
|
271
|
+
create_records: "Creating records",
|
|
272
|
+
update_records: "Updating records",
|
|
273
|
+
delete_records: "Removing records",
|
|
274
|
+
run_app_query: "Reading data",
|
|
275
|
+
run_app_workflow: "Running an action",
|
|
276
|
+
grep_knowledge: "Searching reference material",
|
|
277
|
+
read_knowledge: "Reading reference material",
|
|
278
|
+
list_knowledge: "Listing reference material",
|
|
279
|
+
view_files: "Reading files",
|
|
280
|
+
ask_user_choice: "Asking a question",
|
|
281
|
+
generate_pdf_from_template: "Generating PDF",
|
|
282
|
+
generate_excel_from_template: "Generating spreadsheet",
|
|
283
|
+
generate_docx_from_template: "Generating document",
|
|
284
|
+
},
|
|
285
|
+
steps: (n) => `${n} steps`,
|
|
286
|
+
},
|
|
241
287
|
approvalPrompt: { message: "The assistant wants to perform an action that needs your approval.", approve: "Approve", deny: "Deny" },
|
|
242
288
|
messageActions: { copy: "Copy", copied: "Copied", regenerate: "Regenerate", edit: "Edit", previousVersion: "Previous version", nextVersion: "Next version" },
|
|
243
289
|
};
|
|
@@ -335,7 +381,35 @@ export const vi: LoticsLocale = {
|
|
|
335
381
|
matrix: { total: "Tổng", less: "Ít", more: "Nhiều" },
|
|
336
382
|
scrollToBottom: { tooltip: "Cuộn xuống cuối" },
|
|
337
383
|
textInputField: { clear: "Xóa" },
|
|
338
|
-
agentRun: {
|
|
384
|
+
agentRun: {
|
|
385
|
+
starting: "Đang bắt đầu…",
|
|
386
|
+
thinking: "Suy nghĩ",
|
|
387
|
+
thinkingStreaming: "Đang suy nghĩ…",
|
|
388
|
+
input: "Đầu vào",
|
|
389
|
+
error: "Lỗi",
|
|
390
|
+
output: "Kết quả",
|
|
391
|
+
awaiting: "Chờ duyệt",
|
|
392
|
+
retry: "Thử lại",
|
|
393
|
+
tools: {
|
|
394
|
+
query_records: "Đang tìm dữ liệu",
|
|
395
|
+
get_record: "Đang đọc một bản ghi",
|
|
396
|
+
get_records: "Đang đọc dữ liệu",
|
|
397
|
+
create_records: "Đang tạo dữ liệu",
|
|
398
|
+
update_records: "Đang cập nhật dữ liệu",
|
|
399
|
+
delete_records: "Đang xoá dữ liệu",
|
|
400
|
+
run_app_query: "Đang đọc dữ liệu",
|
|
401
|
+
run_app_workflow: "Đang chạy tác vụ",
|
|
402
|
+
grep_knowledge: "Đang tra tài liệu",
|
|
403
|
+
read_knowledge: "Đang đọc tài liệu",
|
|
404
|
+
list_knowledge: "Đang liệt kê tài liệu",
|
|
405
|
+
view_files: "Đang đọc tệp",
|
|
406
|
+
ask_user_choice: "Đang hỏi thêm",
|
|
407
|
+
generate_pdf_from_template: "Đang tạo PDF",
|
|
408
|
+
generate_excel_from_template: "Đang tạo bảng tính",
|
|
409
|
+
generate_docx_from_template: "Đang tạo văn bản",
|
|
410
|
+
},
|
|
411
|
+
steps: (n) => `${n} bước`,
|
|
412
|
+
},
|
|
339
413
|
approvalPrompt: { message: "Trợ lý muốn thực hiện thao tác cần bạn duyệt.", approve: "Cho phép", deny: "Từ chối" },
|
|
340
414
|
messageActions: { copy: "Sao chép", copied: "Đã sao chép", regenerate: "Tạo lại", edit: "Chỉnh sửa", previousVersion: "Phiên bản trước", nextVersion: "Phiên bản sau" },
|
|
341
415
|
};
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { toSegments, anyRunning, lastRunningStep, type AgentUIPart, type AgentSegment, type AgentStep } from "./agent_transform";
|
|
3
|
-
|
|
4
|
-
// RN-free logic test for the parts → timeline fold. The renderer (agent_run.tsx)
|
|
5
|
-
// is verified visually in the gallery; the mapping — ai's 7-state tool machine to
|
|
6
|
-
// the feed's 4 states, plus grouping — is unit-tested here. Fixtures are the ai-sdk
|
|
7
|
-
// `dynamic-tool` part shape (contextually typed as `AgentUIPart`, so each concrete
|
|
8
|
-
// `state` discriminates to the right member — no casts).
|
|
9
|
-
|
|
10
|
-
function groups(parts: AgentUIPart[]): Extract<AgentSegment, { kind: "group" }>[] {
|
|
11
|
-
return toSegments(parts).filter((s): s is Extract<AgentSegment, { kind: "group" }> => s.kind === "group");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function onlyStep(parts: AgentUIPart[]): AgentStep {
|
|
15
|
-
const [group, ...rest] = groups(parts);
|
|
16
|
-
expect(rest).toHaveLength(0);
|
|
17
|
-
expect(group.steps).toHaveLength(1);
|
|
18
|
-
return group.steps[0];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("agent_transform — tool state mapping", () => {
|
|
22
|
-
it("input-streaming → running", () => {
|
|
23
|
-
expect(onlyStep([{ type: "dynamic-tool", toolName: "update_records", toolCallId: "t", state: "input-streaming", input: {} }]).status).toBe("running");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("input-available → running", () => {
|
|
27
|
-
expect(onlyStep([{ type: "dynamic-tool", toolName: "update_records", toolCallId: "t", state: "input-available", input: {} }]).status).toBe("running");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("approval-requested → awaiting (parked on a human decision)", () => {
|
|
31
|
-
const step = onlyStep([{ type: "dynamic-tool", toolName: "delete_records", toolCallId: "t", state: "approval-requested", input: { table: "Containers" }, approval: { id: "a1" } }]);
|
|
32
|
-
expect(step.status).toBe("awaiting");
|
|
33
|
-
// The input still rides along for the on-demand peek.
|
|
34
|
-
expect(step.input).toEqual({ table: "Containers" });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("approval-responded → running (the decision is in, work resumes)", () => {
|
|
38
|
-
expect(onlyStep([{ type: "dynamic-tool", toolName: "delete_records", toolCallId: "t", state: "approval-responded", input: {}, approval: { id: "a1", approved: true } }]).status).toBe("running");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("output-available → done, carrying the output", () => {
|
|
42
|
-
const step = onlyStep([{ type: "dynamic-tool", toolName: "query_records", toolCallId: "t", state: "output-available", input: { q: 1 }, output: { rows: 3 } }]);
|
|
43
|
-
expect(step.status).toBe("done");
|
|
44
|
-
expect(step.output).toEqual({ rows: 3 });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("output-error → error, carrying errorText", () => {
|
|
48
|
-
const step = onlyStep([{ type: "dynamic-tool", toolName: "query_records", toolCallId: "t", state: "output-error", input: {}, errorText: "Unknown field" }]);
|
|
49
|
-
expect(step.status).toBe("error");
|
|
50
|
-
expect(step.errorText).toBe("Unknown field");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("output-denied → error, with the denial reason as the error text", () => {
|
|
54
|
-
const step = onlyStep([{ type: "dynamic-tool", toolName: "delete_records", toolCallId: "t", state: "output-denied", input: {}, approval: { id: "a1", approved: false, reason: "Delete not permitted" } }]);
|
|
55
|
-
expect(step.status).toBe("error");
|
|
56
|
-
expect(step.errorText).toBe("Delete not permitted");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("output-denied without a reason → error with no error text", () => {
|
|
60
|
-
const step = onlyStep([{ type: "dynamic-tool", toolName: "delete_records", toolCallId: "t", state: "output-denied", input: {}, approval: { id: "a1", approved: false } }]);
|
|
61
|
-
expect(step.status).toBe("error");
|
|
62
|
-
expect(step.errorText).toBeUndefined();
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe("agent_transform — streaming signals", () => {
|
|
67
|
-
it("anyRunning is true while a call runs, false once every call has settled", () => {
|
|
68
|
-
const running = toSegments([{ type: "dynamic-tool", toolName: "query_records", toolCallId: "t", state: "input-available", input: {} }]);
|
|
69
|
-
expect(anyRunning(running)).toBe(true);
|
|
70
|
-
|
|
71
|
-
const settled = toSegments([{ type: "dynamic-tool", toolName: "query_records", toolCallId: "t", state: "output-available", input: {}, output: {} }]);
|
|
72
|
-
expect(anyRunning(settled)).toBe(false);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("an awaiting call is NOT running — it's parked, so the feed never pulses it", () => {
|
|
76
|
-
const parked = toSegments([{ type: "dynamic-tool", toolName: "delete_records", toolCallId: "t", state: "approval-requested", input: {}, approval: { id: "a1" } }]);
|
|
77
|
-
expect(anyRunning(parked)).toBe(false);
|
|
78
|
-
expect(lastRunningStep(parked)).toBeUndefined();
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe("agent_transform — grouping", () => {
|
|
83
|
-
it("folds consecutive tool parts into ONE group, split by prose", () => {
|
|
84
|
-
const parts: AgentUIPart[] = [
|
|
85
|
-
{ type: "text", text: "First" },
|
|
86
|
-
{ type: "dynamic-tool", toolName: "query_records", toolCallId: "a", state: "output-available", input: {}, output: {} },
|
|
87
|
-
{ type: "dynamic-tool", toolName: "query_records", toolCallId: "b", state: "output-available", input: {}, output: {} },
|
|
88
|
-
{ type: "text", text: "Then" },
|
|
89
|
-
{ type: "dynamic-tool", toolName: "delete_records", toolCallId: "c", state: "approval-requested", input: {}, approval: { id: "a1" } },
|
|
90
|
-
];
|
|
91
|
-
expect(toSegments(parts).map((s) => s.kind)).toEqual(["text", "group", "text", "group"]);
|
|
92
|
-
const [first, second] = groups(parts);
|
|
93
|
-
expect(first.steps.map((s) => s.id)).toEqual(["a", "b"]);
|
|
94
|
-
expect(second.steps.map((s) => s.status)).toEqual(["awaiting"]);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { layoutDayColumns, packEventLanes } from "./layout";
|
|
3
|
-
import type { CalendarEvent } from "./types";
|
|
4
|
-
|
|
5
|
-
const DAY = new Date(2026, 0, 15);
|
|
6
|
-
const at = (h: number, m = 0) => new Date(2026, 0, 15, h, m);
|
|
7
|
-
const ev = (id: string, sh: number, sm: number, eh: number, em: number): CalendarEvent => ({
|
|
8
|
-
id,
|
|
9
|
-
title: id,
|
|
10
|
-
start: at(sh, sm),
|
|
11
|
-
end: at(eh, em),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
/** column/columns keyed by event id, for order-independent assertions. */
|
|
15
|
-
function geom(events: CalendarEvent[]) {
|
|
16
|
-
return Object.fromEntries(
|
|
17
|
-
layoutDayColumns(DAY, events).map((c) => [c.event.id, { column: c.column, columns: c.columns }]),
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("layoutDayColumns", () => {
|
|
22
|
-
it("returns nothing for an empty day", () => {
|
|
23
|
-
expect(layoutDayColumns(DAY, [])).toEqual([]);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("stacks non-overlapping events in a single column", () => {
|
|
27
|
-
const g = geom([ev("a", 9, 0, 10, 0), ev("b", 11, 0, 12, 0)]);
|
|
28
|
-
expect(g.a).toEqual({ column: 0, columns: 1 });
|
|
29
|
-
expect(g.b).toEqual({ column: 0, columns: 1 });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("splits two fully overlapping events into two columns", () => {
|
|
33
|
-
const g = geom([ev("a", 9, 0, 10, 0), ev("b", 9, 0, 10, 0)]);
|
|
34
|
-
expect(g.a.columns).toBe(2);
|
|
35
|
-
expect(g.b.columns).toBe(2);
|
|
36
|
-
expect(new Set([g.a.column, g.b.column])).toEqual(new Set([0, 1]));
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("gives three concurrent events three columns", () => {
|
|
40
|
-
const g = geom([ev("a", 9, 0, 12, 0), ev("b", 9, 0, 12, 0), ev("c", 9, 0, 12, 0)]);
|
|
41
|
-
expect(new Set([g.a.column, g.b.column, g.c.column])).toEqual(new Set([0, 1, 2]));
|
|
42
|
-
for (const k of ["a", "b", "c"]) expect(g[k].columns).toBe(3);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("reuses a freed column within a transitive cluster (A→B→C chain)", () => {
|
|
46
|
-
// A 9-10, B 9:30-11 (overlaps A), C 10:30-12 (overlaps B, not A).
|
|
47
|
-
// One cluster via B; 2 columns; C reuses A's column once A ends.
|
|
48
|
-
const g = geom([ev("a", 9, 0, 10, 0), ev("b", 9, 30, 11, 0), ev("c", 10, 30, 12, 0)]);
|
|
49
|
-
expect(g.a.columns).toBe(2);
|
|
50
|
-
expect(g.a.column).toBe(0);
|
|
51
|
-
expect(g.b.column).toBe(1);
|
|
52
|
-
expect(g.c.column).toBe(0); // freed by A
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("treats time-separated events as independent clusters", () => {
|
|
56
|
-
const g = geom([ev("morn1", 9, 0, 10, 0), ev("morn2", 9, 0, 10, 0), ev("aft", 14, 0, 15, 0)]);
|
|
57
|
-
expect(g.morn1.columns).toBe(2);
|
|
58
|
-
expect(g.morn2.columns).toBe(2);
|
|
59
|
-
expect(g.aft).toEqual({ column: 0, columns: 1 }); // own cluster
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("positions and floors height; clamps to the day window", () => {
|
|
63
|
-
const [c] = layoutDayColumns(DAY, [ev("x", 9, 0, 9, 5)]);
|
|
64
|
-
expect(c.topMinutes).toBe(540); // 9:00
|
|
65
|
-
expect(c.heightMinutes).toBeGreaterThanOrEqual(20); // 5-min event floored
|
|
66
|
-
const spill = layoutDayColumns(DAY, [
|
|
67
|
-
{ id: "y", title: "y", start: at(23, 0), end: new Date(2026, 0, 16, 2, 0) },
|
|
68
|
-
])[0];
|
|
69
|
-
expect(spill.topMinutes).toBe(1380); // 23:00
|
|
70
|
-
expect(spill.topMinutes + spill.heightMinutes).toBeLessThanOrEqual(1440); // clamped
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe("packEventLanes", () => {
|
|
75
|
-
const WEEK = new Date(2026, 0, 12); // Mon
|
|
76
|
-
const allDay = (id: string, startDay: number, endDay: number): CalendarEvent => ({
|
|
77
|
-
id,
|
|
78
|
-
title: id,
|
|
79
|
-
start: new Date(2026, 0, 12 + startDay),
|
|
80
|
-
end: new Date(2026, 0, 12 + endDay),
|
|
81
|
-
allDay: true,
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("places non-overlapping spans in one lane", () => {
|
|
85
|
-
const { bars, lanes } = packEventLanes([allDay("a", 0, 1), allDay("b", 3, 4)], WEEK, 7);
|
|
86
|
-
expect(lanes).toBe(1);
|
|
87
|
-
expect(bars.find((b) => b.event.id === "a")).toMatchObject({ startCol: 0, span: 2, lane: 0 });
|
|
88
|
-
expect(bars.find((b) => b.event.id === "b")).toMatchObject({ startCol: 3, span: 2, lane: 0 });
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("stacks overlapping multi-day spans into separate lanes", () => {
|
|
92
|
-
const { lanes } = packEventLanes([allDay("a", 0, 3), allDay("b", 2, 5)], WEEK, 7);
|
|
93
|
-
expect(lanes).toBe(2);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("clamps a span that starts before the window and filters out-of-range", () => {
|
|
97
|
-
const before: CalendarEvent = { id: "x", title: "x", start: new Date(2026, 0, 9), end: new Date(2026, 0, 13), allDay: true };
|
|
98
|
-
const after = allDay("y", 9, 10); // entirely after a 7-day window
|
|
99
|
-
const { bars } = packEventLanes([before, after], WEEK, 7);
|
|
100
|
-
expect(bars).toHaveLength(1);
|
|
101
|
-
expect(bars[0]).toMatchObject({ event: { id: "x" }, startCol: 0, span: 2 }); // clamped to days 0..1
|
|
102
|
-
});
|
|
103
|
-
});
|
package/src/colors.test.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { isColorName, asColorName } from "./colors";
|
|
3
|
-
|
|
4
|
-
describe("isColorName", () => {
|
|
5
|
-
it("accepts palette family names", () => {
|
|
6
|
-
expect(isColorName("blue")).toBe(true);
|
|
7
|
-
expect(isColorName("emerald")).toBe(true);
|
|
8
|
-
expect(isColorName("zinc")).toBe(true);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("rejects role keys and scalar colors (not selectable families)", () => {
|
|
12
|
-
expect(isColorName("border")).toBe(false);
|
|
13
|
-
expect(isColorName("border_shadow")).toBe(false);
|
|
14
|
-
expect(isColorName("background")).toBe(false);
|
|
15
|
-
expect(isColorName("shadow")).toBe(false);
|
|
16
|
-
expect(isColorName("black")).toBe(false);
|
|
17
|
-
expect(isColorName("white")).toBe(false);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("rejects unknown tokens and non-strings", () => {
|
|
21
|
-
expect(isColorName("chartreuse")).toBe(false);
|
|
22
|
-
expect(isColorName("")).toBe(false);
|
|
23
|
-
expect(isColorName(undefined)).toBe(false);
|
|
24
|
-
expect(isColorName(null)).toBe(false);
|
|
25
|
-
expect(isColorName(42)).toBe(false);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("asColorName", () => {
|
|
30
|
-
it("passes a valid family through", () => {
|
|
31
|
-
expect(asColorName("purple")).toBe("purple");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("degrades an unknown/absent token to the neutral default", () => {
|
|
35
|
-
// The graceful-degradation contract: a select option whose color token this
|
|
36
|
-
// build doesn't recognize, or none at all, renders neutral rather than break.
|
|
37
|
-
expect(asColorName("chartreuse")).toBe("zinc");
|
|
38
|
-
expect(asColorName(undefined)).toBe("zinc");
|
|
39
|
-
expect(asColorName(null)).toBe("zinc");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("honors a custom fallback", () => {
|
|
43
|
-
expect(asColorName(undefined, "slate")).toBe("slate");
|
|
44
|
-
});
|
|
45
|
-
});
|