@loomwork/ui-renderer-terminal 0.0.0-stage → 0.1.1
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 +202 -0
- package/NOTICE +8 -0
- package/README.md +369 -2
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/inert.js +13 -0
- package/dist/esm/inert.js.map +1 -0
- package/dist/esm/interactive.js +677 -0
- package/dist/esm/interactive.js.map +1 -0
- package/dist/esm/registry.js +428 -0
- package/dist/esm/registry.js.map +1 -0
- package/dist/esm/status.js +64 -0
- package/dist/esm/status.js.map +1 -0
- package/dist/esm/watch.js +17 -0
- package/dist/esm/watch.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/inert.d.ts +12 -0
- package/dist/inert.d.ts.map +1 -0
- package/dist/inert.js +17 -0
- package/dist/inert.js.map +1 -0
- package/dist/interactive.d.ts +142 -0
- package/dist/interactive.d.ts.map +1 -0
- package/dist/interactive.js +685 -0
- package/dist/interactive.js.map +1 -0
- package/dist/registry.d.ts +55 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +434 -0
- package/dist/registry.js.map +1 -0
- package/dist/status.d.ts +49 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +69 -0
- package/dist/status.js.map +1 -0
- package/dist/watch.d.ts +9 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +20 -0
- package/dist/watch.js.map +1 -0
- package/package.json +60 -3
- package/src/index.ts +25 -0
- package/src/inert.ts +14 -0
- package/src/interactive.ts +868 -0
- package/src/registry.ts +630 -0
- package/src/status.ts +103 -0
- package/src/watch.ts +22 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
202
|
+
|
package/NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
loom-ui
|
|
2
|
+
Copyright 2026 Sushil Chhetri
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this project except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
package/README.md
CHANGED
|
@@ -1,3 +1,370 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @loomwork/ui-renderer-terminal
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
String renderers for every framework kind, merged over loom's own kind-aware terminal registry,
|
|
4
|
+
plus `renderExperience` and `watch` — the terminal rendering surface for the `Blueprint`
|
|
5
|
+
`@loomwork/ui-core` compiles and the projection state `@loomwork/ui-hydration` publishes. Where
|
|
6
|
+
`@loomwork/ui-renderer-lit` mounts DOM elements, this package renders one plain-text frame; status
|
|
7
|
+
(loading/empty/stale/error) comes entirely from loom's own Blueprint traversal — this package
|
|
8
|
+
contributes only the per-kind value check.
|
|
9
|
+
|
|
10
|
+
## Install / import
|
|
11
|
+
|
|
12
|
+
```jsonc
|
|
13
|
+
// package.json of a consumer checked out beside this repo (the packages are not published)
|
|
14
|
+
"@loomwork/ui-renderer-terminal": "link:../loom-ui/packages/renderer-terminal"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { renderExperience, watch, terminalRegistry } from "@loomwork/ui-renderer-terminal";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Key concepts
|
|
22
|
+
|
|
23
|
+
- **The status matrix is loom's.** Since Blueprint IR 2.0.0 each binding declares its status channel
|
|
24
|
+
(`status.<handle>`, beside `values.<handle>` in the same projection state), and loom's own
|
|
25
|
+
`renderBlueprintToString` applies the eight-row decision table (loading, ready, empty, stale,
|
|
26
|
+
error, …). This package contributes exactly one thing to it: what a bound VALUE must look like,
|
|
27
|
+
through the `check` seam loom leaves open — `kindValueCheck`.
|
|
28
|
+
- **One registry, merged over loom's.** `terminalRegistry()` returns loom's
|
|
29
|
+
`defaultTerminalRegistry` merged with this package's own renderers for the framework kinds
|
|
30
|
+
(`TimeSeries`, `BarChart`, `Kpi`, `Table`, `StatusCard`, `Note`, `Prose`, `Diagram`, `A2UIView`,
|
|
31
|
+
`SandboxView`, the media kinds `Image`/`Audio`/`Video`/`File`/`MediaInput` and the chat kinds `Chat`,
|
|
32
|
+
`TextInput`, `FormInput`, `SelectInput`, `MultiSelectInput`) — a fresh registry each call, since loom's default registries are shared singletons that must never be
|
|
33
|
+
mutated in place.
|
|
34
|
+
- **`renderExperience` reads one well-known projection.** A `HydrationStore` always publishes under
|
|
35
|
+
`HYDRATION_PROJECTION_ID`; `renderExperience` maps every OTHER projection id the Blueprint
|
|
36
|
+
declares to `undefined` state, so a binding under a different id renders as its honest loading row
|
|
37
|
+
rather than an error.
|
|
38
|
+
- **Numeric parity with the web.** Every framework-kind renderer here formats a number exactly as
|
|
39
|
+
`@loomwork/ui-renderer-lit`'s `measure()` would for the same value, format and unit — same rounding,
|
|
40
|
+
grouping, currency symbol and unit suffix — via `@loomwork/ui-core`'s `formatSemanticNumber` and
|
|
41
|
+
`effectiveFormat`, at one fixed locale (`en-US` by default) so the terminal output does not vary
|
|
42
|
+
with the host's own locale; a host opts into another with `i18n.locale` (see "Localising the
|
|
43
|
+
renderer").
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
- `renderExperience(blueprint, store: HydrationStore, opts?: RenderExperienceOptions): string` —
|
|
48
|
+
render one frame: loom's status matrix over `store.state()`, this package's registry and value
|
|
49
|
+
check by default. `opts.registry` overrides the registry.
|
|
50
|
+
- `watch(blueprint, store, onFrame: (frame: string) => void, opts?): () => void` — call `onFrame`
|
|
51
|
+
now and on every change to the store's projection (via `store.runtimeSubscribe`); returns an
|
|
52
|
+
unsubscribe function.
|
|
53
|
+
- `terminalRegistry(): RendererRegistry<TerminalWidgetRenderer>` — loom's built-in terminal registry
|
|
54
|
+
merged with `loomUiTerminalRegistry`. Call it fresh each time you need one. `terminalRegistry({
|
|
55
|
+
chrome: "host" })` (also on `renderExperience` and `createLoomUiTerminalRegistry`) is for an app
|
|
56
|
+
that draws its own title bar or frame: the framework kinds print no `Title:` prefix — ready, error
|
|
57
|
+
(`✗ msg`) or loading (`—`) — and a Kpi's caption trails the figure, and multi-line bodies (Table,
|
|
58
|
+
TimeSeries, BarChart) print with no 4-space hanging indent — that indent only sits rows under the
|
|
59
|
+
`Title:` lead, which host chrome does not have (so a narrow cell keeps its columns). Default
|
|
60
|
+
`"widget"` is unchanged. loom's own traversal rows (status lines) and its eight built-in kinds are
|
|
61
|
+
loom's output and keep their titles. Only a non-empty string is a title: `""` or a non-string
|
|
62
|
+
(`0`, `5`) is none — the kind stands in, a Note prints no title — exactly as in the lit renderer.
|
|
63
|
+
`renderExperience` throws if given both `chrome` and `registry` (chrome would be silently
|
|
64
|
+
ignored); use `terminalRegistry({ chrome })` for yours.
|
|
65
|
+
- `loomUiTerminalRegistry` — just the framework kinds' renderers, unmerged, for a caller building
|
|
66
|
+
its own composed registry.
|
|
67
|
+
- `sparkline(values: readonly number[]): string` — an eight-level Unicode sparkline (`▁▂▃▄▅▆▇█`)
|
|
68
|
+
over a series of numbers, used by the `TimeSeries` renderer and available standalone.
|
|
69
|
+
- `kindValueCheck: WidgetValueCheck` — the framework's per-kind value contract, plugged into loom's
|
|
70
|
+
`check` seam; exported so a caller assembling its own `renderBlueprintToString` call can reuse it.
|
|
71
|
+
`kindValueCheckFor(contracts)` is the same check that also validates a host's own kinds against the
|
|
72
|
+
catalogue's `valueContracts` (what `renderExperience` uses when given `contracts`).
|
|
73
|
+
- `RenderExperienceOptions` — `{ registry?, contracts?, chrome?, media?, i18n?, projectionState?,
|
|
74
|
+
projectionSubscribe? }` (it extends `TerminalRegistryOptions`: `chrome`, `media`, `i18n`).
|
|
75
|
+
`projectionState(id)` supplies the state of any NON-hydration projection the blueprint declares (a
|
|
76
|
+
node bound through core's `SemanticNode.projection`), e.g. `(id) => runtime.getState(id,
|
|
77
|
+
store.executionId)`; absent or throwing, its widgets render as loading.
|
|
78
|
+
`projectionSubscribe(onChange)` lets `watch` re-render when such a projection changes.
|
|
79
|
+
|
|
80
|
+
## Examples
|
|
81
|
+
|
|
82
|
+
Rendering once, and re-rendering on every change (illustrative — not a compiling snippet):
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { renderExperience, watch } from "@loomwork/ui-renderer-terminal";
|
|
86
|
+
|
|
87
|
+
// One frame, right now:
|
|
88
|
+
process.stdout.write(renderExperience(blueprint, store));
|
|
89
|
+
|
|
90
|
+
// Live: re-render on every hydration update, clearing the screen each time.
|
|
91
|
+
const stop = watch(blueprint, store, (frame) => process.stdout.write(`\x1Bc${frame}`));
|
|
92
|
+
// later: stop();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Composing your own registry (illustrative):
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { loomUiTerminalRegistry } from "@loomwork/ui-renderer-terminal";
|
|
99
|
+
import { defaultTerminalRegistry } from "@loomwork/plugin-renderer-terminal";
|
|
100
|
+
|
|
101
|
+
const myRegistry = defaultTerminalRegistry.merge(loomUiTerminalRegistry).merge(myOwnKinds);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Notable behaviours and guarantees
|
|
105
|
+
|
|
106
|
+
- **Chat and inputs (U8).** `Chat` prints under its title as the SAME lines the web speaks
|
|
107
|
+
(`chatTranscriptLines`): `You: ...`, `Assistant (interrupted): ...`, `You (edit of turn 1): ...`,
|
|
108
|
+
a failure as its fixed sentence plus `(ref <id>)`, a restricted entry as `[restricted assistant
|
|
109
|
+
turn]`, media described without an address, then `(the assistant is replying...)` / `(conversation
|
|
110
|
+
closed)` / `... earlier messages are available`. Each turn is one bounded line of cleaned text: an
|
|
111
|
+
escape sequence, a bidi override or a newline in a turn cannot reach the screen or forge a
|
|
112
|
+
`Assistant:` line. `TextInput` / `FormInput` / `SelectInput` / `MultiSelectInput` print a prompt that says what they
|
|
113
|
+
ask (fields, numbered options, the current choice listed with `(current)`, and a count of the other options) and that the answer goes through an interactive surface (the
|
|
114
|
+
registry draws, it does not read a line): a terminal host that collects answers sends them through
|
|
115
|
+
the same `createWriteBack` as the web. The web renderer's `chatAccessories` (host content drawn under
|
|
116
|
+
an entry) has no terminal counterpart: the terminal prints the transcript only, and a host's
|
|
117
|
+
accessory content is not shown here.
|
|
118
|
+
- **Source references are footnotes.** A `Kpi`, `BarChart`, `TimeSeries`, `Table`, `StatusCard` or `Diagram` value
|
|
119
|
+
with `refs` prints one `[n] label` line per source under the widget's output (`[n]` alone without a label), the
|
|
120
|
+
label cut to one clean line like any untrusted text. The terminal has no clickable markers, so it does not emit
|
|
121
|
+
`REF_OPEN` and does not claim the conformance capability `references`.
|
|
122
|
+
- **Diagram prints core\'s shared outline.** `Diagram` prints its title, the value's own `title`, then the
|
|
123
|
+
SAME lines the web's hidden list holds (`diagramOutline`): a tree as an indented outline, a flow or
|
|
124
|
+
network as `A → B (label)` lines, a timeline as `date — label` lines in chronological order. Every label
|
|
125
|
+
is cut to one clean line before the outline joins them, so no control or bidi character, and no newline,
|
|
126
|
+
reaches the screen. A value that breaks a rule (over 200 nodes, a dangling edge, two tree roots, ...) is
|
|
127
|
+
the usual `✗` error line.
|
|
128
|
+
- **Media prints a line, never loads (U9).** `Image`/`Audio`/`Video`/`File` print the same one-line
|
|
129
|
+
description the web speaks (`describeMedia`: `image chart.png (image/png, 2 KiB)`), `[untrusted]`
|
|
130
|
+
unless `media.trusted(ref)` says the host vouches (never for `origin: "user"`), and either a
|
|
131
|
+
re-checked `media.link(ref)` (`checkMediaUrl`) or a NON-REVERSIBLE tag `ref#1a2b3c4d` (first 8 hex
|
|
132
|
+
of the SHA-256 of the address; never any part of the address itself), which is a capability and
|
|
133
|
+
terminal output is copied and logged. A declared size over `media.policy.maxBytes` prints "too
|
|
134
|
+
large to open" with no link. Name and alt are stripped of control and bidi characters. Untrusted
|
|
135
|
+
active types print "blocked (active content type)" with no link; a same-origin link is refused for
|
|
136
|
+
untrusted media. `MediaInput` prints that it is a web control. A restricted media node is a
|
|
137
|
+
`Note`, as for any kind.
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
- **A restricted node prints as a `Note`.** `compile({ viewer })` replaces a node the viewer may
|
|
141
|
+
not see with a static, unbound `Note`, printed as `Restricted: Restricted` (or `Restricted` under
|
|
142
|
+
`chrome: "host"`); it is bound to nothing, so a value the store happens to hold is never read.
|
|
143
|
+
Enforcement is `compile`'s and hydration's, not the renderer's.
|
|
144
|
+
|
|
145
|
+
- **A declared channel with no value yet prints loom's loading row** (`⋯ <title>`); an `empty`
|
|
146
|
+
status prints `○ <title>: <message>` — both are loom's own rows, not this package's.
|
|
147
|
+
- **A value that fails its kind's contract shows a visible error**, never a silent blank: every
|
|
148
|
+
framework-kind renderer here checks the bound value with `checkKindValue` (via `kindValueCheck`)
|
|
149
|
+
before drawing it.
|
|
150
|
+
- **Unbound widgets read props, not a channel.** `Note` is never bound: loom calls its renderer
|
|
151
|
+
with `value === undefined`, and its text comes from `props.text` — a compiled `LocalizedText`
|
|
152
|
+
always resolves to its fallback text at compile time, so the terminal never shows a message key.
|
|
153
|
+
- **Web-only kinds say so, honestly.** `A2UIView` and `SandboxView` cannot render their real
|
|
154
|
+
content in a terminal (an A2UI interpreter and sandboxed script execution are web surfaces this
|
|
155
|
+
CommonJS package does not import); their terminal renderers print
|
|
156
|
+
`[A2UI view — rendered on the web]` / `[sandboxed component — rendered on the web]` rather than
|
|
157
|
+
faking a render.
|
|
158
|
+
- **Never mutate a shared registry.** `terminalRegistry()` and `loom-ui`'s framework registry are
|
|
159
|
+
built fresh; never call `.register()` on loom's own `defaultTerminalRegistry` — it is a shared
|
|
160
|
+
singleton other code may depend on being unmodified.
|
|
161
|
+
- **Deterministic, bounded output.** A long series or table never spreads its full length into a
|
|
162
|
+
single-pass min/max (avoiding V8's argument-count ceiling); `Table` prints at most 20 rows before
|
|
163
|
+
an `"… N more rows"` line, and every numeric column is width-capped so one long cell cannot push
|
|
164
|
+
the rest of a row off-screen.
|
|
165
|
+
|
|
166
|
+
## Localising the renderer
|
|
167
|
+
|
|
168
|
+
Every fixed word the terminal prints is a key in the catalogue `LU_MESSAGES` (and the typed key list
|
|
169
|
+
`LU_MESSAGE_KEYS`) exported by `@loomwork/ui-core`; English is the default. Pass a host override as
|
|
170
|
+
`i18n` to `terminalRegistry`, `createLoomUiTerminalRegistry` or `renderExperience`:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { renderExperience } from "@loomwork/ui-renderer-terminal";
|
|
174
|
+
|
|
175
|
+
renderExperience(blueprint, store, {
|
|
176
|
+
i18n: {
|
|
177
|
+
locale: "de-DE", // numbers and plural categories; default "en-US" (DEFAULT_TERMINAL_LOCALE)
|
|
178
|
+
messages: { "lu.terminal.chat.empty": "(keine Nachrichten)" },
|
|
179
|
+
translate: (key, english) => undefined, // optional; undefined keeps English
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- **The default locale stays `en-US`**, not the process's locale: terminal output is logged, diffed
|
|
185
|
+
and snapshotted, so the same value must print the same bytes on every machine. A host that wants
|
|
186
|
+
another opts in with `i18n.locale`.
|
|
187
|
+
- Lookup order is `messages`, then `translate(key, english)`, then English; a throwing `translate`
|
|
188
|
+
is English. `{name}` placeholders must be kept. A counted noun is `<base>.<category>`
|
|
189
|
+
(`lu.plural.row.one` / `.other`, `.few`, ...); the locale's category applies only to a translated
|
|
190
|
+
form.
|
|
191
|
+
- Not covered: bound data (titles, labels, cells, chat text), loom's own eight built-in kinds and
|
|
192
|
+
the `✗`/`○`/`~`/`⋯` status lines loom's traversal writes (loom's renderers), and developer
|
|
193
|
+
diagnostics.
|
|
194
|
+
|
|
195
|
+
The keys the terminal asks for (including the shared chat and media words from `@loomwork/ui-core`):
|
|
196
|
+
|
|
197
|
+
| Key | English default |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| `lu.plural.row.one` | `row` |
|
|
200
|
+
| `lu.plural.row.other` | `rows` |
|
|
201
|
+
| `lu.level.ok` | `ok` |
|
|
202
|
+
| `lu.level.warning` | `warning` |
|
|
203
|
+
| `lu.level.critical` | `critical` |
|
|
204
|
+
| `lu.terminal.table.moreRows` | `… {n} more {noun}` |
|
|
205
|
+
| `lu.media.kind.image` | `image` |
|
|
206
|
+
| `lu.media.kind.audio` | `audio` |
|
|
207
|
+
| `lu.media.kind.video` | `video` |
|
|
208
|
+
| `lu.media.kind.file` | `file` |
|
|
209
|
+
| `lu.chat.who.assistant` | `Assistant` |
|
|
210
|
+
| `lu.chat.who.notice` | `Notice` |
|
|
211
|
+
| `lu.chat.who.you` | `You` |
|
|
212
|
+
| `lu.chat.who.user` | `User` |
|
|
213
|
+
| `lu.prose.more` | `Show more` |
|
|
214
|
+
| `lu.prose.cut` | `(document cut short)` |
|
|
215
|
+
| `lu.prose.empty` | `(no text)` |
|
|
216
|
+
| `lu.chat.who.userNamed` | `User "{name}"` |
|
|
217
|
+
| `lu.approval.approve` | `Approve` |
|
|
218
|
+
| `lu.approval.reject` | `Reject` |
|
|
219
|
+
| `lu.chat.sending` | `Sending…` |
|
|
220
|
+
| `lu.chat.typeFirst` | `Type a message first.` |
|
|
221
|
+
| `lu.chat.dispatch.stale` | `The view changed. Review it and send again.` |
|
|
222
|
+
| `lu.chat.dispatch.unavailable` | `Sending is not available here.` |
|
|
223
|
+
| `lu.chat.dispatch.failed` | `Not sent. Try again.` |
|
|
224
|
+
| `lu.input.sent` | `Sent` |
|
|
225
|
+
| `lu.input.none` | `None` |
|
|
226
|
+
| `lu.input.error.tooLong` | `This is too long.` |
|
|
227
|
+
| `lu.chat.status.replying` | `replying` |
|
|
228
|
+
| `lu.chat.status.interrupted` | `interrupted` |
|
|
229
|
+
| `lu.chat.status.failed` | `failed` |
|
|
230
|
+
| `lu.chat.status.rejected` | `not accepted` |
|
|
231
|
+
| `lu.chat.status.editOf` | `edit of turn {turn}` |
|
|
232
|
+
| `lu.chat.line.restricted` | `[restricted {role} turn]` |
|
|
233
|
+
| `lu.chat.line.restrictedFrom` | `[restricted {role} turn from {by}]` |
|
|
234
|
+
| `lu.chat.line.ref` | `(ref {id})` |
|
|
235
|
+
| `lu.chat.line.tools` | `[tools: {tools}]` |
|
|
236
|
+
| `lu.chat.line.reasoning` | `[reasoning: {text}]` |
|
|
237
|
+
| `lu.chat.line.reasoningSummary` | `[reasoning summary: {text}]` |
|
|
238
|
+
| `lu.chat.line.reasoningRedacted` | `[reasoning redacted]` |
|
|
239
|
+
| `lu.chat.line.reasoningErased` | `[reasoning removed]` |
|
|
240
|
+
| `lu.chat.reason.model-error` | `The assistant ran into a problem.` |
|
|
241
|
+
| `lu.chat.reason.rate-limited` | `The assistant is busy right now. Try again in a moment.` |
|
|
242
|
+
| `lu.chat.reason.quota` | `The assistant's usage limit has been reached.` |
|
|
243
|
+
| `lu.chat.reason.tool-error` | `A tool the assistant used failed.` |
|
|
244
|
+
| `lu.chat.reason.denied` | `That was not permitted.` |
|
|
245
|
+
| `lu.chat.reason.turn-failed` | `The turn could not be completed.` |
|
|
246
|
+
| `lu.chat.reason.cancelled` | `The turn was cancelled.` |
|
|
247
|
+
| `lu.chat.reason.too-long` | `The message was too long.` |
|
|
248
|
+
| `lu.chat.reason.prompt-too-large` | `The message was too large to answer.` |
|
|
249
|
+
| `lu.chat.reason.malformed` | `The message was not understood.` |
|
|
250
|
+
| `lu.chat.reason.unauthenticated` | `The message could not be verified.` |
|
|
251
|
+
| `lu.chat.reason.replayed` | `The message was already received.` |
|
|
252
|
+
| `lu.chat.reason.stale` | `The message arrived too late.` |
|
|
253
|
+
| `lu.chat.reason.not-a-participant` | `You are not a participant in this conversation.` |
|
|
254
|
+
| `lu.chat.reason.edit-invalid` | `That edit could not be applied to the earlier message.` |
|
|
255
|
+
| `lu.chat.reason.generic` | `The message was not processed.` |
|
|
256
|
+
| `lu.input.message` | `Message` |
|
|
257
|
+
| `lu.input.form` | `Form` |
|
|
258
|
+
| `lu.input.choose` | `Choose` |
|
|
259
|
+
| `lu.terminal.a2ui` | `[A2UI view — rendered on the web]` |
|
|
260
|
+
| `lu.terminal.sandbox` | `[sandboxed component — rendered on the web]` |
|
|
261
|
+
| `lu.terminal.fileUpload` | `[file upload — available on the web]` |
|
|
262
|
+
| `lu.terminal.untrusted` | `[untrusted]` |
|
|
263
|
+
| `lu.terminal.media.tooLarge` | `too large to open` |
|
|
264
|
+
| `lu.terminal.media.blocked` | `blocked (active content type)` |
|
|
265
|
+
| `lu.terminal.chat.earlier` | `… earlier messages are available` |
|
|
266
|
+
| `lu.terminal.chat.empty` | `(no messages)` |
|
|
267
|
+
| `lu.terminal.chat.replying` | `(the assistant is replying…)` |
|
|
268
|
+
| `lu.terminal.chat.closed` | `(conversation closed)` |
|
|
269
|
+
| `lu.terminal.multiline` | ` (multi-line)` |
|
|
270
|
+
| `lu.terminal.textInput` | `[text input — answer on an interactive surface]` |
|
|
271
|
+
| `lu.terminal.formInput` | `[form — answer on an interactive surface]` |
|
|
272
|
+
| `lu.terminal.current` | `(current)` |
|
|
273
|
+
| `lu.terminal.selectInput` | `[choice — answer on an interactive surface]` |
|
|
274
|
+
| `lu.terminal.multiSelectInput` | `[choices — answer on an interactive surface]` |
|
|
275
|
+
| `lu.terminal.moreOptions` | `more options` |
|
|
276
|
+
|
|
277
|
+
## Interactive terminal and incremental render
|
|
278
|
+
|
|
279
|
+
`renderExperience` stays a snapshot: a string in, a string out, byte for byte what it always was.
|
|
280
|
+
`interactiveTerminal(blueprint, { store, ... })` is the opt-in live session over the same renderer.
|
|
281
|
+
Its frame is that snapshot followed by a controls footer when the Blueprint has actionable widgets;
|
|
282
|
+
with none, the frame IS the snapshot.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { interactiveTerminal } from "@loomwork/ui-renderer-terminal";
|
|
286
|
+
|
|
287
|
+
const term = interactiveTerminal(blueprint, {
|
|
288
|
+
store, // the HydrationStore; changes repaint
|
|
289
|
+
input: process.stdin, // anything with on("data"); raw mode is set while live
|
|
290
|
+
output: process.stdout, // anything with write(); omit for a headless frame()
|
|
291
|
+
onAction: (a) => writeBack.handle(a), // WidgetAction + viewedVersion; may return an ActionOutcome
|
|
292
|
+
});
|
|
293
|
+
term.update(revisedBlueprint); // incremental: only the changed lines are rewritten
|
|
294
|
+
term.stop();
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
- **Incremental render.** The first paint clears the screen and writes the frame; afterwards each
|
|
298
|
+
change diffs the new frame against the last (`diffFrames`) and writes only the changed lines as
|
|
299
|
+
cursor-addressed rewrites (`paintPatches`). `onPaint` reports `{ mode: "full" | "patch" | "none",
|
|
300
|
+
changedLines, totalLines }`. A change that leaves the frame as it was writes nothing.
|
|
301
|
+
- **Controls.** `ApprovalCard` (only while an approval is pending, like the web), `SelectInput`, `MultiSelectInput`
|
|
302
|
+
and `TextInput`. Tab / Shift-Tab (or the arrows) move focus. Approvals take `y` / `n` only: Enter is
|
|
303
|
+
never consent. A select opens a list (Enter), picks by digit or arrows + Enter, Escape backs out. The list starts on the first option: it does not preselect a bound current choice (the non-interactive prompt does mark it, `(current)`), and the terminal ignores a SelectInput's `size`.
|
|
304
|
+
`MultiSelectInput` opens a checklist (Enter): arrows move, Space ticks or unticks (never past `maxSelected`), Enter sends the whole set, Escape backs out. It starts with the bound choice ticked and shows 10 options around the cursor.
|
|
305
|
+
A text input starts on Enter, edits with Backspace, sends on Enter, Escape cancels.
|
|
306
|
+
- **Consent is deliberate.** `y` / `n` and every submit act only when they are the SOLE key of their
|
|
307
|
+
input chunk, on the frame they were pressed on: a key that changes mode or submits ends the chunk
|
|
308
|
+
(the rest is dropped), so a paste can never navigate to an approval and answer it. The session
|
|
309
|
+
turns on bracketed paste (`ESC[?2004h`, off again on `stop`) and a pasted span is TEXT only: it is
|
|
310
|
+
appended to an open draft (newlines kept), otherwise it is nothing. A consent key queued behind an
|
|
311
|
+
in-flight action is refused (`lu.chat.dispatch.stale`, "The view changed. Review it and send again.") if the blueprint or a control's binding status
|
|
312
|
+
moved since it was pressed, and `viewedVersion` is stamped from that frame, not from whatever is
|
|
313
|
+
current when the key finally runs. After an `APPROVE` / `REJECT` is dispatched for a pending
|
|
314
|
+
approval, further `y` / `n` for it are ignored until the frame moves (the host answered, or a new
|
|
315
|
+
version landed): the HOST must still dedupe by its own key, since another session can answer too.
|
|
316
|
+
`press(...keys)` treats each key as its own arrival; `send(chunk)` and real input are chunks.
|
|
317
|
+
- **Sole means sole.** Every decoded unit of a chunk counts toward "sole", including dropped escape
|
|
318
|
+
sequences (arrows, OSC) and non-printables: `ESC[C` + `y` is two units and cannot approve. Paste
|
|
319
|
+
mode always ends: at the end marker (even split across chunks), at Ctrl-C (paste discarded,
|
|
320
|
+
interrupt delivered), when the paste passes 1 Mi UTF-16 code units (discarded; the rest is
|
|
321
|
+
swallowed to the end marker, never typed), or after `pasteIdleMs` (default 2000, read from an
|
|
322
|
+
injectable `now`) without a byte. An idle-abandoned paste may still be arriving, so the decoder
|
|
323
|
+
then DRAINS: it discards input (nothing is typed) until the end marker or Ctrl-C (delivered as an
|
|
324
|
+
interrupt). A further silence does not end the drain (a slow link can stall twice); a bound plus
|
|
325
|
+
silence does: after 1 Mi UTF-16 code units more with no end marker AND `pasteIdleMs` of silence
|
|
326
|
+
the input is handed back (a still-flowing paste keeps draining), so a lost marker costs at most
|
|
327
|
+
that much typing, never all of it. A drain or an oversize paste that ends inside a chunk yields an
|
|
328
|
+
`ignored` key, so a `y` after it is not sole. During a drain Ctrl-C is delivered as an interrupt
|
|
329
|
+
(by default this stops the session).
|
|
330
|
+
- **Known limits, and `confirmConsent`.** A chunk boundary is all a terminal gives us. (1) On a
|
|
331
|
+
terminal WITHOUT bracketed paste, a one-character paste `y` into a focused approval is a one-key
|
|
332
|
+
chunk, taken as a keypress. (2) A host reading byte by byte (slow serial / ssh) makes every key
|
|
333
|
+
sole, so a multi-line non-bracketed paste could reach `\t`, `y`. For raw-byte-stream hosts set
|
|
334
|
+
`confirmConsent: true` (recommended there; default off): the first `y` / `n` only arms the
|
|
335
|
+
approval (the frame shows `y ×2`, or `labels.confirm`), and the same key alone in a LATER chunk on
|
|
336
|
+
the same frame acts; any other key disarms. (3) A paste whose end marker never arrives is drained
|
|
337
|
+
for 1 MiB, after which what follows is decoded as keys (the sole-key rule still blocks consent);
|
|
338
|
+
Ctrl-C leaves the drain at once.
|
|
339
|
+
- **Actions** are the shapes the web emits (`APPROVE` / `REJECT` with `payload.approved`, `SELECT`,
|
|
340
|
+
`SUBMIT_TEXT` with an idempotency `key`), stamped with `viewedVersion`. What was typed is kept
|
|
341
|
+
until the host answers `ok`; a refusal or a throw is a fixed sentence from `LU_MESSAGES`, never
|
|
342
|
+
the host's text. No `onAction` = "not available here"; nothing is pretended to be sent.
|
|
343
|
+
- **No TTY needed.** `input` / `output` are injected; `send("\t")` and `press({ name: "enter" })`
|
|
344
|
+
feed keys and resolve when the effect, including the action, has settled. `decodeKeys` is the pure
|
|
345
|
+
parser: unknown escape sequences are dropped whole, control characters and bidi overrides are
|
|
346
|
+
never typed text. Ctrl-C calls `onInterrupt` (default `stop`).
|
|
347
|
+
- `labels` overrides the footer's key legend and focus marker (symbols by default); every other word
|
|
348
|
+
uses the host's `i18n`. Form inputs are not driven here (use a surface with fields).
|
|
349
|
+
|
|
350
|
+
`renderExperience` and the interactive frame (controls footer included) strip core's one unsafe set
|
|
351
|
+
(`stripUnsafeChars`, the class `sanitizeChatText` removes: C0/C1 controls, ESC, bidi overrides and
|
|
352
|
+
isolates, zero-width and invisible marks, soft hyphen, TAG characters) plus tab, CR and U+2028/9
|
|
353
|
+
from the whole frame; only newline survives. A title, heading, cell, translated word or typed draft
|
|
354
|
+
can be model- or user-authored, and an ESC would otherwise reach the terminal as an escape sequence.
|
|
355
|
+
"Inert" means STRIPPED, not parsed: the printable bytes after a removed ESC (such as `]8;;http://…`)
|
|
356
|
+
may remain visible. They are harmless text. A typed `Key.char` must be exactly one printable code
|
|
357
|
+
point or it is ignored.
|
|
358
|
+
|
|
359
|
+
The renderer runs the shared conformance kit (`@loomwork/ui-renderer-conformance`) in
|
|
360
|
+
`src/conformance.test.ts`.
|
|
361
|
+
|
|
362
|
+
## See also
|
|
363
|
+
|
|
364
|
+
- [../core/README.md](../core/README.md) — the framework kinds and number semantics this package
|
|
365
|
+
renders, and `checkKindValue`.
|
|
366
|
+
- [../hydration/README.md](../hydration/README.md) — the `HydrationStore` and projection state
|
|
367
|
+
`renderExperience`/`watch` read.
|
|
368
|
+
- [../renderer-lit/README.md](../renderer-lit/README.md) — the DOM counterpart surface, with the
|
|
369
|
+
same numeric formatting.
|
|
370
|
+
- [../../README.md](../../README.md) — root overview.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createLoomUiTerminalRegistry, loomUiTerminalRegistry, sparkline, terminalRegistry, } from "./registry";
|
|
2
|
+
export { DEFAULT_TERMINAL_LOCALE } from "./registry";
|
|
3
|
+
export { kindValueCheck, kindValueCheckFor, renderExperience } from "./status";
|
|
4
|
+
export { watch } from "./watch";
|
|
5
|
+
export { decodeKeys, diffFrames, interactiveTerminal, paintPatches } from "./interactive";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACtB,SAAS,EACT,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE/E,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { stripUnsafeChars } from "@loomwork/ui-core/chat-ui";
|
|
2
|
+
/**
|
|
3
|
+
* Make text inert for a terminal: core's ONE unsafe set (`stripUnsafeChars` — the same class
|
|
4
|
+
* `sanitizeChatText` removes: C0/C1 controls incl. ESC and NUL, bidi overrides/isolates, zero-width
|
|
5
|
+
* and invisible marks, soft hyphen, TAG characters), plus tab, carriage return and the Unicode line
|
|
6
|
+
* separators. Newline is the only control kept: it is the frame's own line structure.
|
|
7
|
+
*
|
|
8
|
+
* It STRIPS; it does not parse. The bytes after a stripped ESC are ordinary printable text, so
|
|
9
|
+
* `ESC ]8;;http://x BEL` leaves a harmless visible `]8;;http://x`. Idempotent; printable text is
|
|
10
|
+
* never touched.
|
|
11
|
+
*/
|
|
12
|
+
export const inert = (text) => stripUnsafeChars(text).replace(/[\t\r\u{2028}\u{2029}]/gu, "");
|
|
13
|
+
//# sourceMappingURL=inert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inert.js","sourceRoot":"","sources":["../../src/inert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAE7D;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,IAAY,EAAU,EAAE,CAC5C,gBAAgB,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,0BAA0B,EAAE,EAAE,CAAC,CAAC"}
|