@weft-js/warp 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 +16 -0
- package/README.md +55 -2
- package/package.json +43 -4
- package/plugin.d.mts +30 -0
- package/plugin.mjs +358 -0
- package/src/Accordion.tsx +58 -0
- package/src/Alert.tsx +25 -0
- package/src/Badge.tsx +17 -0
- package/src/Button.tsx +100 -0
- package/src/Calendar.tsx +162 -0
- package/src/Canvas.tsx +110 -0
- package/src/Card.tsx +29 -0
- package/src/Chart.tsx +133 -0
- package/src/Charts.tsx +69 -0
- package/src/Chip.tsx +27 -0
- package/src/Choice.tsx +105 -0
- package/src/CodeBlock.tsx +65 -0
- package/src/CodeInput.tsx +130 -0
- package/src/Combobox.tsx +113 -0
- package/src/CommandPalette.tsx +348 -0
- package/src/DataGrid.tsx +561 -0
- package/src/DateField.tsx +221 -0
- package/src/Dither.tsx +129 -0
- package/src/Drawer.tsx +41 -0
- package/src/DurationField.tsx +98 -0
- package/src/Feed.tsx +74 -0
- package/src/Field.tsx +147 -0
- package/src/Icon.tsx +8 -0
- package/src/Kbd.tsx +6 -0
- package/src/KeyValueEditor.tsx +114 -0
- package/src/Layout.tsx +109 -0
- package/src/Markdown.tsx +63 -0
- package/src/MentionsInput.tsx +150 -0
- package/src/Menu.tsx +111 -0
- package/src/Modal.tsx +40 -0
- package/src/MonthPicker.tsx +190 -0
- package/src/Nav.tsx +20 -0
- package/src/NumberField.tsx +78 -0
- package/src/Pagination.tsx +122 -0
- package/src/PaymentCard.tsx +63 -0
- package/src/Picker.tsx +120 -0
- package/src/PlaceField.tsx +126 -0
- package/src/Popover.tsx +34 -0
- package/src/Progress.tsx +26 -0
- package/src/RecurrenceField.tsx +126 -0
- package/src/RichSelect.tsx +243 -0
- package/src/RichTextEditor.tsx +207 -0
- package/src/ScrollGrid.tsx +190 -0
- package/src/Segments.tsx +70 -0
- package/src/Select.tsx +192 -0
- package/src/Shape.tsx +20 -0
- package/src/Shell.tsx +308 -0
- package/src/SideNav.tsx +147 -0
- package/src/SignaturePad.tsx +120 -0
- package/src/Skeleton.tsx +14 -0
- package/src/Slider.tsx +42 -0
- package/src/Spinner.tsx +7 -0
- package/src/Table.tsx +528 -0
- package/src/Tabs.tsx +67 -0
- package/src/Tag.tsx +15 -0
- package/src/Ticker.tsx +36 -0
- package/src/TimeField.tsx +237 -0
- package/src/Toast.tsx +56 -0
- package/src/Toggle.tsx +42 -0
- package/src/Tooltip.tsx +32 -0
- package/src/Type.tsx +46 -0
- package/src/VirtualList.tsx +386 -0
- package/src/admin/ApiKeys.tsx +136 -0
- package/src/admin/Audit.tsx +93 -0
- package/src/admin/Billing.tsx +123 -0
- package/src/admin/Cart.tsx +96 -0
- package/src/admin/Checkout.tsx +300 -0
- package/src/admin/CsvImport.tsx +174 -0
- package/src/admin/Danger.tsx +110 -0
- package/src/admin/Integrations.tsx +200 -0
- package/src/admin/InviteDialog.tsx +64 -0
- package/src/admin/Members.tsx +186 -0
- package/src/admin/Order.tsx +63 -0
- package/src/admin/PlainTable.tsx +22 -0
- package/src/admin/Plans.tsx +167 -0
- package/src/admin/SaveBar.tsx +112 -0
- package/src/admin/Secret.tsx +37 -0
- package/src/admin/Settings.tsx +76 -0
- package/src/admin/StepBar.tsx +35 -0
- package/src/admin/TagsInput.tsx +79 -0
- package/src/admin/atoms.tsx +33 -0
- package/src/admin/index.ts +16 -0
- package/src/agent/Actions.tsx +85 -0
- package/src/agent/AgentPlan.tsx +49 -0
- package/src/agent/ArtifactPanel.tsx +80 -0
- package/src/agent/Assistant.tsx +96 -0
- package/src/agent/Checkpoint.tsx +14 -0
- package/src/agent/Cite.tsx +39 -0
- package/src/agent/ContextMeter.tsx +31 -0
- package/src/agent/Conversation.tsx +34 -0
- package/src/agent/Message.tsx +53 -0
- package/src/agent/ModelPicker.tsx +65 -0
- package/src/agent/Prompt.tsx +197 -0
- package/src/agent/Prose.tsx +54 -0
- package/src/agent/Question.tsx +52 -0
- package/src/agent/Reasoning.tsx +19 -0
- package/src/agent/Shimmer.tsx +7 -0
- package/src/agent/Sources.tsx +23 -0
- package/src/agent/SpeechInput.tsx +102 -0
- package/src/agent/Suggestions.tsx +22 -0
- package/src/agent/ToolCall.tsx +91 -0
- package/src/agent/TraceViewer.tsx +55 -0
- package/src/agent/bits.tsx +40 -0
- package/src/agent/index.ts +22 -0
- package/src/bind.ts +42 -0
- package/src/collab/AutoTrack.tsx +42 -0
- package/src/collab/Comments.tsx +94 -0
- package/src/collab/Composer.tsx +173 -0
- package/src/collab/ConversationList.tsx +73 -0
- package/src/collab/Cursors.tsx +35 -0
- package/src/collab/DataIntegrity.tsx +45 -0
- package/src/collab/Kanban.tsx +185 -0
- package/src/collab/Lock.tsx +23 -0
- package/src/collab/PresenceStack.tsx +67 -0
- package/src/collab/ShareDialog.tsx +118 -0
- package/src/collab/Status.tsx +42 -0
- package/src/collab/SyncedForm.tsx +19 -0
- package/src/collab/Thread.tsx +273 -0
- package/src/collab/VersionHistory.tsx +49 -0
- package/src/collab/ViewportRail.tsx +49 -0
- package/src/collab/index.ts +16 -0
- package/src/collab/peer.tsx +16 -0
- package/src/fields.ts +5 -0
- package/src/flow/AuthScreen.tsx +40 -0
- package/src/flow/AuthSteps.tsx +120 -0
- package/src/flow/Checklist.tsx +84 -0
- package/src/flow/ConnBanner.tsx +147 -0
- package/src/flow/Flow.tsx +63 -0
- package/src/flow/MfaSetup.tsx +103 -0
- package/src/flow/Otp.tsx +114 -0
- package/src/flow/ReauthDialog.tsx +47 -0
- package/src/flow/ShortcutsOverlay.tsx +60 -0
- package/src/flow/SignGate.tsx +98 -0
- package/src/flow/SignIn.tsx +80 -0
- package/src/flow/SignUp.tsx +71 -0
- package/src/flow/Sso.tsx +38 -0
- package/src/flow/SystemPage.tsx +247 -0
- package/src/flow/Tour.tsx +108 -0
- package/src/flow/index.ts +17 -0
- package/src/flow/kit.tsx +170 -0
- package/src/index.ts +74 -0
- package/src/layout.css +4 -0
- package/src/links.css +4 -0
- package/src/live/ActivityFeed.tsx +49 -0
- package/src/live/Bell.tsx +62 -0
- package/src/live/Connection.tsx +49 -0
- package/src/live/Inbox.tsx +220 -0
- package/src/live/MeetingRoom.tsx +298 -0
- package/src/live/NotificationPreferences.tsx +193 -0
- package/src/live/Prejoin.tsx +140 -0
- package/src/live/PushSoftAsk.tsx +37 -0
- package/src/live/TransportPicker.tsx +163 -0
- package/src/live/index.ts +12 -0
- package/src/live/notify.tsx +16 -0
- package/src/live/shared.tsx +39 -0
- package/src/parts.tsx +93 -0
- package/src/schedule/Agenda.tsx +173 -0
- package/src/schedule/AvailabilityEditor.tsx +164 -0
- package/src/schedule/BookingCalendar.tsx +132 -0
- package/src/schedule/Calendar.tsx +746 -0
- package/src/schedule/EventEditor.tsx +87 -0
- package/src/schedule/EventPopover.tsx +65 -0
- package/src/schedule/MeetingPlanner.tsx +214 -0
- package/src/schedule/MoveSheet.tsx +78 -0
- package/src/schedule/YearView.tsx +163 -0
- package/src/schedule/bits.tsx +51 -0
- package/src/schedule/index.ts +10 -0
- package/src/skeleton-css.ts +4 -0
- package/src/styles.ts +10 -0
- package/src/tokens.d.ts +2 -0
- package/src/type.css +15 -0
- package/src/ui.ts +26 -0
- package/src/use-ui.ts +10 -0
- package/tailwind.css +38 -0
- package/tokens/color.mjs +66 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { MicIcon } from '@weft-js/warp-icons'
|
|
2
|
+
import { announce, dictation, formatClock, listMics, speechWave, type DictationEngine, type DictationEvents, type DictationState } from '@weft-js/warp-core'
|
|
3
|
+
import { derived, onCleanup, onMount, state, untrack } from '@weft-js/core'
|
|
4
|
+
import { Alert } from '../Alert'
|
|
5
|
+
import { Button } from '../Button'
|
|
6
|
+
import { Select } from '../Select'
|
|
7
|
+
import { use } from '@weft-js/warp-core/weft'
|
|
8
|
+
import { Sweep, useSweep } from './bits'
|
|
9
|
+
import '../styles'
|
|
10
|
+
import '@weft-js/warp-core/styles/app/speech.css'
|
|
11
|
+
|
|
12
|
+
const BARS = Array.from({ length: 40 }, (_, i) => i)
|
|
13
|
+
const DEFAULT_MIC = [{ value: '', label: 'Default microphone' }]
|
|
14
|
+
|
|
15
|
+
/** Props for SpeechInput. */
|
|
16
|
+
export interface SpeechInputProps {
|
|
17
|
+
/** Writes the transcript somewhere: the Prompt's `control.insert`. Nothing is sent until then. */
|
|
18
|
+
onInsert?: (text: string) => void
|
|
19
|
+
mics?: { value: string; label: string }[]
|
|
20
|
+
/** Replaces the browser's speech recognition. */
|
|
21
|
+
engine?: (on: DictationEvents) => DictationEngine
|
|
22
|
+
/** The state and words to start from (a demo, a test). */
|
|
23
|
+
initial?: { state?: DictationState | undefined; final?: string; interim?: string; seconds?: number } | undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* SpeechInput + MicSelector (07 §6.4): the mic Select and input level, then Record and Stop on one button. Listening
|
|
28
|
+
* holds the strip's sweep, moves the wave and counts; words arrive grey while they might change and ink once final. A
|
|
29
|
+
* blocked mic is a caution Alert with the fix, not a dead button. Uses the browser's speech recognition unless given an `engine`.
|
|
30
|
+
*/
|
|
31
|
+
export function SpeechInput({ onInsert, mics, engine, initial }: SpeechInputProps) {
|
|
32
|
+
const start = untrack(() => initial)
|
|
33
|
+
const mode = state<DictationState>(start?.state ?? 'idle')
|
|
34
|
+
const final = state(start?.final ?? '')
|
|
35
|
+
const interim = state(start?.interim ?? '')
|
|
36
|
+
const seconds = state(start?.seconds ?? 0)
|
|
37
|
+
const level = state(0)
|
|
38
|
+
const found = state<{ value: string; label: string }[]>(DEFAULT_MIC)
|
|
39
|
+
const mic = state('')
|
|
40
|
+
const options = derived(() => mics ?? found.value)
|
|
41
|
+
const on = derived(() => mode.value === 'listening')
|
|
42
|
+
const words = derived(() => (final.value + ' ' + interim.value).trim())
|
|
43
|
+
let root: HTMLElement | undefined
|
|
44
|
+
let wave: HTMLElement | undefined
|
|
45
|
+
let live: DictationEngine | undefined
|
|
46
|
+
let clock: ReturnType<typeof setInterval> | undefined
|
|
47
|
+
useSweep()
|
|
48
|
+
use(speechWave, () => wave, () => level.value)
|
|
49
|
+
const say = (text: string, urgent = false) => announce(root!.ownerDocument, text, urgent)
|
|
50
|
+
const tick = (run: boolean) => {
|
|
51
|
+
clearInterval(clock)
|
|
52
|
+
if (run) { seconds.value = 0; clock = setInterval(() => seconds.value++, 1000) }
|
|
53
|
+
}
|
|
54
|
+
const events: DictationEvents = {
|
|
55
|
+
onState(s) {
|
|
56
|
+
const was = mode.value
|
|
57
|
+
mode.value = s
|
|
58
|
+
tick(s === 'listening')
|
|
59
|
+
if (s === 'listening') say('Listening.')
|
|
60
|
+
if (s === 'blocked') say('Microphone blocked. Allow it from the site settings, then try again.', true)
|
|
61
|
+
if (s === 'unsupported') say('This browser has no dictation.', true)
|
|
62
|
+
if (s === 'idle' && was === 'listening') { final.value = words.value; interim.value = ''; if (final.value) say('Dictation stopped. Transcript ready.') }
|
|
63
|
+
},
|
|
64
|
+
onTranscript(f, i) { final.value = f; interim.value = i },
|
|
65
|
+
onLevel(v) { level.value = v },
|
|
66
|
+
}
|
|
67
|
+
onMount(() => {
|
|
68
|
+
if (!untrack(() => mics)) void listMics(window).then(list => { if (list.length) found.value = list })
|
|
69
|
+
onCleanup(() => { clearInterval(clock); live?.stop() })
|
|
70
|
+
})
|
|
71
|
+
const toggle = () => {
|
|
72
|
+
if (on.value) return live?.stop()
|
|
73
|
+
live ??= (untrack(() => engine) ?? (e => dictation(window, e)))(events)
|
|
74
|
+
final.value = interim.value = ''
|
|
75
|
+
live.start(mic.value || undefined)
|
|
76
|
+
}
|
|
77
|
+
const clear = () => { if (on.value) live?.stop(); final.value = interim.value = '' }
|
|
78
|
+
const insert = () => { onInsert?.(words.value); clear() }
|
|
79
|
+
return (
|
|
80
|
+
<div ref={n => (root = n)} data-scope="speech" data-state={mode.value}>
|
|
81
|
+
<Select label="Microphone" options={options.value} onChange={(e: Event) => { const s = e.currentTarget as HTMLSelectElement; mic.value = s.value; say(`Microphone: ${s.selectedOptions[0]!.textContent}`) }} />
|
|
82
|
+
<div data-part="mic">
|
|
83
|
+
<span data-part="level" aria-hidden="true" style={`--level:${level.value}`}><i></i><i></i><i></i></span>
|
|
84
|
+
<span>{mode.value === 'blocked' ? 'Blocked by the browser' : 'Input level'}</span>
|
|
85
|
+
</div>
|
|
86
|
+
<div data-part="strip" data-sweep="" data-pin={on.value ? '' : undefined}>
|
|
87
|
+
<Sweep />
|
|
88
|
+
<Button data-icon-only="" tone={on.value ? 'critical' : undefined} aria-pressed={on.value ? 'true' : 'false'} label={on.value ? 'Stop dictation' : 'Start dictation'} onClick={toggle}><MicIcon /></Button>
|
|
89
|
+
<span ref={n => (wave = n)} data-part="wave" data-loop="" aria-hidden="true">{BARS.map(b => <i key={b}></i>)}</span>
|
|
90
|
+
<span data-part="time" role="timer" aria-label="Recording time" data-numeric="">{formatClock(seconds.value)}</span>
|
|
91
|
+
</div>
|
|
92
|
+
<div data-part="transcript" role="log" aria-live="polite" aria-label="Transcript">
|
|
93
|
+
{words.value ? <>{final.value} <span data-part="interim">{interim.value}</span></> : <small>Press the mic and speak. Nothing is sent until you insert it.</small>}
|
|
94
|
+
</div>
|
|
95
|
+
<div>
|
|
96
|
+
<Button size="sm" disabled={!words.value || on.value} onClick={insert}>Insert into message</Button> <Button emphasis="ghost" tone="neutral" size="sm" onClick={clear}>Clear</Button>
|
|
97
|
+
</div>
|
|
98
|
+
{mode.value === 'blocked' && <Alert tone="caution" title="The browser is blocking the microphone">Click the lock in the address bar, allow Microphone, then press the mic again. Typing still works.</Alert>}
|
|
99
|
+
{mode.value === 'unsupported' && <Alert tone="info" title="This browser has no dictation">Typing still works.</Alert>}
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Sweep, useSweep } from './bits'
|
|
2
|
+
import '../styles'
|
|
3
|
+
import '@weft-js/warp-core/styles/app/suggestions.css'
|
|
4
|
+
|
|
5
|
+
/** A prompt offered as a starting point. */
|
|
6
|
+
export type Suggestion = { id?: string; title: string; description?: string | undefined }
|
|
7
|
+
|
|
8
|
+
/** Starter prompts (sweep cards) on an empty conversation, chips after a turn (07 §1.3). Picking one sends it. */
|
|
9
|
+
export function Suggestions({ items, variant, onPick, ...rest }: { items: Suggestion[]; variant?: 'cards' | 'chips' | undefined; onPick?: (s: Suggestion) => void; [attr: string]: unknown }) {
|
|
10
|
+
useSweep()
|
|
11
|
+
return (
|
|
12
|
+
<div data-scope="suggestions" data-variant={variant === 'chips' ? 'chips' : undefined} {...rest}>
|
|
13
|
+
{items.map(s => (
|
|
14
|
+
<button key={s.id ?? s.title} type="button" data-sweep="" onClick={() => onPick?.(s)}>
|
|
15
|
+
<Sweep />
|
|
16
|
+
<span>{s.title}</span>
|
|
17
|
+
{s.description !== undefined && <small>{s.description}</small>}
|
|
18
|
+
</button>
|
|
19
|
+
))}
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { AlertIcon, CheckIcon, RightIcon, XIcon } from '@weft-js/warp-icons'
|
|
2
|
+
import { announce, argsDiff, installSweep, toolStatus, uid, type AgentTool } from '@weft-js/warp-core'
|
|
3
|
+
import { derived, onMount, state, untrack, watch } from '@weft-js/core'
|
|
4
|
+
import { Button } from '../Button'
|
|
5
|
+
import { Checkbox } from '../Choice'
|
|
6
|
+
import { Sweep } from './bits'
|
|
7
|
+
import '../styles'
|
|
8
|
+
import '@weft-js/warp-core/styles/components/spinner.css'
|
|
9
|
+
import '@weft-js/warp-core/styles/app/tool-call.css'
|
|
10
|
+
|
|
11
|
+
/** Props for ToolCall. */
|
|
12
|
+
export interface ToolCallProps {
|
|
13
|
+
call: AgentTool
|
|
14
|
+
/** Called with the arguments as edited, and whether "always allow" is ticked. Approval never auto-runs. */
|
|
15
|
+
onApprove?: (args: string, always: boolean) => void
|
|
16
|
+
/** Called when the call is denied. */
|
|
17
|
+
onDeny?: () => void
|
|
18
|
+
/** The "always allow" line, scoped to tool and resource: "Always allow edits in src/calendar". */
|
|
19
|
+
alwaysAllow?: string | undefined
|
|
20
|
+
/** Was there on load: takes no focus and announces nothing. */
|
|
21
|
+
quiet?: boolean | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* ToolCall (07 §1.2, §1.6): running carries a light on its bottom edge, and every other state is an icon plus words.
|
|
26
|
+
* Approval opens the card with editable arguments and a diff of the edit, takes focus and announces assertively. The
|
|
27
|
+
* summary reads with its state ("run_tests, failed, exit 1"); settling confirms one sweep.
|
|
28
|
+
*/
|
|
29
|
+
export function ToolCall({ call, onApprove, onDeny, alwaysAllow, quiet }: ToolCallProps) {
|
|
30
|
+
const id = uid('tool')
|
|
31
|
+
const meta = derived(() => toolStatus(call))
|
|
32
|
+
const st = derived(() => call.state)
|
|
33
|
+
const args = state(untrack(() => call.args))
|
|
34
|
+
const always = state(false)
|
|
35
|
+
const diff = derived(() => argsDiff(call.args, args.value).join('\n'))
|
|
36
|
+
let frame: HTMLElement | undefined
|
|
37
|
+
let head: HTMLElement | undefined
|
|
38
|
+
let ok: HTMLElement | undefined
|
|
39
|
+
onMount(() => {
|
|
40
|
+
const asked = () => queueMicrotask(() => {
|
|
41
|
+
ok?.focus({ preventScroll: true })
|
|
42
|
+
if (frame) announce(frame.ownerDocument, `The assistant wants to run ${untrack(() => call.name)}. Approve or deny.`, true)
|
|
43
|
+
})
|
|
44
|
+
watch(() => st.value, (s, prev) => {
|
|
45
|
+
if (s === 'approval') { args.value = untrack(() => call.args); if (prev !== undefined || !untrack(() => quiet)) asked() }
|
|
46
|
+
if (prev !== undefined && s === 'done') {
|
|
47
|
+
installSweep().confirm(frame)
|
|
48
|
+
announce(frame!.ownerDocument, `${untrack(() => call.name)} finished.`)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
const leave = (then: () => void) => { head?.focus({ preventScroll: true }); then() }
|
|
53
|
+
return (
|
|
54
|
+
<div ref={n => (frame = n)} data-part="tool-frame" data-sweep="">
|
|
55
|
+
<Sweep />
|
|
56
|
+
<details data-scope="tool-call" data-state={st.value} open={st.value === 'approval'}>
|
|
57
|
+
<summary ref={n => (head = n)}>
|
|
58
|
+
<span>
|
|
59
|
+
{st.value === 'running' && <span data-scope="spinner" style="--_size:13px;color:var(--warp-color-accent-text)"></span>}
|
|
60
|
+
{st.value === 'done' && <CheckIcon style="color:var(--warp-color-positive-text)" />}
|
|
61
|
+
{st.value === 'approval' && <AlertIcon style="color:var(--warp-color-caution-text)" />}
|
|
62
|
+
{(st.value === 'failed' || st.value === 'denied') && <XIcon style="color:var(--warp-color-critical-text)" />}
|
|
63
|
+
</span>
|
|
64
|
+
<span data-part="name">{call.name}</span>
|
|
65
|
+
<span data-part="sr-only">{meta.value.word}</span>
|
|
66
|
+
<span data-part="arg" aria-hidden="true">{call.args}</span>
|
|
67
|
+
<span data-part="dur" data-numeric="">{meta.value.status}</span>
|
|
68
|
+
<RightIcon data-part="chev" />
|
|
69
|
+
</summary>
|
|
70
|
+
<div data-part="io">
|
|
71
|
+
<span>Input</span>
|
|
72
|
+
<pre>{call.args}</pre>
|
|
73
|
+
<span>Output</span>
|
|
74
|
+
<pre>{call.output ?? (st.value === 'approval' ? 'Waiting for approval' : st.value === 'denied' ? 'Denied. Nothing was changed.' : '…')}</pre>
|
|
75
|
+
</div>
|
|
76
|
+
{st.value === 'approval' && (
|
|
77
|
+
<div data-part="approve" role="group" aria-label={`Approve ${call.name}`}>
|
|
78
|
+
<span data-part="eyebrow" id={`${id}-a`}>Arguments · editable</span>
|
|
79
|
+
<textarea aria-labelledby={`${id}-a`} spellcheck="false" value={args.value} onInput={e => (args.value = (e.currentTarget).value)}></textarea>
|
|
80
|
+
{diff.value && <section data-part="io"><span>Your edit</span><pre>{diff.value}</pre></section>}
|
|
81
|
+
<div>
|
|
82
|
+
<Button size="sm" ref={n => (ok = n)} onClick={() => leave(() => onApprove?.(args.value, always.value))}><CheckIcon />Approve</Button>
|
|
83
|
+
<Button emphasis="outline" tone="neutral" size="sm" onClick={() => leave(() => onDeny?.())}>Deny</Button>
|
|
84
|
+
{alwaysAllow !== undefined && <Checkbox checked={always.value} onChange={(e: Event & { currentTarget: HTMLInputElement }) => (always.value = e.currentTarget.checked)}>{alwaysAllow}</Checkbox>}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</details>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { formatSeconds, traceKey, traceRows, traceSummary, type TraceSpan } from '@weft-js/warp-core'
|
|
2
|
+
import { derived, state, untrack } from '@weft-js/core'
|
|
3
|
+
import '../styles'
|
|
4
|
+
import '@weft-js/warp-core/styles/app/tokens.css'
|
|
5
|
+
import '@weft-js/warp-core/styles/app/base.css'
|
|
6
|
+
import '@weft-js/warp-core/styles/app/trace.css'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* TraceViewer (07 §1.3): nested spans on a shared time axis, an APG treegrid. One tab stop; ↑/↓, Home/End move, →/←
|
|
10
|
+
* expand, collapse and step in and out, Enter toggles. The line above follows the focused span's time, tokens and cost.
|
|
11
|
+
* Live: pass new `spans`, and rows patch by id.
|
|
12
|
+
*/
|
|
13
|
+
export function TraceViewer({ spans, label = 'Spans' }: { spans: TraceSpan[]; label?: string | undefined }) {
|
|
14
|
+
const shut = state<ReadonlySet<string>>(new Set())
|
|
15
|
+
const rows = derived(() => traceRows(spans, shut.value))
|
|
16
|
+
const at = state<string | undefined>(undefined)
|
|
17
|
+
const stop = derived(() => rows.value.find(r => r.id === at.value) ?? rows.value[0])
|
|
18
|
+
let grid: HTMLElement | undefined
|
|
19
|
+
const toggle = (id: string) => {
|
|
20
|
+
const next = new Set(shut.value)
|
|
21
|
+
if (!next.delete(id)) next.add(id)
|
|
22
|
+
shut.value = next
|
|
23
|
+
}
|
|
24
|
+
const onKey = (e: KeyboardEvent) => {
|
|
25
|
+
const list = rows.value
|
|
26
|
+
const i = list.findIndex(r => r.id === stop.value?.id)
|
|
27
|
+
if (i < 0) return
|
|
28
|
+
const rtl = getComputedStyle(e.currentTarget as Element).direction === 'rtl'
|
|
29
|
+
const go = traceKey(e.key, list, i, rtl)
|
|
30
|
+
if (!go) return
|
|
31
|
+
e.preventDefault()
|
|
32
|
+
if (go.toggle) toggle(go.toggle)
|
|
33
|
+
if (go.focus) {
|
|
34
|
+
at.value = go.focus
|
|
35
|
+
queueMicrotask(() => grid?.querySelector<HTMLElement>(`[data-id="${untrack(() => at.value)}"]`)?.focus())
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return (
|
|
39
|
+
<div style="display:grid;gap:14px">
|
|
40
|
+
<div style="display:flex;justify-content:space-between;gap:8px">
|
|
41
|
+
<span data-part="eyebrow">{label}</span>
|
|
42
|
+
<small data-numeric="">{stop.value ? traceSummary(stop.value) : ''}</small>
|
|
43
|
+
</div>
|
|
44
|
+
<div ref={n => (grid = n)} data-scope="trace" role="treegrid" aria-label={label} aria-readonly="true" onKeyDown={onKey}>
|
|
45
|
+
{rows.value.map(r => (
|
|
46
|
+
<div key={r.id} data-id={r.id} role="row" aria-level={r.level} aria-posinset={r.pos} aria-setsize={r.size} aria-expanded={r.branch ? (r.open ? 'true' : 'false') : undefined} aria-label={`${r.name}, ${traceSummary(r)}`} tabindex={r.id === stop.value?.id ? '0' : '-1'} onFocus={() => (at.value = r.id)} onClick={() => r.branch && toggle(r.id)}>
|
|
47
|
+
<span role="gridcell" style={`--_d:${r.level - 1}`}>{r.branch ? (r.open ? '▾ ' : '▸ ') : ''}{r.name}</span>
|
|
48
|
+
<i role="gridcell" style={`--_x:${r.x};--_w:${r.w};--_c:var(--warp-color-data-${r.color})`}></i>
|
|
49
|
+
<em role="gridcell" data-numeric="">{formatSeconds(r.ms)}</em>
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// What the agent pack's components share: glyphs from the app specimen, the sweep's two parts, an icon button, a value
|
|
2
|
+
// held in a State or a plain prop, and the thin progress bar.
|
|
3
|
+
import { type IconComponent } from '@weft-js/warp-icons'
|
|
4
|
+
import { installSweep } from '@weft-js/warp-core'
|
|
5
|
+
import { Box, onMount, state, untrack, type State } from '@weft-js/core'
|
|
6
|
+
import { Button } from '../Button'
|
|
7
|
+
import '@weft-js/warp-core/styles/app/tokens.css'
|
|
8
|
+
import '@weft-js/warp-core/styles/app/base.css'
|
|
9
|
+
import '@weft-js/warp-core/styles/components/progress.css'
|
|
10
|
+
|
|
11
|
+
/** The two sweep parts a [data-sweep] host carries. */
|
|
12
|
+
export const Sweep = () => <><span data-part="sweep-base" aria-hidden="true"></span><span data-part="sweep" aria-hidden="true"></span></>
|
|
13
|
+
|
|
14
|
+
/** Follow the sweep machine; for hosts this pack draws by hand. */
|
|
15
|
+
export const useSweep = () => onMount(() => { installSweep() })
|
|
16
|
+
|
|
17
|
+
/** A small ghost icon button, named by `label`. */
|
|
18
|
+
export function IconButton({ icon, label, ...rest }: { icon: IconComponent; label: string; [attr: string]: unknown }) {
|
|
19
|
+
return <Button emphasis="ghost" tone="neutral" size="sm" label={label} data-icon-only="" {...rest}>{icon({})}</Button>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A thin determinate bar; `value` is 0 to 1. */
|
|
23
|
+
export function Bar({ label, value }: { label: string; value: number }) {
|
|
24
|
+
return <div data-scope="progress" data-part="root" role="progressbar" aria-label={label} aria-valuemin="0" aria-valuemax="100" aria-valuenow={Math.round(value * 100)} style={`--_v:${value}`}><div data-part="track"><div data-part="range"></div></div></div>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One read and write over a State to bind or a plain value with its change callback. */
|
|
28
|
+
export function held<T>(value: () => State<T> | T | undefined, fallback: T, onChange?: () => ((v: T) => void) | undefined) {
|
|
29
|
+
const box = (v: unknown): v is State<T> => v instanceof Box
|
|
30
|
+
const own = state<T>(untrack(() => { const v = value(); return box(v) || v === undefined ? fallback : v }))
|
|
31
|
+
return {
|
|
32
|
+
get value(): T { const v = value(); return box(v) ? v.value : v !== undefined ? v : own.value },
|
|
33
|
+
set(next: T) {
|
|
34
|
+
const v = untrack(value)
|
|
35
|
+
if (box(v)) v.value = next
|
|
36
|
+
else own.value = next
|
|
37
|
+
untrack(onChange ?? (() => undefined))?.(next)
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The agent pack: see docs/design/warp. One file per component family; each imports only its own CSS.
|
|
2
|
+
export { Conversation } from './Conversation'
|
|
3
|
+
export { Message as UserMessage } from './Message'
|
|
4
|
+
export { Assistant, type AssistantProps } from './Assistant'
|
|
5
|
+
export { Reasoning } from './Reasoning'
|
|
6
|
+
export { ToolCall, type ToolCallProps } from './ToolCall'
|
|
7
|
+
export { Cite } from './Cite'
|
|
8
|
+
export { Sources } from './Sources'
|
|
9
|
+
export { Prose } from './Prose'
|
|
10
|
+
export { AgentPlan, type QueuedMessage } from './AgentPlan'
|
|
11
|
+
export { Prompt, type PromptProps, type PromptTool, type PromptFile, type PromptSuggestion, type PromptControl } from './Prompt'
|
|
12
|
+
export { ModelPicker, type ModelOption } from './ModelPicker'
|
|
13
|
+
export { MessageActions, BranchSwitcher, type MessageActionsProps, type Feedback } from './Actions'
|
|
14
|
+
export { Suggestions, type Suggestion } from './Suggestions'
|
|
15
|
+
export { Question } from './Question'
|
|
16
|
+
export { Checkpoint } from './Checkpoint'
|
|
17
|
+
export { Shimmer } from './Shimmer'
|
|
18
|
+
export { ContextMeter, type ContextUse } from './ContextMeter'
|
|
19
|
+
export { TraceViewer } from './TraceViewer'
|
|
20
|
+
export { ArtifactPanel, type ArtifactVersion } from './ArtifactPanel'
|
|
21
|
+
export { SpeechInput, type SpeechInputProps } from './SpeechInput'
|
|
22
|
+
export { foldAnswer, type Answer, type AgentEvent, type AgentTool, type AgentSource, type AgentQuestion, type PlanStep, type TraceSpan } from '@weft-js/warp-core'
|
package/src/bind.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ChangeReason } from '@weft-js/warp-core'
|
|
2
|
+
import { Box, state, untrack, type State } from '@weft-js/core'
|
|
3
|
+
|
|
4
|
+
/** 04 §1.1: a State to bind, or a plain value (controlled) or defaultValue; `onValueChange` hears every change. */
|
|
5
|
+
export interface Bindable<T> {
|
|
6
|
+
value?: State<T> | T
|
|
7
|
+
defaultValue?: T
|
|
8
|
+
/** Called with each new value, and whether a pointer, a key or code made the change. */
|
|
9
|
+
onValueChange?: (v: T, reason: ChangeReason) => void
|
|
10
|
+
}
|
|
11
|
+
/** An overlay's open state, held by the caller or by the component. */
|
|
12
|
+
export interface Openable {
|
|
13
|
+
open?: State<boolean> | boolean
|
|
14
|
+
/** Starts open, when the component keeps its own open state. */
|
|
15
|
+
defaultOpen?: boolean
|
|
16
|
+
/** Called with `true` when it opens and `false` when it closes. */
|
|
17
|
+
onOpenChange?: (o: boolean) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const isState = (v: unknown): v is State<unknown> => v instanceof Box
|
|
21
|
+
|
|
22
|
+
/** One read/write over the three ways a value can be held. */
|
|
23
|
+
export function bindable<T>(value: () => State<T> | T | undefined, initial: () => T | undefined, fallback: T, onChange?: () => ((v: T, r: ChangeReason) => void) | undefined) {
|
|
24
|
+
const own = state<T>(untrack(() => { const v = value(); return (isState(v) ? undefined : (v)) ?? initial() ?? fallback }))
|
|
25
|
+
const read = (): T => {
|
|
26
|
+
const v = value()
|
|
27
|
+
return isState(v) ? v.value : v !== undefined ? (v) : own.value
|
|
28
|
+
}
|
|
29
|
+
const first = untrack(read)
|
|
30
|
+
const set = (next: T, reason: ChangeReason = 'programmatic') => {
|
|
31
|
+
const v = untrack(value)
|
|
32
|
+
if (isState(v)) v.value = next
|
|
33
|
+
else own.value = next
|
|
34
|
+
untrack(onChange ?? (() => undefined))?.(next, reason)
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
get value(): T { return read() },
|
|
38
|
+
set,
|
|
39
|
+
/** Back to the value it started with: a form's reset. */
|
|
40
|
+
reset: () => { set(first) },
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { announce, autoTrack, firstName, type Peer } from '@weft-js/warp-core'
|
|
2
|
+
import { derived, state } from '@weft-js/core'
|
|
3
|
+
import { Button } from '../Button'
|
|
4
|
+
import { Switch } from '../Choice'
|
|
5
|
+
import { use } from '@weft-js/warp-core/weft'
|
|
6
|
+
import { PeerAvatar } from './peer'
|
|
7
|
+
import '../styles'
|
|
8
|
+
import '@weft-js/warp-core/styles/app/tokens.css'
|
|
9
|
+
import '@weft-js/warp-core/styles/app/auto-track.css'
|
|
10
|
+
|
|
11
|
+
type Track = 'following' | 'paused' | 'off'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* MultiplayerAutoTrack (07 §6.4): a Switch, a status line, and a scroll region that follows the presenter's section
|
|
15
|
+
* (`[data-section]` children). A manual scroll pauses it with Resume; presenter changes are shown, not announced.
|
|
16
|
+
*/
|
|
17
|
+
export function AutoTrack({ presenter, section, label, height = 180, children }: { presenter: Peer | null; section?: string | undefined; label: string; height?: number; children?: unknown }) {
|
|
18
|
+
const mode = state<Track>('following')
|
|
19
|
+
const name = derived(() => (presenter ? firstName(presenter.name) : ''))
|
|
20
|
+
let region: HTMLDivElement | undefined
|
|
21
|
+
use(autoTrack, () => region, () => ({
|
|
22
|
+
following: mode.value === 'following',
|
|
23
|
+
section,
|
|
24
|
+
onPause() {
|
|
25
|
+
mode.value = 'paused'
|
|
26
|
+
announce(region!.ownerDocument, 'Auto-track paused. Resume to follow the presenter again.')
|
|
27
|
+
},
|
|
28
|
+
}))
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<Switch checked={mode.value !== 'off'} onChange={(e: Event) => (mode.value = (e.currentTarget as HTMLInputElement).checked ? 'following' : 'off')}>Follow whoever presents</Switch>
|
|
32
|
+
<div>
|
|
33
|
+
<span data-scope="auto-track" data-state={mode.value} role="status">
|
|
34
|
+
{mode.value === 'following' && presenter && <PeerAvatar peer={presenter} size="sm" />}
|
|
35
|
+
{mode.value === 'following' ? (presenter ? `Following ${name.value} · presenting` : 'Nobody is presenting') : mode.value === 'paused' ? 'Paused: you scrolled' : 'Off'}
|
|
36
|
+
</span>
|
|
37
|
+
{mode.value === 'paused' && <Button emphasis="outline" tone="neutral" size="sm" onClick={() => { mode.value = 'following'; announce(region!.ownerDocument, `Following ${presenter?.name ?? 'the presenter'}`) }}>Resume</Button>}
|
|
38
|
+
</div>
|
|
39
|
+
<div ref={n => (region = n)} tabindex="0" role="region" aria-label={label} style={`block-size:${height}px;overflow:auto`}>{children}</div>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { CheckIcon } from '@weft-js/warp-icons'
|
|
2
|
+
import { announce, arrivalTracker, isSheet, uid, type Peer } from '@weft-js/warp-core'
|
|
3
|
+
import { derived, onMount, state, untrack, watch, type State } from '@weft-js/core'
|
|
4
|
+
import { Button } from '../Button'
|
|
5
|
+
import { Drawer } from '../Drawer'
|
|
6
|
+
import { PeerAvatar, read, SWEEP, type Live } from './peer'
|
|
7
|
+
import '../styles'
|
|
8
|
+
import '@weft-js/warp-core/styles/app/tokens.css'
|
|
9
|
+
import '@weft-js/warp-core/styles/app/base.css'
|
|
10
|
+
import '@weft-js/warp-core/styles/app/comment-thread.css'
|
|
11
|
+
import '@weft-js/warp-core/styles/app/comment-anchor.css'
|
|
12
|
+
|
|
13
|
+
/** One comment in a thread. */
|
|
14
|
+
export interface Comment { id: string; author: Peer; time: string; text: string }
|
|
15
|
+
/** A comment thread and whether it's resolved. */
|
|
16
|
+
export interface CommentThreadData { id: string; comments: Comment[]; resolved?: boolean }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A comment thread (07 §6.2): an article of comments, a reply field and Resolve; resolved collapses to one line with
|
|
20
|
+
* Reopen. A new comment arrives with `data-arrive`; `active` raises it and holds its sweep.
|
|
21
|
+
*/
|
|
22
|
+
export function CommentThread({ thread, active, onReply, onResolve }: { thread: Live<CommentThreadData>; active?: boolean; onReply?: (text: string) => void; onResolve?: (resolved: boolean) => void }) {
|
|
23
|
+
const id = uid('reply')
|
|
24
|
+
const data = derived(() => read(thread))
|
|
25
|
+
const own = state(untrack(() => !!data.value.resolved))
|
|
26
|
+
const resolved = derived(() => own.value)
|
|
27
|
+
const mark = arrivalTracker()
|
|
28
|
+
const fresh = derived(() => mark(data.value.comments.map(c => c.id)))
|
|
29
|
+
const first = derived(() => data.value.comments[0])
|
|
30
|
+
let root: HTMLElement | undefined
|
|
31
|
+
const flip = (on: boolean) => {
|
|
32
|
+
own.value = on
|
|
33
|
+
onResolve?.(on)
|
|
34
|
+
if (root) announce(root.ownerDocument, on ? 'Comment resolved' : 'Comment reopened')
|
|
35
|
+
}
|
|
36
|
+
const reply = (e: KeyboardEvent) => {
|
|
37
|
+
const el = e.currentTarget as HTMLInputElement
|
|
38
|
+
if (e.key !== 'Enter' || !el.value.trim()) return
|
|
39
|
+
e.preventDefault()
|
|
40
|
+
onReply?.(el.value.trim())
|
|
41
|
+
el.value = ''
|
|
42
|
+
}
|
|
43
|
+
return (
|
|
44
|
+
<article ref={n => (root = n)} data-scope="comment-thread" data-c={data.value.id} data-sweep="surface" data-active={active ? '' : undefined} data-pin={active ? '' : undefined} data-resolved={resolved.value ? '' : undefined} aria-label={`Comment from ${first.value?.author.name}`}>
|
|
45
|
+
<SWEEP />
|
|
46
|
+
{resolved.value
|
|
47
|
+
? <div data-part="reply"><span>{first.value?.author.name}: {first.value?.text}</span><Button emphasis="ghost" tone="neutral" size="sm" onClick={() => flip(false)}>Reopen</Button></div>
|
|
48
|
+
: <>
|
|
49
|
+
{data.value.comments.map(c => (
|
|
50
|
+
<div key={c.id} data-part="comment" data-arrive={fresh.value.includes(c.id) ? '' : undefined}>
|
|
51
|
+
<PeerAvatar peer={c.author} size="sm" /><b>{c.author.name}</b><time>{c.time}</time><p>{c.text}</p>
|
|
52
|
+
</div>
|
|
53
|
+
))}
|
|
54
|
+
<div data-part="reply">
|
|
55
|
+
<label data-part="sr-only" for={id}>Reply to {first.value?.author.name}</label>
|
|
56
|
+
<input id={id} placeholder="Reply…" onKeyDown={reply} />
|
|
57
|
+
<Button emphasis="ghost" tone="neutral" size="sm" onClick={() => flip(true)}><CheckIcon /> Resolve</Button>
|
|
58
|
+
</div>
|
|
59
|
+
</>}
|
|
60
|
+
</article>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The highlighted range a thread hangs from (07 §6.2): activates its thread on press and on focus. */
|
|
65
|
+
export function CommentAnchor({ thread, active, children }: { thread: string; active: State<string | null>; children?: unknown }) {
|
|
66
|
+
const on = () => (active.value = thread)
|
|
67
|
+
return <span data-scope="comment-anchor" data-c={thread} data-active={active.value === thread ? '' : undefined} role="button" tabindex="0" aria-pressed={active.value === thread ? 'true' : 'false'} onClick={on} onFocus={on} onKeyDown={(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); on() } }}>{children}</span>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The margin rail of anchored threads (07 §6.2): a complementary landmark of articles. On a phone the rail gives way
|
|
72
|
+
* to a bottom Sheet holding the active thread.
|
|
73
|
+
*/
|
|
74
|
+
export function CommentRail({ threads, active, onReply, onResolve, label = 'Comments' }: { threads: Live<CommentThreadData[]>; active: State<string | null>; onReply?: (thread: string, text: string) => void; onResolve?: (thread: string, resolved: boolean) => void; label?: string | undefined }) {
|
|
75
|
+
const list = derived(() => read(threads))
|
|
76
|
+
const phone = state(false)
|
|
77
|
+
let rail: HTMLElement | undefined
|
|
78
|
+
const sheet = state(false)
|
|
79
|
+
onMount(() => {
|
|
80
|
+
queueMicrotask(() => { if (rail && isSheet(rail)) phone.value = true })
|
|
81
|
+
watch(() => active.value, a => { if (phone.value) sheet.value = a !== null })
|
|
82
|
+
watch(() => sheet.value, on => { if (!on) active.value = null })
|
|
83
|
+
})
|
|
84
|
+
const open = derived(() => list.value.find(t => t.id === active.value))
|
|
85
|
+
const body = (t: CommentThreadData) => <CommentThread thread={t} active={active.value === t.id} onReply={x => onReply?.(t.id, x)} onResolve={r => onResolve?.(t.id, r)} />
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
<aside ref={n => (rail = n)} aria-label={label} hidden={phone.value}>
|
|
89
|
+
{!phone.value && list.value.map(t => <div key={t.id}>{body(t)}</div>)}
|
|
90
|
+
</aside>
|
|
91
|
+
{phone.value && <Drawer open={sheet} title={label} side="bottom">{open.value ? body(open.value) : null}</Drawer>}
|
|
92
|
+
</>
|
|
93
|
+
)
|
|
94
|
+
}
|