@workerdeck/ui 0.23.0 → 1.0.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/build/{SessionPanel-DUt2VzXG.mjs → SessionPanel-DDgBkHsi.mjs} +2151 -3357
- package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
- package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
- package/build/format-B8ZM9LPy.d.mts +16 -0
- package/build/format.d.mts +8 -75
- package/build/format.mjs +2 -2
- package/build/index.d.mts +63 -1138
- package/build/index.mjs +4 -300
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +15 -23
- package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
- package/build/status-C0HSLZas.mjs.map +1 -0
- package/build/workspace.d.mts +5 -126
- package/build/workspace.mjs +11 -143
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/CodeEditor.tsx +31 -103
- package/src/components/agent/Composer.tsx +114 -333
- package/src/components/agent/ContextDialog.tsx +20 -41
- package/src/components/agent/ContextRing.tsx +6 -28
- package/src/components/agent/Conversation.tsx +21 -33
- package/src/components/agent/EditorTabs.tsx +39 -67
- package/src/components/agent/EngineIcon.tsx +29 -72
- package/src/components/agent/EntryIcon.tsx +16 -0
- package/src/components/agent/FileCard.tsx +10 -16
- package/src/components/agent/FileTree.tsx +52 -125
- package/src/components/agent/FileViewer.tsx +30 -60
- package/src/components/agent/HostFilesDialog.tsx +40 -69
- package/src/components/agent/Loader.tsx +3 -24
- package/src/components/agent/McpDialog.tsx +64 -137
- package/src/components/agent/Message.tsx +2 -21
- package/src/components/agent/ModelSelect.tsx +33 -62
- package/src/components/agent/PermissionModeSelect.tsx +21 -79
- package/src/components/agent/PermissionPrompt.tsx +59 -63
- package/src/components/agent/ProjectIcon.tsx +228 -93
- package/src/components/agent/PromptTokenText.tsx +10 -16
- package/src/components/agent/QuestionPrompt.tsx +43 -64
- package/src/components/agent/Reasoning.tsx +13 -16
- package/src/components/agent/Response.tsx +3 -9
- package/src/components/agent/Scrubber.tsx +31 -80
- package/src/components/agent/SessionBrowser.tsx +64 -243
- package/src/components/agent/SessionEmptyState.tsx +10 -40
- package/src/components/agent/SessionInfoDialog.tsx +46 -78
- package/src/components/agent/SessionItem.tsx +53 -320
- package/src/components/agent/SessionList.tsx +23 -39
- package/src/components/agent/SessionPanel.tsx +315 -996
- package/src/components/agent/SessionStatusIcon.tsx +14 -28
- package/src/components/agent/SessionSteps.tsx +34 -187
- package/src/components/agent/SessionWorkspace.tsx +104 -219
- package/src/components/agent/SkillsDialog.tsx +35 -80
- package/src/components/agent/StatusBar.tsx +51 -136
- package/src/components/agent/SubagentStrip.tsx +12 -77
- package/src/components/agent/ToolCallCard.tsx +94 -135
- package/src/components/agent/Transcript.tsx +103 -1288
- package/src/components/agent/TranscriptItemView.tsx +155 -0
- package/src/components/agent/TranscriptRows.tsx +418 -0
- package/src/components/agent/UsageDialog.tsx +7 -29
- package/src/components/agent/UsageMeters.tsx +18 -73
- package/src/components/agent/pulse.tsx +9 -32
- package/src/components/agent/scrubber-marks.ts +33 -103
- package/src/components/agent/status.ts +1 -5
- package/src/components/agent/tool-result-fetch.tsx +3 -25
- package/src/components/agent/tool-result-image.tsx +32 -111
- package/src/components/agent/transcript-rows.ts +41 -118
- package/src/components/agent/transcript-variant.tsx +3 -80
- package/src/components/agent/use-height-epoch.ts +10 -24
- package/src/components/agent/use-path-links.ts +45 -63
- package/src/components/agent/use-subagent-frame.ts +106 -0
- package/src/components/agent/use-transcript-jumps.ts +49 -107
- package/src/components/prompt-area/animated-placeholder.tsx +6 -5
- package/src/components/prompt-area/clipboard-helpers.ts +26 -21
- package/src/components/prompt-area/cursor-helpers.ts +79 -37
- package/src/components/prompt-area/dom-helpers.ts +97 -69
- package/src/components/prompt-area/file-strip.tsx +90 -80
- package/src/components/prompt-area/html-to-markdown.ts +67 -46
- package/src/components/prompt-area/image-strip.tsx +8 -18
- package/src/components/prompt-area/index.ts +2 -15
- package/src/components/prompt-area/prompt-area-engine.ts +100 -115
- package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
- package/src/components/prompt-area/prompt-area.tsx +31 -38
- package/src/components/prompt-area/remove-button.tsx +3 -9
- package/src/components/prompt-area/segment-helpers.ts +4 -12
- package/src/components/prompt-area/trigger-popover.tsx +15 -27
- package/src/components/prompt-area/trigger-presets.ts +0 -10
- package/src/components/prompt-area/types.ts +3 -8
- package/src/components/prompt-area/use-chip-editing.ts +305 -0
- package/src/components/prompt-area/use-markdown-mode.ts +7 -17
- package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
- package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
- package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
- package/src/components/prompt-area/use-prompt-area.ts +170 -896
- package/src/components/prompt-area/use-trigger-search.ts +21 -19
- package/src/components/terminal/PermissionPrompt.tsx +56 -59
- package/src/components/terminal/QuestionPrompt.tsx +28 -79
- package/src/components/terminal/StatusLine.tsx +13 -63
- package/src/components/terminal/TerminalTranscript.tsx +44 -160
- package/src/components/terminal/affordances.tsx +68 -98
- package/src/components/terminal/blocks.ts +20 -156
- package/src/components/terminal/diff.tsx +19 -67
- package/src/components/terminal/height.ts +163 -313
- package/src/components/terminal/image-box.ts +0 -44
- package/src/components/terminal/items.tsx +150 -305
- package/src/components/terminal/markdown.tsx +36 -95
- package/src/components/terminal/press.tsx +26 -53
- package/src/components/terminal/prompt.tsx +40 -170
- package/src/components/terminal/result-preview.ts +8 -56
- package/src/components/terminal/row.tsx +11 -98
- package/src/components/terminal/scrubber.tsx +108 -401
- package/src/components/terminal/surface.tsx +6 -54
- package/src/components/terminal/todos.ts +66 -0
- package/src/components/terminal/tool-run.ts +21 -192
- package/src/components/ui/AlertDialog.tsx +8 -23
- package/src/components/ui/Badge.tsx +18 -29
- package/src/components/ui/Button.tsx +7 -15
- package/src/components/ui/Card.tsx +2 -13
- package/src/components/ui/CodeBlock.tsx +10 -37
- package/src/components/ui/CopyButton.tsx +10 -23
- package/src/components/ui/Dialog.tsx +23 -36
- package/src/components/ui/Empty.tsx +6 -29
- package/src/components/ui/Input.tsx +14 -16
- package/src/components/ui/Menu.tsx +6 -18
- package/src/components/ui/PortalScope.tsx +1 -21
- package/src/components/ui/ProgressRing.tsx +8 -19
- package/src/components/ui/Select.tsx +18 -31
- package/src/components/ui/Sonner.tsx +1 -2
- package/src/components/ui/Spinner.tsx +1 -1
- package/src/components/ui/Splitter.tsx +30 -64
- package/src/components/ui/Textarea.tsx +2 -5
- package/src/components/ui/Tooltip.tsx +3 -13
- package/src/format.ts +0 -9
- package/src/index.ts +16 -113
- package/src/lib/clipboard.ts +8 -28
- package/src/lib/css.ts +8 -0
- package/src/lib/format.ts +101 -86
- package/src/lib/plan-request.ts +16 -0
- package/src/lib/status.ts +45 -95
- package/src/lib/tool-icon.ts +39 -47
- package/src/lib/utils.ts +1 -3
- package/src/styles/terminal.css +81 -49
- package/src/styles/theme.css +96 -32
- package/src/workspace.ts +1 -24
- package/build/SessionPanel-BobynUo4.d.mts +0 -669
- package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
- package/build/format-ljc3lKpA.d.mts +0 -59
- package/build/status-BE-zg88x.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workerdeck/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Styled agent-control component library for WorkerDeck: session panel, transcript with streaming, tool-call cards, permission prompts, session list, composer. Tailwind v4 + Base UI + cva. Pairs with the headless @workerdeck/react layer.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"streamdown": "^2.5.0",
|
|
40
40
|
"tailwind-merge": "^3.6.0",
|
|
41
41
|
"use-stick-to-bottom": "^1.1.6",
|
|
42
|
-
"@workerdeck/client": "0.
|
|
43
|
-
"@workerdeck/
|
|
44
|
-
"@workerdeck/
|
|
42
|
+
"@workerdeck/client": "1.0.0",
|
|
43
|
+
"@workerdeck/react": "1.0.0",
|
|
44
|
+
"@workerdeck/protocol": "1.0.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"monaco-editor": "^0.56.0",
|
|
@@ -4,67 +4,30 @@ import { cn } from '../../lib/utils.ts'
|
|
|
4
4
|
import { Spinner } from '../ui/Spinner.tsx'
|
|
5
5
|
|
|
6
6
|
export interface CodeEditorProps {
|
|
7
|
-
/** Absolute path — decides the language and identifies the model. */
|
|
8
7
|
path: string
|
|
9
|
-
/** Text to show. Applied to the model when it differs from what is on screen,
|
|
10
|
-
* so an external reload lands without fighting the user's cursor. */
|
|
11
8
|
value: string
|
|
12
9
|
onChange?: (value: string) => void
|
|
13
|
-
/** Ctrl/Cmd+S. Wired inside Monaco because the editor swallows keydown. */
|
|
14
10
|
onSave?: () => void
|
|
15
11
|
readOnly?: boolean
|
|
16
12
|
className?: string
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
* Monaco — VS Code's own editor — behind a small React surface.
|
|
21
|
-
*
|
|
22
|
-
* Two deliberate choices, both about `@workerdeck/ui` being a **published
|
|
23
|
-
* library** rather than an app:
|
|
24
|
-
*
|
|
25
|
-
* 1. **Loaded on demand.** The `import()` is inside an effect, so Monaco is a
|
|
26
|
-
* separate chunk that arrives when someone first opens a file. A dashboard
|
|
27
|
-
* that never opens one never pays for it, and Monaco's ~90 language grammars
|
|
28
|
-
* are themselves lazy (each `registerLanguage` carries an `import()` loader),
|
|
29
|
-
* so opening a `.ts` file fetches the TypeScript grammar and nothing else.
|
|
30
|
-
* 2. **No `MonacoEnvironment` is configured here, and none is needed.** Workers
|
|
31
|
-
* in a library become every embedder's bootstrapping problem, and
|
|
32
|
-
* `packages/web` ships prebuilt static files at a domain root, which is
|
|
33
|
-
* exactly where hardcoded worker URLs break. The editor is configured so it
|
|
34
|
-
* never asks for one: `wordBasedSuggestions` and `quickSuggestions` off,
|
|
35
|
-
* no diff editor. A host that wants the worker-backed language services
|
|
36
|
-
* (TypeScript IntelliSense, JSON schema validation) sets `MonacoEnvironment`
|
|
37
|
-
* itself before the first file is opened — Monaco is a singleton and nothing
|
|
38
|
-
* here fights that.
|
|
39
|
-
*
|
|
40
|
-
* One model per path, kept across tab switches, so undo history and view state
|
|
41
|
-
* survive clicking away and back — which is most of what makes tabs feel like
|
|
42
|
-
* tabs rather than like re-opening a file.
|
|
43
|
-
*/
|
|
44
|
-
export function CodeEditor({
|
|
45
|
-
path,
|
|
46
|
-
value,
|
|
47
|
-
onChange,
|
|
48
|
-
onSave,
|
|
49
|
-
readOnly,
|
|
50
|
-
className,
|
|
51
|
-
}: CodeEditorProps) {
|
|
15
|
+
export function CodeEditor({ path, value, onChange, onSave, readOnly, className }: CodeEditorProps) {
|
|
52
16
|
const host = useRef<HTMLDivElement>(null)
|
|
53
17
|
const editor = useRef<Monaco.editor.IStandaloneCodeEditor | null>(null)
|
|
54
18
|
const monaco = useRef<typeof Monaco | null>(null)
|
|
55
19
|
const [ready, setReady] = useState(false)
|
|
56
20
|
const theme = useDocumentTheme()
|
|
57
21
|
|
|
58
|
-
// Callbacks through refs: they change identity every render, and re-creating
|
|
59
|
-
// the editor (or re-registering its listeners) on each one would drop the
|
|
60
|
-
// cursor mid-keystroke.
|
|
61
22
|
const handlers = useRef({ onChange, onSave })
|
|
62
23
|
handlers.current = { onChange, onSave }
|
|
63
24
|
|
|
64
25
|
useEffect(() => {
|
|
65
26
|
let disposed = false
|
|
66
27
|
void loadMonaco().then((api) => {
|
|
67
|
-
if (disposed || !host.current)
|
|
28
|
+
if (disposed || !host.current) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
68
31
|
monaco.current = api
|
|
69
32
|
const instance = api.editor.create(host.current, {
|
|
70
33
|
value,
|
|
@@ -76,14 +39,11 @@ export function CodeEditor({
|
|
|
76
39
|
scrollBeyondLastLine: false,
|
|
77
40
|
fontSize: 12,
|
|
78
41
|
lineHeight: 20,
|
|
79
|
-
fontFamily:
|
|
80
|
-
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace',
|
|
42
|
+
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace',
|
|
81
43
|
renderLineHighlight: 'line',
|
|
82
44
|
smoothScrolling: true,
|
|
83
45
|
padding: { top: 8, bottom: 8 },
|
|
84
46
|
scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },
|
|
85
|
-
// Without a worker there is no word-based suggestion provider to ask, so
|
|
86
|
-
// asking would surface an empty popup on every identifier.
|
|
87
47
|
wordBasedSuggestions: 'off',
|
|
88
48
|
quickSuggestions: false,
|
|
89
49
|
})
|
|
@@ -98,74 +58,62 @@ export function CodeEditor({
|
|
|
98
58
|
})
|
|
99
59
|
return () => {
|
|
100
60
|
disposed = true
|
|
101
|
-
//
|
|
102
|
-
// outlives this mount so that reopening a tab restores its undo stack.
|
|
61
|
+
// The editor, never its models: a model is keyed by path and outlives this mount, which is what restores a reopened tab's undo stack.
|
|
103
62
|
editor.current?.dispose()
|
|
104
63
|
editor.current = null
|
|
105
64
|
setReady(false)
|
|
106
65
|
}
|
|
107
|
-
// Deliberately created once, with no dependencies. Path, value and readOnly
|
|
108
|
-
// are applied by the effects below instead, because re-creating the editor
|
|
109
|
-
// to change any of them would lose the cursor, the scroll position and the
|
|
110
|
-
// undo history.
|
|
111
66
|
}, [])
|
|
112
67
|
|
|
113
|
-
// Swap the model when the focused file changes. One model per path, created
|
|
114
|
-
// lazily and kept, so each tab keeps its own undo history and view state.
|
|
115
68
|
useEffect(() => {
|
|
116
69
|
const api = monaco.current
|
|
117
70
|
const instance = editor.current
|
|
118
|
-
if (!api || !instance || !ready)
|
|
71
|
+
if (!api || !instance || !ready) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
119
74
|
const uri = api.Uri.parse(`workerdeck://host${path}`)
|
|
120
75
|
const model = api.editor.getModel(uri) ?? api.editor.createModel(value, languageOf(path), uri)
|
|
121
|
-
if (instance.getModel() !== model)
|
|
76
|
+
if (instance.getModel() !== model) {
|
|
77
|
+
instance.setModel(model)
|
|
78
|
+
}
|
|
122
79
|
}, [path, ready, value])
|
|
123
80
|
|
|
124
|
-
// Apply an external change — a reload from disk, a revert — without
|
|
125
|
-
// disturbing anything when the text already matches, which is the common case
|
|
126
|
-
// because most changes to `value` are echoes of the user's own typing.
|
|
127
81
|
useEffect(() => {
|
|
128
82
|
const instance = editor.current
|
|
129
|
-
if (!instance || !ready)
|
|
83
|
+
if (!instance || !ready) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
130
86
|
const model = instance.getModel()
|
|
131
|
-
if (!model || model.getValue() === value)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
model.pushEditOperations(
|
|
135
|
-
[],
|
|
136
|
-
[{ range: model.getFullModelRange(), text: value }],
|
|
137
|
-
() => null,
|
|
138
|
-
)
|
|
87
|
+
if (!model || model.getValue() === value) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
model.pushEditOperations([], [{ range: model.getFullModelRange(), text: value }], () => null)
|
|
139
91
|
}, [value, ready])
|
|
140
92
|
|
|
141
93
|
useEffect(() => {
|
|
142
|
-
if (ready)
|
|
94
|
+
if (ready) {
|
|
95
|
+
editor.current?.updateOptions({ readOnly })
|
|
96
|
+
}
|
|
143
97
|
}, [readOnly, ready])
|
|
144
98
|
|
|
145
|
-
// The theme is global to Monaco, not per-editor — `setTheme` is on the
|
|
146
|
-
// namespace for that reason.
|
|
147
99
|
useEffect(() => {
|
|
148
|
-
if (ready)
|
|
100
|
+
if (ready) {
|
|
101
|
+
monaco.current?.editor.setTheme(monacoTheme(theme))
|
|
102
|
+
}
|
|
149
103
|
}, [theme, ready])
|
|
150
104
|
|
|
151
105
|
return (
|
|
152
106
|
<div className={cn('relative min-h-0 min-w-0 flex-1', className)}>
|
|
153
|
-
<div ref={host} className=
|
|
107
|
+
<div ref={host} className="absolute inset-0" />
|
|
154
108
|
{!ready ? (
|
|
155
|
-
<div className=
|
|
156
|
-
<Spinner className=
|
|
109
|
+
<div className="absolute inset-0 flex items-center justify-center bg-bg">
|
|
110
|
+
<Spinner className="size-4 text-fg-4" />
|
|
157
111
|
</div>
|
|
158
112
|
) : null}
|
|
159
113
|
</div>
|
|
160
114
|
)
|
|
161
115
|
}
|
|
162
116
|
|
|
163
|
-
/**
|
|
164
|
-
* Follow the `data-theme` the design tokens already swap on, so the editor is
|
|
165
|
-
* never the one light rectangle in a dark app (or the reverse). An attribute
|
|
166
|
-
* observer rather than a media query: the app has a manual toggle, and the
|
|
167
|
-
* attribute is what that toggle writes.
|
|
168
|
-
*/
|
|
169
117
|
function useDocumentTheme(): string | null {
|
|
170
118
|
const [theme, setTheme] = useState<string | null>(() =>
|
|
171
119
|
typeof document === 'undefined' ? null : document.documentElement.getAttribute('data-theme'),
|
|
@@ -181,26 +129,14 @@ function useDocumentTheme(): string | null {
|
|
|
181
129
|
return theme
|
|
182
130
|
}
|
|
183
131
|
|
|
184
|
-
/** An unset attribute means the host never opted into the token themes, in which
|
|
185
|
-
* case dark matches this package's default surface. */
|
|
186
132
|
function monacoTheme(theme: string | null): string {
|
|
187
133
|
return theme === 'light' ? 'wd-light' : 'wd-dark'
|
|
188
134
|
}
|
|
189
135
|
|
|
190
|
-
/**
|
|
191
|
-
* Load Monaco once, register the themes, and hand back the API.
|
|
192
|
-
*
|
|
193
|
-
* Cached as a promise rather than a value so that several editors mounting in
|
|
194
|
-
* the same frame share one load instead of racing three of them.
|
|
195
|
-
*/
|
|
196
136
|
let monacoPromise: Promise<typeof Monaco> | undefined
|
|
197
137
|
function loadMonaco(): Promise<typeof Monaco> {
|
|
198
138
|
monacoPromise ??= (async () => {
|
|
199
139
|
const api = await import('monaco-editor')
|
|
200
|
-
// Themes that inherit the surrounding surface instead of Monaco's own
|
|
201
|
-
// near-black, so the editor does not sit in the layout as a differently
|
|
202
|
-
// coloured rectangle. Both are defined; the CSS variable decides nothing
|
|
203
|
-
// here, so the panel picks by `prefers-color-scheme` on the document.
|
|
204
140
|
api.editor.defineTheme('wd-dark', {
|
|
205
141
|
base: 'vs-dark',
|
|
206
142
|
inherit: true,
|
|
@@ -218,22 +154,16 @@ function loadMonaco(): Promise<typeof Monaco> {
|
|
|
218
154
|
return monacoPromise
|
|
219
155
|
}
|
|
220
156
|
|
|
221
|
-
/**
|
|
222
|
-
* Monaco's language id for a path.
|
|
223
|
-
*
|
|
224
|
-
* By extension, with a short table for the files that have none — Monaco's own
|
|
225
|
-
* registry is keyed on extensions it knows, and an unmatched file falls through
|
|
226
|
-
* to `plaintext`, which is the honest answer rather than a guess.
|
|
227
|
-
*/
|
|
228
157
|
function languageOf(path: string): string {
|
|
229
158
|
const name = path.slice(path.lastIndexOf('/') + 1)
|
|
230
159
|
const byName = FILENAME_LANGUAGES[name.toLowerCase()]
|
|
231
|
-
if (byName)
|
|
160
|
+
if (byName) {
|
|
161
|
+
return byName
|
|
162
|
+
}
|
|
232
163
|
const extension = name.slice(name.lastIndexOf('.') + 1).toLowerCase()
|
|
233
164
|
return EXTENSION_LANGUAGES[extension] ?? 'plaintext'
|
|
234
165
|
}
|
|
235
166
|
|
|
236
|
-
/** Files whose type is their whole name. */
|
|
237
167
|
const FILENAME_LANGUAGES: Record<string, string> = {
|
|
238
168
|
dockerfile: 'dockerfile',
|
|
239
169
|
makefile: 'plaintext',
|
|
@@ -241,8 +171,6 @@ const FILENAME_LANGUAGES: Record<string, string> = {
|
|
|
241
171
|
'.env': 'shell',
|
|
242
172
|
}
|
|
243
173
|
|
|
244
|
-
/** Extension → Monaco language id. Only where the id differs from the extension
|
|
245
|
-
* or the extension is ambiguous; everything else Monaco resolves itself. */
|
|
246
174
|
const EXTENSION_LANGUAGES: Record<string, string> = {
|
|
247
175
|
ts: 'typescript',
|
|
248
176
|
tsx: 'typescript',
|