@deepwatch/dsh-live 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { toneFor, tokenFor } from '@deepwatch/dsh-client-brand';
3
+ import { describeContinuity, } from '../session.js';
4
+ /** Connection states, mapped onto the brand's status vocabulary. */
5
+ const CONNECTION_STATUS = {
6
+ connecting: 'queued',
7
+ live: 'running',
8
+ reconnecting: 'gap',
9
+ lost: 'unavailable',
10
+ stopped: 'completed',
11
+ };
12
+ /** A millisecond count as a clock reading. */
13
+ function clock(ms) {
14
+ if (ms === null)
15
+ return '—';
16
+ const total = Math.max(0, Math.floor(ms / 1000));
17
+ const seconds = String(total % 60).padStart(2, '0');
18
+ const minutes = String(Math.floor(total / 60) % 60).padStart(2, '0');
19
+ const hours = String(Math.floor(total / 3600)).padStart(2, '0');
20
+ return `${hours}:${minutes}:${seconds}`;
21
+ }
22
+ /**
23
+ * Session, media and wall clocks, side by side.
24
+ *
25
+ * Three readings rather than one, because they answer three questions and
26
+ * disagree constantly during a live observation. A single "time" field would
27
+ * be right about one of them and quietly wrong about the other two.
28
+ */
29
+ export function LiveHeader({ state }) {
30
+ const status = CONNECTION_STATUS[state.connection] ?? 'unavailable';
31
+ return (_jsxs("header", { "data-watch-live-header": "", "data-watch-live-session": state.sessionId, children: [_jsx("span", { "data-watch-field": "target", dir: "ltr", children: state.target }), _jsx("span", { "data-watch-field": "kind", children: state.kind }), _jsx("span", { "data-watch-field": "status", children: state.status }), _jsxs("span", { "data-watch-field": "connection", style: { color: tokenFor(toneFor(status)) }, children: [_jsx("span", { "aria-hidden": "true", children: state.connection === 'live' ? '●' : '▲' }), _jsx("span", { children: state.connection })] }), _jsx("span", { "data-watch-field": "session-clock", dir: "ltr", children: `session ${clock(state.clocks.sessionMs)}` }), _jsx("span", { "data-watch-field": "media-clock", dir: "ltr", children: `media ${clock(state.clocks.mediaMs)}` }), _jsx("span", { "data-watch-field": "wall-clock", dir: "ltr", children: state.clocks.wallMs === null ? 'wall —' : `wall ${new Date(state.clocks.wallMs).toISOString()}` }), _jsx("span", { "data-watch-field": "latency", children: state.clocks.latencyMs === null ? 'latency —' : `latency ${String(state.clocks.latencyMs)}ms` }), _jsx("span", { "data-watch-field": "continuity", children: describeContinuity(state) })] }));
32
+ }
33
+ /**
34
+ * One observed event.
35
+ *
36
+ * A gap draws as a gap — dashed, glyphed and labelled — and is never
37
+ * collapsed into the events around it.
38
+ */
39
+ export function LiveEventRow({ event, onSelect }) {
40
+ const isGap = event.kind === 'gap';
41
+ return (_jsx("li", { "data-watch-live-event": event.kind, "data-watch-seq": String(event.seq), children: _jsxs("button", { type: "button", onClick: () => { onSelect(event); }, style: {
42
+ font: 'inherit',
43
+ color: 'inherit',
44
+ cursor: 'pointer',
45
+ background: isGap ? 'var(--watch-wash-caution)' : 'none',
46
+ border: isGap ? '1px dashed var(--watch-tone-caution)' : '1px solid transparent',
47
+ textAlign: 'start',
48
+ width: '100%',
49
+ }, children: [_jsx("span", { dir: "ltr", style: { fontVariantNumeric: 'tabular-nums' }, children: clock(event.mediaMs) }), isGap && _jsx("span", { "aria-hidden": "true", children: ' ⌇ ' }), _jsx("span", { dir: "auto", children: event.text })] }) }));
50
+ }
51
+ /**
52
+ * The Live mode body.
53
+ *
54
+ * `Resnapshot needed` is rendered as a banner rather than a toast. A transient
55
+ * notification for "your view of this is not continuous" is a notification
56
+ * that will be missed exactly when it matters.
57
+ */
58
+ export function LiveSurface(props) {
59
+ const { state } = props;
60
+ return (_jsxs("section", { "data-watch-live": "", "aria-label": "Live observation", children: [_jsx(LiveHeader, { state: state }), state.needsSnapshot && (_jsxs("p", { role: "alert", "data-watch-live-resnapshot": "", style: { borderInlineStart: '3px solid var(--watch-tone-caution)', paddingInlineStart: '8px' }, children: ['This view is not continuous. ', state.lastError ?? 'The stream did not continue from the last cursor.', ' A fresh snapshot is needed before anything here can be read as unbroken.'] })), _jsxs("div", { "data-watch-live-controls": "", children: [_jsx("button", { type: "button", "data-watch-action": "start", onClick: props.onStart, children: "Start" }), _jsx("button", { type: "button", "data-watch-action": "pin", onClick: props.onPin, children: "Pin moment" }), _jsx("button", { type: "button", "data-watch-action": "finalize", onClick: () => { props.onStop(true); }, children: "Stop and keep" }), _jsx("button", { type: "button", "data-watch-action": "discard", onClick: () => { props.onStop(false); }, children: "Stop and discard" })] }), _jsx("ul", { "data-watch-live-events": "", style: { listStyle: 'none', margin: 0, padding: 0 }, children: state.events.map(event => (_jsx(LiveEventRow, { event: event, onSelect: props.onSelect }, String(event.seq)))) }), state.pinned.length > 0 && (_jsx("ul", { "data-watch-live-pinned": "", "aria-label": "Pinned moments", children: state.pinned.map(moment => (_jsxs("li", { "data-watch-pinned": moment.momentId, children: [_jsx("span", { dir: "ltr", children: clock(moment.atMediaMs) }), _jsx("span", { dir: "auto", children: ` ${moment.note}` })] }, moment.momentId))) })), state.trimmed > 0 && (_jsxs("p", { "data-watch-live-trimmed": String(state.trimmed), children: [`${String(state.trimmed)} earlier event(s) are no longer held in this view. `, 'Gaps and pinned moments were kept.'] }))] }));
61
+ }
62
+ //# sourceMappingURL=components.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The Live surface, registered into DSH's slots.
3
+ *
4
+ * @module @deepwatch/dsh-live/client
5
+ */
6
+ import type { Context } from '@deepseek-ai/cordis';
7
+ export * from './components.js';
8
+ export * from './live-mode.js';
9
+ export * from '../session.js';
10
+ /** Services this half needs before it can register anything. */
11
+ export declare const inject: string[];
12
+ /** Register the Live mode body. */
13
+ export declare function apply(ctx: Context): void;
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The Live surface, registered into DSH's slots.
3
+ *
4
+ * @module @deepwatch/dsh-live/client
5
+ */
6
+ import { LiveModeView } from './live-mode.js';
7
+ export * from './components.js';
8
+ export * from './live-mode.js';
9
+ export * from '../session.js';
10
+ /** Services this half needs before it can register anything. */
11
+ export const inject = ['slots'];
12
+ /** Register the Live mode body. */
13
+ export function apply(ctx) {
14
+ const slots = ctx.slots;
15
+ // Live is a product mode, so it registers as one of DSH's views. The session
16
+ // header turns the registered set into its own tab strip, which is why this
17
+ // is a view rather than a panel Watch would have to place and style itself.
18
+ slots.inject('conversation.view', () => {
19
+ slots.register({ name: 'conversation.view', id: 'live', label: 'Live', order: 30 }, LiveModeView);
20
+ });
21
+ }
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The Live mode body.
3
+ *
4
+ * It lives in the package that owns the capability rather than in the workspace
5
+ * shell. That is structural, not tidiness: the shell provides the scaffold every
6
+ * mode shares, and a mode that also needed something back from its own package
7
+ * made the two depend on each other. TypeScript refused the circular project
8
+ * reference, which was the right answer to the wrong arrangement.
9
+ *
10
+ * Nothing here asks the operating system for anything. Rendering the source list
11
+ * calls no probe and no permission API; a prompt on page load teaches people to
12
+ * click Allow without reading, and after that the prompt means nothing.
13
+ *
14
+ * @module @deepwatch/dsh-live/client/live-mode
15
+ */
16
+ import type { ReactNode } from 'react';
17
+ import type { ModeViewProps } from '@deepwatch/dsh-workspace/surface';
18
+ import type { CaptureReceipt, CaptureState, Observation, PermissionState } from '../capture.js';
19
+ /** A snapshot of a session, flat enough to render without owning the session. */
20
+ export interface LiveSessionView {
21
+ readonly sessionId: string;
22
+ readonly sourceId: string;
23
+ readonly state: CaptureState;
24
+ readonly permission: PermissionState;
25
+ readonly runId: string | null;
26
+ readonly startedAt: string | null;
27
+ readonly observations: readonly Observation[];
28
+ readonly reason: string;
29
+ }
30
+ export interface LiveModeProps extends ModeViewProps {
31
+ /** The session in progress, when there is one. */
32
+ readonly session?: LiveSessionView | null;
33
+ /** Receipts from sessions that have ended, newest first. */
34
+ readonly receipts?: readonly CaptureReceipt[];
35
+ }
36
+ /** The Live mode: what could be observed, and what is being observed. */
37
+ export declare function LiveModeView({ session, receipts }?: LiveModeProps): ReactNode;
38
+ //# sourceMappingURL=live-mode.d.ts.map
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { EmptyState, Facts, ModeSurface, Panel, Unavailable } from '@deepwatch/dsh-workspace/surface';
3
+ import { SOURCES } from '../sources-catalogue.js';
4
+ /** Words for a state, so it is never only a colour. */
5
+ const STATE_WORDS = {
6
+ idle: 'Not started',
7
+ requesting_permission: 'Waiting for your permission',
8
+ starting: 'Starting',
9
+ active: 'Observing',
10
+ paused: 'Paused',
11
+ stopping: 'Stopping',
12
+ stopped: 'Stopped',
13
+ cancelled: 'Cancelled',
14
+ denied: 'Permission refused — nothing was captured',
15
+ unavailable: 'Source unavailable',
16
+ timed_out: 'The source did not start in time',
17
+ failed: 'Failed',
18
+ };
19
+ const PERMISSION_WORDS = {
20
+ not_requested: 'Not requested',
21
+ requested: 'Requested',
22
+ granted: 'Granted',
23
+ denied: 'Refused',
24
+ };
25
+ function toneFor(state) {
26
+ if (state === 'active')
27
+ return 'var(--watch-tone-active)';
28
+ if (['denied', 'failed', 'timed_out', 'unavailable'].includes(state))
29
+ return 'var(--watch-tone-error)';
30
+ if (['paused', 'starting', 'requesting_permission', 'stopping'].includes(state))
31
+ return 'var(--watch-tone-caution)';
32
+ return 'var(--watch-tone-neutral)';
33
+ }
34
+ /** The Live mode: what could be observed, and what is being observed. */
35
+ export function LiveModeView({ session = null, receipts = [] } = {}) {
36
+ return (_jsxs(ModeSurface, { title: "Live", lead: 'A continuous observation over a source, with its clock, its gaps and '
37
+ + 'its freshness. Opening this page starts nothing and asks for nothing.', children: [session === null
38
+ ? (_jsx(EmptyState, { shows: 'An active session: which source is bound, its permission state, '
39
+ + 'the observations arriving with their timestamps, and how it ended.', why: "No live session is running, and no source has been started.", next: [
40
+ 'Choose a source below and start it — each asks for its own permission at that moment, not before.',
41
+ 'Bind ASR or Visual Perception in Settings → Role Bindings if you want a source interpreted rather than only recorded.',
42
+ ] }))
43
+ : (_jsxs(_Fragment, { children: [_jsxs(Panel, { children: [_jsxs("div", { style: { display: 'flex', alignItems: 'baseline', gap: '10px', flexWrap: 'wrap' }, children: [_jsx("strong", { style: { fontSize: '15px', color: toneFor(session.state) }, children: STATE_WORDS[session.state] }), _jsx("span", { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' }, children: session.reason === '' ? session.sourceId : session.reason })] }), _jsx("div", { style: { marginTop: '12px' }, children: _jsx(Facts, { rows: [
44
+ ['Session', _jsx("span", { "data-watch-ltr": true, children: session.sessionId }, "s")],
45
+ ['Source', session.sourceId],
46
+ ['Permission', PERMISSION_WORDS[session.permission]],
47
+ ['Run', session.runId ?? 'Not associated with a run'],
48
+ ['Started', _jsx("span", { "data-watch-ltr": true, children: session.startedAt ?? '—' }, "t")],
49
+ ['Observations', String(session.observations.length)],
50
+ ] }) })] }), _jsx(Panel, { heading: `Observations (${String(session.observations.length)})`, children: session.observations.length === 0
51
+ ? (_jsx("p", { style: { fontSize: '13px', margin: 0, color: 'var(--dsw-alias-label-tertiary)' }, children: "Nothing observed yet. An observation without a timestamp could not be cited, so none is recorded until the clock is running." }))
52
+ : (_jsx("ol", { style: { margin: 0, paddingInlineStart: '20px', fontSize: '12.5px', lineHeight: 1.7 }, children: session.observations.slice(-25).map(observation => (_jsxs("li", { children: [_jsx("span", { "data-watch-ltr": true, style: { color: 'var(--dsw-alias-label-tertiary)' }, children: `+${String(observation.offsetMs)}ms` }), ' ', observation.text] }, observation.observationId))) })) })] })), _jsx(Panel, { heading: "Sources", children: _jsx(Facts, { rows: SOURCES.map(source => [
53
+ source.name,
54
+ _jsxs("span", { children: [source.what, _jsx("br", {}), _jsx("span", { style: { color: 'var(--dsw-alias-label-tertiary)' }, children: source.asks }), source.canAct
55
+ ? (_jsxs(_Fragment, { children: [_jsx("br", {}), _jsx("span", { style: { color: 'var(--watch-tone-caution)' }, children: "This one can act on the world, not only record it." })] }))
56
+ : null] }, source.id),
57
+ ]) }) }), receipts.length === 0
58
+ ? null
59
+ : (_jsx(Panel, { heading: `Finished sessions (${String(receipts.length)})`, children: _jsx(Facts, { rows: receipts.slice(0, 10).map(receipt => [
60
+ receipt.sessionId,
61
+ _jsxs("span", { children: [STATE_WORDS[receipt.finalState], ` · ${String(receipt.observationCount)} observation(s)`, receipt.reason === '' ? '' : ` · ${receipt.reason}`] }, receipt.sessionId),
62
+ ]) }) })), _jsx(Unavailable, { what: "Sources this machine cannot provide", because: 'Every source above keeps its real adapter and its real permission '
63
+ + 'boundary, and each is exercised deterministically. A source is only '
64
+ + 'offered as startable where its adapter can actually run here — a '
65
+ + 'control that fails when pressed teaches people the product is '
66
+ + 'broken rather than that a capability is absent.', wouldNeed: [
67
+ 'The hardware or OS support the adapter names, on the machine running Watch.',
68
+ 'The relevant OS permission, granted at first use rather than on load.',
69
+ 'A role bound for interpretation if the stream is to be transcribed or described.',
70
+ ] })] }));
71
+ }
72
+ //# sourceMappingURL=live-mode.js.map