@khorsheed/dsh-room 0.2.2 → 0.2.3
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/CHANGELOG.md +53 -0
- package/README.en.md +29 -62
- package/README.i18n.yaml +2 -2
- package/README.md +28 -61
- package/lib/client.js +13097 -4182
- package/lib/index.js +4045 -730
- package/lib/journal-B4FOGO6S.js +482 -0
- package/lib/journal-Rid9AAnD.js +485 -0
- package/lib/tool.js +112 -166
- package/lib/typert.host.js +2031 -374
- package/lib/typert.remote-client.d.ts +34 -10
- package/lib/typert.remote-client.js +1599 -210
- package/lib/types/agent-setup.d.ts +1 -1
- package/lib/types/client/InviteAgentAction.js +4 -16
- package/lib/types/client/InviteDialog.d.ts +28 -6
- package/lib/types/client/InviteDialog.js +76 -12
- package/lib/types/client/MemberCard.d.ts +5 -1
- package/lib/types/client/MemberCard.js +4 -4
- package/lib/types/client/MembersView.d.ts +1 -1
- package/lib/types/client/MembersView.js +58 -22
- package/lib/types/client/RemoveMemberConfirm.d.ts +40 -0
- package/lib/types/client/RemoveMemberConfirm.js +85 -0
- package/lib/types/client/RoomApprovalView.d.ts +11 -0
- package/lib/types/client/RoomApprovalView.js +28 -0
- package/lib/types/client/RoomComposer.d.ts +1 -1
- package/lib/types/client/RoomComposer.js +195 -29
- package/lib/types/client/RoomDecisionView.d.ts +22 -0
- package/lib/types/client/RoomDecisionView.js +35 -0
- package/lib/types/client/RoomHelpView.d.ts +17 -0
- package/lib/types/client/RoomHelpView.js +56 -0
- package/lib/types/client/RoomInboxView.d.ts +10 -0
- package/lib/types/client/RoomInboxView.js +14 -0
- package/lib/types/client/RoomQuestionView.d.ts +13 -0
- package/lib/types/client/RoomQuestionView.js +61 -0
- package/lib/types/client/RoomRelayView.js +1 -1
- package/lib/types/client/RoomRunView.d.ts +3 -1
- package/lib/types/client/RoomRunView.js +12 -5
- package/lib/types/client/RoomSettingsView.d.ts +19 -0
- package/lib/types/client/RoomSettingsView.js +30 -0
- package/lib/types/client/RoomSpeechView.d.ts +22 -8
- package/lib/types/client/RoomSpeechView.js +94 -11
- package/lib/types/client/RoomStatsLine.d.ts +26 -5
- package/lib/types/client/RoomStatsLine.js +53 -7
- package/lib/types/client/RoomTaskCapsule.d.ts +19 -0
- package/lib/types/client/RoomTaskCapsule.js +37 -0
- package/lib/types/client/TasksTab.d.ts +41 -0
- package/lib/types/client/TasksTab.js +43 -0
- package/lib/types/client/TasksView.d.ts +44 -0
- package/lib/types/client/TasksView.js +149 -0
- package/lib/types/client/TeamView.d.ts +33 -0
- package/lib/types/client/TeamView.js +210 -0
- package/lib/types/client/at-menu.d.ts +127 -0
- package/lib/types/client/at-menu.js +173 -0
- package/lib/types/client/draft-chips.d.ts +67 -0
- package/lib/types/client/draft-chips.js +95 -0
- package/lib/types/client/executions.d.ts +0 -30
- package/lib/types/client/executions.js +4 -34
- package/lib/types/client/icons-local.d.ts +25 -0
- package/lib/types/client/icons-local.js +10 -0
- package/lib/types/client/icons.d.ts +2 -1
- package/lib/types/client/icons.js +1 -0
- package/lib/types/client/index.js +276 -72
- package/lib/types/client/locales.d.ts +482 -52
- package/lib/types/client/locales.js +490 -60
- package/lib/types/client/nodes.d.ts +112 -17
- package/lib/types/client/nodes.js +197 -26
- package/lib/types/client/slots.d.ts +117 -47
- package/lib/types/client/task-tag.d.ts +32 -0
- package/lib/types/client/task-tag.js +33 -0
- package/lib/types/client/tasks.d.ts +50 -0
- package/lib/types/client/tasks.js +82 -0
- package/lib/types/client/team.d.ts +73 -0
- package/lib/types/client/team.js +187 -0
- package/lib/types/decisions.d.ts +49 -0
- package/lib/types/decisions.js +101 -0
- package/lib/types/deliveries.d.ts +17 -1
- package/lib/types/deliveries.js +82 -6
- package/lib/types/dispatch.d.ts +92 -4
- package/lib/types/dispatch.js +477 -109
- package/lib/types/help.d.ts +137 -0
- package/lib/types/help.js +216 -0
- package/lib/types/helps.d.ts +26 -0
- package/lib/types/helps.js +47 -0
- package/lib/types/index.d.ts +292 -52
- package/lib/types/index.js +1415 -199
- package/lib/types/jobs.d.ts +22 -0
- package/lib/types/jobs.js +77 -0
- package/lib/types/journal.d.ts +17 -27
- package/lib/types/journal.js +120 -43
- package/lib/types/messaging.d.ts +65 -0
- package/lib/types/messaging.js +160 -0
- package/lib/types/plan-service.js +7 -1
- package/lib/types/report-gate.d.ts +81 -0
- package/lib/types/report-gate.js +89 -0
- package/lib/types/tool-definition.d.ts +43 -20
- package/lib/types/tool-definition.js +145 -212
- package/lib/types/tool.d.ts +2 -2
- package/lib/types/tool.js +3 -2
- package/lib/types/types.d.ts +755 -78
- package/locale/zh.json +1 -1
- package/package.json +3 -3
- package/lib/types/client/RoomActivityDock.d.ts +0 -19
- package/lib/types/client/RoomActivityDock.js +0 -105
- package/lib/types/client/RoomDockCapsules.d.ts +0 -64
- package/lib/types/client/RoomDockCapsules.js +0 -277
- package/lib/types/client/RoomPlanTab.d.ts +0 -15
- package/lib/types/client/RoomPlanTab.js +0 -13
- package/lib/types/client/RoomTaskLineView.d.ts +0 -13
- package/lib/types/client/RoomTaskLineView.js +0 -21
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The team view: the room's organization in the right sidebar, opened from
|
|
4
|
+
* the members tab. One layout that adapts to its own width — narrow, an
|
|
5
|
+
* indented tree (you → coordinator → members) whose nodes expand into the
|
|
6
|
+
* deliveries they received; wide (≥ 600px), the same tree laid out
|
|
7
|
+
* top-down with drawn edges (solid = tasks handed over, with the report
|
|
8
|
+
* count coming back; dashed = FYIs off the tree) and the selected member's
|
|
9
|
+
* details below. Opening it with a `member` param selects and reveals that
|
|
10
|
+
* member (the cards' 在团队中定位).
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
13
|
+
import { formatDurationMs } from './format.js';
|
|
14
|
+
import { memberColor } from './member-color.js';
|
|
15
|
+
import { formatTokens, TEAM_ROOT, teamOf } from './team.js';
|
|
16
|
+
import css from './TeamView.module.css';
|
|
17
|
+
/** A node element by id, without building a selector from member ids. */
|
|
18
|
+
function nodeElement(root, id) {
|
|
19
|
+
return [...root.querySelectorAll('[data-team-node]')].find(element => element.dataset['teamNode'] === id);
|
|
20
|
+
}
|
|
21
|
+
/** Wide layout threshold (the view's own width). */
|
|
22
|
+
const WIDE = 600;
|
|
23
|
+
function StatusDot({ node, t }) {
|
|
24
|
+
const label = node.status === 'running' ? t('members.status.running') : node.status === 'failed' ? t('members.status.failed') : t('members.status.idle');
|
|
25
|
+
return _jsxs("span", { className: css.status, "data-status": node.status, children: [_jsx("i", { "aria-hidden": true }), label] });
|
|
26
|
+
}
|
|
27
|
+
function Avatar({ node }) {
|
|
28
|
+
return _jsx("span", { className: css.avatar, style: { '--member-color': memberColor(node.member.name) }, "aria-hidden": true, children: node.member.name.slice(0, 1).toUpperCase() });
|
|
29
|
+
}
|
|
30
|
+
/** Provider · model line (an outside session: what it is and how often it was asked). */
|
|
31
|
+
function configLine(node, t) {
|
|
32
|
+
if (node.external !== undefined) {
|
|
33
|
+
return [t(node.external.room ? 'team.external.room' : 'team.external.session'), ...node.external.helps.length === 0 ? [] : [t('team.external.asks', { count: node.external.helps.length })]].join(' · ');
|
|
34
|
+
}
|
|
35
|
+
const provider = node.member.kind === 'main-agent' ? t('member.kind.main')
|
|
36
|
+
: node.member.kind === 'native' ? t('member.kind.native', { preset: node.member.preset ?? t('member.kind.defaultPreset') })
|
|
37
|
+
: node.member.provider ?? '';
|
|
38
|
+
return [provider, node.model].filter(Boolean).join(' · ');
|
|
39
|
+
}
|
|
40
|
+
function statsLine(node, t) {
|
|
41
|
+
return t('team.stats', { tokens: formatTokens(node.tokens), time: formatDurationMs(node.elapsedMs), rounds: node.rounds });
|
|
42
|
+
}
|
|
43
|
+
/** What flowed into a node: tasks and returned reports from its parent, FYIs, and work from elsewhere. */
|
|
44
|
+
function flowOf(team, node, t) {
|
|
45
|
+
const name = (id) => id === TEAM_ROOT ? t('team.you') : team.nodes.find(entry => entry.id === id)?.member.name ?? id;
|
|
46
|
+
const into = team.edges.filter(edge => edge.to === node.id);
|
|
47
|
+
const fromParent = into.find(edge => edge.from === node.parent);
|
|
48
|
+
const others = into.filter(edge => edge.from !== node.parent && edge.tasks > 0)
|
|
49
|
+
.map(edge => t('team.crossTask', { from: name(edge.from), count: edge.tasks }));
|
|
50
|
+
return {
|
|
51
|
+
tasks: fromParent?.tasks ?? 0,
|
|
52
|
+
reports: fromParent?.reports ?? 0,
|
|
53
|
+
notifies: into.reduce((sum, edge) => sum + edge.notifies, 0),
|
|
54
|
+
others: others.length === 0 ? undefined : others.join(' · '),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** The counts pill: 「派 3 · 回 2 · 知会 1」, other sources in its tooltip. */
|
|
58
|
+
function FlowPill({ team, node, t, floating }) {
|
|
59
|
+
if (node.parent === TEAM_ROOT && node.coordinator)
|
|
60
|
+
return null;
|
|
61
|
+
const flow = flowOf(team, node, t);
|
|
62
|
+
if (flow.tasks === 0 && flow.notifies === 0 && flow.others === undefined)
|
|
63
|
+
return null;
|
|
64
|
+
return (_jsxs("span", { className: floating ? css.pillFloat : css.pill, title: flow.others === undefined ? undefined : t('team.alsoFrom', { list: flow.others }), children: [t('team.pillTasks', { count: flow.tasks }), flow.reports > 0 && _jsx("span", { className: css.pillReport, children: t('team.pillReports', { count: flow.reports }) }), flow.notifies > 0 && _jsx("span", { className: css.pillNotify, children: t('team.pillNotify', { count: flow.notifies }) }), flow.others !== undefined && _jsx("span", { children: "\u2026" })] }));
|
|
65
|
+
}
|
|
66
|
+
function Deliveries({ node, t }) {
|
|
67
|
+
if (node.deliveries.length === 0)
|
|
68
|
+
return _jsx("p", { className: css.empty, children: t('team.noDeliveries') });
|
|
69
|
+
return (_jsx("ul", { className: css.deliveries, children: node.deliveries.slice(0, 20).map(delivery => (_jsxs("li", { children: [_jsxs("span", { className: css.msgId, children: ["#m", delivery.seq] }), _jsx("span", { className: css.from, children: delivery.kind === 'report'
|
|
70
|
+
? t('team.kind.report')
|
|
71
|
+
: t(delivery.kind === 'notify' ? 'team.fromNotify' : 'team.from', { from: delivery.from === TEAM_ROOT ? t('team.you') : delivery.from }) }), _jsx("span", { className: css.text, title: delivery.text, children: delivery.text }), _jsx("span", { className: css.outcome, "data-status": delivery.status, children: t(`team.status.${delivery.status}`) })] }, delivery.id))) }));
|
|
72
|
+
}
|
|
73
|
+
function NodeCard({ team, node, selected, onSelect, t, compact }) {
|
|
74
|
+
return (_jsxs("button", { type: "button", className: css.node, "data-selected": selected, "data-team-node": node.id, "data-external": node.external === undefined ? undefined : node.external.linked ? 'linked' : 'helper', "data-done": node.external?.done === true && !node.external.linked ? '' : undefined, onClick: onSelect, "aria-expanded": compact ? selected : undefined, children: [!compact && node.external === undefined && _jsx(FlowPill, { team: team, node: node, t: t, floating: true }), _jsxs("span", { className: css.nodeHead, children: [_jsx(Avatar, { node: node }), _jsx("span", { className: css.nodeName, children: node.member.name }), node.coordinator && _jsx("span", { className: css.chip, children: t('coordinator.label') }), node.external !== undefined && _jsx("span", { className: css.chip, "data-external": "", children: t(node.external.linked ? 'team.external.linked' : 'team.external.helper') }), node.member.role !== undefined && _jsx("span", { className: css.role, children: node.member.role }), _jsx(StatusDot, { node: node, t: t })] }), _jsx("span", { className: css.line, children: configLine(node, t) }), node.external === undefined && (_jsxs("span", { className: css.line, children: [compact && _jsx(FlowPill, { team: team, node: node, t: t, floating: false }), _jsx("span", { children: statsLine(node, t) })] })), node.current !== undefined && _jsxs("span", { className: css.current, children: ["\u25B6 ", node.current] })] }));
|
|
75
|
+
}
|
|
76
|
+
/** An outside session's list control: 加入房间成员 (a plain session asked before) or 移出成员列表. */
|
|
77
|
+
function LinkControl({ node, linkSession, unlinkSession, t }) {
|
|
78
|
+
const external = node.external;
|
|
79
|
+
if (external === undefined || external.room)
|
|
80
|
+
return null;
|
|
81
|
+
if (external.linked) {
|
|
82
|
+
return unlinkSession === undefined ? null : _jsx("button", { type: "button", className: css.open, onClick: () => { unlinkSession(external.sessionId); }, children: t('team.external.unlink') });
|
|
83
|
+
}
|
|
84
|
+
return linkSession === undefined ? null : _jsx("button", { type: "button", className: css.open, onClick: () => { linkSession(external.sessionId, node.member.name); }, children: t('team.external.link') });
|
|
85
|
+
}
|
|
86
|
+
function Tree({ team, at, selected, onSelect, openSession, linkSession, unlinkSession, t }) {
|
|
87
|
+
const children = team.nodes.filter(node => node.parent === at);
|
|
88
|
+
if (children.length === 0)
|
|
89
|
+
return null;
|
|
90
|
+
return (_jsx("ul", { className: css.tree, children: children.map(node => (_jsxs("li", { children: [_jsx(NodeCard, { team: team, node: node, selected: selected === node.id, compact: true, onSelect: () => { onSelect(node.id); }, t: t }), selected === node.id && (_jsxs("div", { className: css.expand, children: [_jsx(Deliveries, { node: node, t: t }), node.member.childSessionId !== undefined && (_jsx("button", { type: "button", className: css.open, onClick: () => { openSession(node.member.childSessionId); }, children: t('team.openSession') })), _jsx(LinkControl, { node: node, linkSession: linkSession, unlinkSession: unlinkSession, t: t })] })), _jsx(Tree, { team: team, at: node.id, selected: selected, onSelect: onSelect, openSession: openSession, linkSession: linkSession, unlinkSession: unlinkSession, t: t })] }, node.id))) }));
|
|
91
|
+
}
|
|
92
|
+
/** Depth levels for the wide layout. */
|
|
93
|
+
function levels(team) {
|
|
94
|
+
const rows = [[TEAM_ROOT]];
|
|
95
|
+
let frontier = [TEAM_ROOT];
|
|
96
|
+
while (frontier.length > 0) {
|
|
97
|
+
const next = team.nodes.filter(node => frontier.includes(node.parent)).map(node => node.id);
|
|
98
|
+
if (next.length > 0)
|
|
99
|
+
rows.push(next);
|
|
100
|
+
frontier = next;
|
|
101
|
+
}
|
|
102
|
+
return rows;
|
|
103
|
+
}
|
|
104
|
+
/** Node width plus gap in the wide layout (matches the stylesheet). */
|
|
105
|
+
const SLOT = 210 + 16;
|
|
106
|
+
/**
|
|
107
|
+
* The wide layout: one row per depth, connected trunk-and-branch — a trunk
|
|
108
|
+
* down from each parent, one horizontal bus over its children, a short drop
|
|
109
|
+
* to each child — so lines never cross and carry no text; the counts ride
|
|
110
|
+
* each node's pill. A row wider than the panel folds its tail into 「+N」.
|
|
111
|
+
*/
|
|
112
|
+
function Graph({ team, selected, onSelect, t }) {
|
|
113
|
+
const ref = useRef(null);
|
|
114
|
+
const [paths, setPaths] = useState([]);
|
|
115
|
+
const [width, setWidth] = useState(0);
|
|
116
|
+
const [unfolded, setUnfolded] = useState(new Set());
|
|
117
|
+
const rows = useMemo(() => levels(team), [team]);
|
|
118
|
+
const perRow = Math.max(2, Math.floor((width + 16) / SLOT));
|
|
119
|
+
useLayoutEffect(() => {
|
|
120
|
+
const root = ref.current;
|
|
121
|
+
if (root === null)
|
|
122
|
+
return undefined;
|
|
123
|
+
const draw = () => {
|
|
124
|
+
setWidth(root.clientWidth);
|
|
125
|
+
const base = root.getBoundingClientRect();
|
|
126
|
+
const box = (id) => {
|
|
127
|
+
const element = nodeElement(root, id);
|
|
128
|
+
if (element === undefined)
|
|
129
|
+
return undefined;
|
|
130
|
+
const rect = element.getBoundingClientRect();
|
|
131
|
+
return { x: rect.left - base.left, y: rect.top - base.top, w: rect.width, h: rect.height };
|
|
132
|
+
};
|
|
133
|
+
const next = [];
|
|
134
|
+
const parents = new Set(team.nodes.map(node => node.parent));
|
|
135
|
+
for (const parent of parents) {
|
|
136
|
+
const from = box(parent);
|
|
137
|
+
const kids = team.nodes.filter(node => node.parent === parent).map(node => box(node.id)).filter(entry => entry !== undefined);
|
|
138
|
+
const more = nodeElement(root, `more:${parent}`);
|
|
139
|
+
if (more !== undefined) {
|
|
140
|
+
const rect = more.getBoundingClientRect();
|
|
141
|
+
kids.push({ x: rect.left - base.left, y: rect.top - base.top, w: rect.width, h: rect.height });
|
|
142
|
+
}
|
|
143
|
+
if (from === undefined || kids.length === 0)
|
|
144
|
+
continue;
|
|
145
|
+
const top = Math.min(...kids.map(kid => kid.y));
|
|
146
|
+
const bus = top - 16;
|
|
147
|
+
const px = from.x + from.w / 2;
|
|
148
|
+
next.push({ d: `M${px},${from.y + from.h} V${bus}`, drop: false });
|
|
149
|
+
const xs = kids.map(kid => kid.x + kid.w / 2);
|
|
150
|
+
next.push({ d: `M${Math.min(px, ...xs)},${bus} H${Math.max(px, ...xs)}`, drop: false });
|
|
151
|
+
for (const [index, kid] of kids.entries())
|
|
152
|
+
next.push({ d: `M${xs[index]},${bus} V${kid.y - 2}`, drop: true });
|
|
153
|
+
}
|
|
154
|
+
setPaths(next);
|
|
155
|
+
};
|
|
156
|
+
draw();
|
|
157
|
+
const observer = new ResizeObserver(draw);
|
|
158
|
+
observer.observe(root);
|
|
159
|
+
return () => { observer.disconnect(); };
|
|
160
|
+
}, [team, unfolded, perRow]);
|
|
161
|
+
return (_jsxs("div", { className: css.graph, ref: ref, children: [_jsxs("svg", { className: css.edges, "aria-hidden": true, children: [_jsx("defs", { children: _jsx("marker", { id: "room-team-arrow", viewBox: "0 0 8 8", refX: "7", refY: "4", markerWidth: "6", markerHeight: "6", orient: "auto", children: _jsx("path", { d: "M0,0 L8,4 L0,8 z", className: css.arrow }) }) }), paths.map((path, index) => _jsx("path", { d: path.d, className: css.edgeTask, markerEnd: path.drop ? 'url(#room-team-arrow)' : undefined }, index))] }), rows.map((row, index) => {
|
|
162
|
+
const folds = row.length > perRow && !unfolded.has(index);
|
|
163
|
+
const shown = folds ? row.slice(0, perRow - 1) : row;
|
|
164
|
+
// One 「+N」 per row, attached to the parent of the first hidden node (the bus already spans it).
|
|
165
|
+
const hidden = folds ? row.slice(perRow - 1) : [];
|
|
166
|
+
const parentOfHidden = hidden.length === 0 ? undefined : team.nodes.find(node => node.id === hidden[0])?.parent;
|
|
167
|
+
return (_jsxs("div", { className: css.level, children: [shown.map((id) => {
|
|
168
|
+
if (id === TEAM_ROOT)
|
|
169
|
+
return _jsx("span", { className: css.root, "data-team-node": TEAM_ROOT, children: t('team.you') }, id);
|
|
170
|
+
const node = team.nodes.find(entry => entry.id === id);
|
|
171
|
+
return _jsx(NodeCard, { team: team, node: node, selected: selected === id, compact: false, onSelect: () => { onSelect(id); }, t: t }, id);
|
|
172
|
+
}), hidden.length > 0 && (_jsx("button", { type: "button", className: css.more, "data-team-node": `more:${parentOfHidden}`, onClick: () => { setUnfolded(current => new Set([...current, index])); }, children: t('team.more', { count: hidden.length }) }))] }, index));
|
|
173
|
+
})] }));
|
|
174
|
+
}
|
|
175
|
+
/** The team view body. */
|
|
176
|
+
export function TeamView({ state, focus, openSession, linkSession, unlinkSession, t }) {
|
|
177
|
+
const team = useMemo(() => teamOf(state), [state]);
|
|
178
|
+
const rootRef = useRef(null);
|
|
179
|
+
const [wide, setWide] = useState(false);
|
|
180
|
+
const [selected, setSelected] = useState(undefined);
|
|
181
|
+
useLayoutEffect(() => {
|
|
182
|
+
const root = rootRef.current;
|
|
183
|
+
if (root === null)
|
|
184
|
+
return undefined;
|
|
185
|
+
const observer = new ResizeObserver(() => { setWide(root.clientWidth >= WIDE); });
|
|
186
|
+
observer.observe(root);
|
|
187
|
+
setWide(root.clientWidth >= WIDE);
|
|
188
|
+
return () => { observer.disconnect(); };
|
|
189
|
+
}, []);
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (focus === undefined)
|
|
192
|
+
return;
|
|
193
|
+
const node = team.nodes.find(entry => entry.member.name === focus.member);
|
|
194
|
+
if (node === undefined)
|
|
195
|
+
return;
|
|
196
|
+
setSelected(node.id);
|
|
197
|
+
requestAnimationFrame(() => {
|
|
198
|
+
const root = rootRef.current;
|
|
199
|
+
if (root !== null)
|
|
200
|
+
nodeElement(root, node.id)?.scrollIntoView({ block: 'nearest' });
|
|
201
|
+
});
|
|
202
|
+
// Every navigation reveals again, even to the same member.
|
|
203
|
+
}, [focus?.member, focus?.revision]);
|
|
204
|
+
const select = (id) => { setSelected(current => current === id && !wide ? undefined : id); };
|
|
205
|
+
const chosen = team.nodes.find(node => node.id === selected);
|
|
206
|
+
return (_jsxs("div", { className: css.team, ref: rootRef, children: [_jsx("p", { className: css.summary, children: t('team.summary', {
|
|
207
|
+
members: team.totals.members, running: team.totals.running, deliveries: team.totals.deliveries,
|
|
208
|
+
tokens: formatTokens(team.totals.tokens), time: formatDurationMs(team.totals.elapsedMs),
|
|
209
|
+
}) }), _jsx("p", { className: css.legend, children: _jsx("span", { children: t('team.legend.pill') }) }), wide ? (_jsxs(_Fragment, { children: [_jsx(Graph, { team: team, selected: selected, onSelect: select, t: t }), chosen === undefined ? _jsx("p", { className: css.empty, children: t('team.pick') }) : (_jsxs("section", { className: css.detail, children: [_jsxs("div", { className: css.nodeHead, children: [_jsx(Avatar, { node: chosen }), _jsx("span", { className: css.nodeName, children: chosen.member.name }), chosen.member.role !== undefined && _jsx("span", { className: css.role, children: chosen.member.role }), _jsx(StatusDot, { node: chosen, t: t }), chosen.member.childSessionId !== undefined && (_jsx("button", { type: "button", className: css.open, onClick: () => { openSession(chosen.member.childSessionId); }, children: t('team.openSession') })), _jsx(LinkControl, { node: chosen, linkSession: linkSession, unlinkSession: unlinkSession, t: t })] }), _jsxs("p", { className: css.line, children: [configLine(chosen, t), chosen.external === undefined ? ` · ${statsLine(chosen, t)}` : ''] }), chosen.external !== undefined && _jsx("p", { className: css.line, children: t('team.external.note') }), _jsx(Deliveries, { node: chosen, t: t })] }))] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: css.root, children: t('team.you') }), _jsx(Tree, { team: team, at: TEAM_ROOT, selected: selected, onSelect: select, openSession: openSession, linkSession: linkSession, unlinkSession: unlinkSession, t: t })] }))] }));
|
|
210
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The room composer's @ menu, data side: two groups — this room's members,
|
|
3
|
+
* then other sessions (the host's session-reference candidates, the same
|
|
4
|
+
* list its own @ menu shows). With nothing typed the second group lists the
|
|
5
|
+
* five most recent; typing filters both groups by a fuzzy match where the
|
|
6
|
+
* typed characters only have to appear in order (「剪插」 finds 「开发剪辑插件」),
|
|
7
|
+
* ranked exact > prefix > contiguous > in-order, then by recency.
|
|
8
|
+
*
|
|
9
|
+
* Pure and DOM-free — unit-tested on its own.
|
|
10
|
+
* @module @khorsheed/dsh-room/client/at-menu
|
|
11
|
+
*/
|
|
12
|
+
/** One session candidate, as the host's session-reference resolver lists it. */
|
|
13
|
+
export interface SessionCandidate {
|
|
14
|
+
readonly sessionId: string;
|
|
15
|
+
/** The latest title (the session id when untitled). */
|
|
16
|
+
readonly label: string;
|
|
17
|
+
readonly displayTitle?: string;
|
|
18
|
+
/** The canonical `@[label](dsh-session:…)` mention the host inserts. */
|
|
19
|
+
readonly mention: string;
|
|
20
|
+
readonly sameWorkspace?: boolean;
|
|
21
|
+
readonly createdAt?: number;
|
|
22
|
+
/** The session is a room (asking a room goes through its own roster). */
|
|
23
|
+
readonly room?: boolean;
|
|
24
|
+
/** A room's members (askable by name). */
|
|
25
|
+
readonly members?: readonly RoomMenuMember[];
|
|
26
|
+
/** On this room's member list (「加入房间成员」). */
|
|
27
|
+
readonly linked?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** A member of another room, as the @ menu lists it. */
|
|
30
|
+
export interface RoomMenuMember {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly kind: string;
|
|
33
|
+
readonly provider?: string;
|
|
34
|
+
}
|
|
35
|
+
/** One row of the other-sessions group: a session (or room), or a member of a room. */
|
|
36
|
+
export type OtherRow = {
|
|
37
|
+
readonly kind: 'session';
|
|
38
|
+
readonly candidate: SessionCandidate;
|
|
39
|
+
} | {
|
|
40
|
+
readonly kind: 'roomMember';
|
|
41
|
+
readonly candidate: SessionCandidate;
|
|
42
|
+
readonly member: RoomMenuMember;
|
|
43
|
+
readonly nested?: true;
|
|
44
|
+
};
|
|
45
|
+
/** How many other sessions show before anything is typed. */
|
|
46
|
+
export declare const RECENT_SESSIONS = 5;
|
|
47
|
+
/** How many other sessions a typed query shows at most. */
|
|
48
|
+
export declare const MATCHED_SESSIONS = 8;
|
|
49
|
+
/**
|
|
50
|
+
* How well a query matches a text: 0 exact, 1 prefix, 2 contiguous, 3 the
|
|
51
|
+
* characters in order; undefined for no match. Case-insensitive; an empty
|
|
52
|
+
* query matches everything at tier 0.
|
|
53
|
+
* @param query - what the human typed after `@`.
|
|
54
|
+
* @param text - a member name or session title.
|
|
55
|
+
* @returns the tier, or undefined.
|
|
56
|
+
*/
|
|
57
|
+
export declare function matchTier(query: string, text: string): number | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Filter and rank by match tier, keeping the incoming order (recency) within a tier.
|
|
60
|
+
* @param query - what the human typed after `@`.
|
|
61
|
+
* @param items - the candidates, most relevant first.
|
|
62
|
+
* @param text - the text an item is matched on.
|
|
63
|
+
* @returns the matching items, best first.
|
|
64
|
+
*/
|
|
65
|
+
export declare function rankByMatch<T>(query: string, items: readonly T[], text: (item: T) => string): T[];
|
|
66
|
+
/** A session candidate's shown title. */
|
|
67
|
+
export declare function sessionTitle(candidate: SessionCandidate): string;
|
|
68
|
+
/**
|
|
69
|
+
* The other-sessions group: the candidates minus this room and its members'
|
|
70
|
+
* own sessions, deduplicated, then the recent five (nothing typed) or the
|
|
71
|
+
* best matches.
|
|
72
|
+
* @param query - what the human typed after `@`.
|
|
73
|
+
* @param candidates - the resolver's candidates, most relevant first (several lookups may be merged).
|
|
74
|
+
* @param exclude - session ids never offered (the room, its members' sessions).
|
|
75
|
+
* @returns the group's rows.
|
|
76
|
+
*/
|
|
77
|
+
export declare function sessionRows(query: string, candidates: readonly SessionCandidate[], exclude: ReadonlySet<string>): SessionCandidate[];
|
|
78
|
+
/**
|
|
79
|
+
* The other-sessions group with rooms. Nothing typed: the recent five, each
|
|
80
|
+
* expanded room followed by its members. Typed: sessions and every room's
|
|
81
|
+
* members match together (a member by its own name), best first.
|
|
82
|
+
* @param query - what the human typed after `@`.
|
|
83
|
+
* @param candidates - the resolver's candidates, most relevant first.
|
|
84
|
+
* @param exclude - session ids never offered.
|
|
85
|
+
* @param expanded - the rooms whose members show.
|
|
86
|
+
* @returns the group's rows.
|
|
87
|
+
*/
|
|
88
|
+
export declare function otherRows(query: string, candidates: readonly SessionCandidate[], exclude: ReadonlySet<string>, expanded: ReadonlySet<string>): OtherRow[];
|
|
89
|
+
/**
|
|
90
|
+
* The mention for a member of a room: the room's own `dsh-session:` URI under
|
|
91
|
+
* a 「成员(房间)」 label (the label `room_help` reads the member from).
|
|
92
|
+
* @param candidate - the room.
|
|
93
|
+
* @param member - its member.
|
|
94
|
+
* @returns the mention text and its label.
|
|
95
|
+
*/
|
|
96
|
+
export declare function memberMention(candidate: SessionCandidate, member: RoomMenuMember): {
|
|
97
|
+
readonly mention: string;
|
|
98
|
+
readonly label: string;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* The host's canonical mention of a session: `@[label](dsh-session:…)`, the
|
|
102
|
+
* URI being base64url of the JSON-encoded id (what the host's own @ menu
|
|
103
|
+
* writes; built here for the room's linked sessions, which the resolver's
|
|
104
|
+
* recent list may not carry).
|
|
105
|
+
* @param sessionId - the session.
|
|
106
|
+
* @param label - its shown title.
|
|
107
|
+
* @returns the mention text.
|
|
108
|
+
*/
|
|
109
|
+
export declare function sessionMention(sessionId: string, label: string): string;
|
|
110
|
+
/**
|
|
111
|
+
* A free-text offset in the composer's textarea as a detect-coordinate
|
|
112
|
+
* offset in the input machine's draft: each chip before it folds to one
|
|
113
|
+
* placeholder (see draft-chips.ts), so the offset moves right by the number
|
|
114
|
+
* of chips whose segments end before it.
|
|
115
|
+
* @param segments - the typed text around the chips (draftSegments).
|
|
116
|
+
* @param freeIndex - the offset in the textarea's text.
|
|
117
|
+
* @returns the detect offset.
|
|
118
|
+
*/
|
|
119
|
+
export declare function detectOffset(segments: readonly string[], freeIndex: number): number;
|
|
120
|
+
/**
|
|
121
|
+
* A row's time hint: HH:MM today, else M/D.
|
|
122
|
+
* @param at - epoch ms.
|
|
123
|
+
* @param now - epoch ms of "now".
|
|
124
|
+
* @returns the hint.
|
|
125
|
+
*/
|
|
126
|
+
export declare function shortTime(at: number, now: number): string;
|
|
127
|
+
//# sourceMappingURL=at-menu.d.ts.map
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The room composer's @ menu, data side: two groups — this room's members,
|
|
3
|
+
* then other sessions (the host's session-reference candidates, the same
|
|
4
|
+
* list its own @ menu shows). With nothing typed the second group lists the
|
|
5
|
+
* five most recent; typing filters both groups by a fuzzy match where the
|
|
6
|
+
* typed characters only have to appear in order (「剪插」 finds 「开发剪辑插件」),
|
|
7
|
+
* ranked exact > prefix > contiguous > in-order, then by recency.
|
|
8
|
+
*
|
|
9
|
+
* Pure and DOM-free — unit-tested on its own.
|
|
10
|
+
* @module @khorsheed/dsh-room/client/at-menu
|
|
11
|
+
*/
|
|
12
|
+
/** How many other sessions show before anything is typed. */
|
|
13
|
+
export const RECENT_SESSIONS = 5;
|
|
14
|
+
/** How many other sessions a typed query shows at most. */
|
|
15
|
+
export const MATCHED_SESSIONS = 8;
|
|
16
|
+
/**
|
|
17
|
+
* How well a query matches a text: 0 exact, 1 prefix, 2 contiguous, 3 the
|
|
18
|
+
* characters in order; undefined for no match. Case-insensitive; an empty
|
|
19
|
+
* query matches everything at tier 0.
|
|
20
|
+
* @param query - what the human typed after `@`.
|
|
21
|
+
* @param text - a member name or session title.
|
|
22
|
+
* @returns the tier, or undefined.
|
|
23
|
+
*/
|
|
24
|
+
export function matchTier(query, text) {
|
|
25
|
+
const needle = query.toLocaleLowerCase();
|
|
26
|
+
const haystack = text.toLocaleLowerCase();
|
|
27
|
+
if (needle === '' || haystack === needle)
|
|
28
|
+
return 0;
|
|
29
|
+
if (haystack.startsWith(needle))
|
|
30
|
+
return 1;
|
|
31
|
+
if (haystack.includes(needle))
|
|
32
|
+
return 2;
|
|
33
|
+
let at = 0;
|
|
34
|
+
for (const char of haystack) {
|
|
35
|
+
if (char === needle[at])
|
|
36
|
+
at += 1;
|
|
37
|
+
if (at === needle.length)
|
|
38
|
+
return 3;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Filter and rank by match tier, keeping the incoming order (recency) within a tier.
|
|
44
|
+
* @param query - what the human typed after `@`.
|
|
45
|
+
* @param items - the candidates, most relevant first.
|
|
46
|
+
* @param text - the text an item is matched on.
|
|
47
|
+
* @returns the matching items, best first.
|
|
48
|
+
*/
|
|
49
|
+
export function rankByMatch(query, items, text) {
|
|
50
|
+
return items
|
|
51
|
+
.map((item, index) => ({ item, index, tier: matchTier(query, text(item)) }))
|
|
52
|
+
.filter((entry) => entry.tier !== undefined)
|
|
53
|
+
.sort((a, b) => a.tier - b.tier || a.index - b.index)
|
|
54
|
+
.map(entry => entry.item);
|
|
55
|
+
}
|
|
56
|
+
/** A session candidate's shown title. */
|
|
57
|
+
export function sessionTitle(candidate) {
|
|
58
|
+
return candidate.displayTitle ?? candidate.label;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The other-sessions group: the candidates minus this room and its members'
|
|
62
|
+
* own sessions, deduplicated, then the recent five (nothing typed) or the
|
|
63
|
+
* best matches.
|
|
64
|
+
* @param query - what the human typed after `@`.
|
|
65
|
+
* @param candidates - the resolver's candidates, most relevant first (several lookups may be merged).
|
|
66
|
+
* @param exclude - session ids never offered (the room, its members' sessions).
|
|
67
|
+
* @returns the group's rows.
|
|
68
|
+
*/
|
|
69
|
+
export function sessionRows(query, candidates, exclude) {
|
|
70
|
+
const pool = uniqueCandidates(candidates, exclude);
|
|
71
|
+
if (query === '')
|
|
72
|
+
return pool.slice(0, RECENT_SESSIONS);
|
|
73
|
+
return rankByMatch(query, pool, sessionTitle).slice(0, MATCHED_SESSIONS);
|
|
74
|
+
}
|
|
75
|
+
/** The candidates minus the excluded, first occurrence kept. */
|
|
76
|
+
function uniqueCandidates(candidates, exclude) {
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
return candidates.filter((candidate) => {
|
|
79
|
+
if (exclude.has(candidate.sessionId) || seen.has(candidate.sessionId))
|
|
80
|
+
return false;
|
|
81
|
+
seen.add(candidate.sessionId);
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The other-sessions group with rooms. Nothing typed: the recent five, each
|
|
87
|
+
* expanded room followed by its members. Typed: sessions and every room's
|
|
88
|
+
* members match together (a member by its own name), best first.
|
|
89
|
+
* @param query - what the human typed after `@`.
|
|
90
|
+
* @param candidates - the resolver's candidates, most relevant first.
|
|
91
|
+
* @param exclude - session ids never offered.
|
|
92
|
+
* @param expanded - the rooms whose members show.
|
|
93
|
+
* @returns the group's rows.
|
|
94
|
+
*/
|
|
95
|
+
export function otherRows(query, candidates, exclude, expanded) {
|
|
96
|
+
if (query === '') {
|
|
97
|
+
return sessionRows('', candidates, exclude).flatMap(candidate => [
|
|
98
|
+
{ kind: 'session', candidate },
|
|
99
|
+
...expanded.has(candidate.sessionId) ? (candidate.members ?? []).map(member => ({ kind: 'roomMember', candidate, member, nested: true })) : [],
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
const pool = uniqueCandidates(candidates, exclude).flatMap(candidate => [
|
|
103
|
+
{ kind: 'session', candidate },
|
|
104
|
+
...(candidate.members ?? []).map(member => ({ kind: 'roomMember', candidate, member })),
|
|
105
|
+
]);
|
|
106
|
+
const ranked = rankByMatch(query, pool, row => row.kind === 'session' ? sessionTitle(row.candidate) : row.member.name).slice(0, MATCHED_SESSIONS);
|
|
107
|
+
// An expanded room lists all its members under it, matched or not (a
|
|
108
|
+
// member already listed by its own match moves under its room).
|
|
109
|
+
const under = (row) => row.kind === 'roomMember' && expanded.has(row.candidate.sessionId);
|
|
110
|
+
return ranked.filter(row => !under(row) || !ranked.some(other => other.kind === 'session' && other.candidate.sessionId === row.candidate.sessionId))
|
|
111
|
+
.flatMap(row => row.kind === 'session' && expanded.has(row.candidate.sessionId)
|
|
112
|
+
? [row, ...(row.candidate.members ?? []).map(member => ({ kind: 'roomMember', candidate: row.candidate, member, nested: true }))]
|
|
113
|
+
: [row]);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The mention for a member of a room: the room's own `dsh-session:` URI under
|
|
117
|
+
* a 「成员(房间)」 label (the label `room_help` reads the member from).
|
|
118
|
+
* @param candidate - the room.
|
|
119
|
+
* @param member - its member.
|
|
120
|
+
* @returns the mention text and its label.
|
|
121
|
+
*/
|
|
122
|
+
export function memberMention(candidate, member) {
|
|
123
|
+
const uri = /\]\((dsh-session:[^)\s]*)\)\s*$/u.exec(candidate.mention)?.[1] ?? '';
|
|
124
|
+
const label = `${member.name}(${sessionTitle(candidate)})`;
|
|
125
|
+
return { mention: `@[${label.replace(/[\\\]]/gu, '\\$&')}](${uri})`, label };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The host's canonical mention of a session: `@[label](dsh-session:…)`, the
|
|
129
|
+
* URI being base64url of the JSON-encoded id (what the host's own @ menu
|
|
130
|
+
* writes; built here for the room's linked sessions, which the resolver's
|
|
131
|
+
* recent list may not carry).
|
|
132
|
+
* @param sessionId - the session.
|
|
133
|
+
* @param label - its shown title.
|
|
134
|
+
* @returns the mention text.
|
|
135
|
+
*/
|
|
136
|
+
export function sessionMention(sessionId, label) {
|
|
137
|
+
const bytes = new TextEncoder().encode(JSON.stringify(sessionId));
|
|
138
|
+
const payload = btoa(String.fromCharCode(...bytes)).replace(/\+/gu, '-').replace(/\//gu, '_').replace(/=+$/u, '');
|
|
139
|
+
return `@[${label.replace(/[\\\]]/gu, '\\$&')}](dsh-session:${payload})`;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A free-text offset in the composer's textarea as a detect-coordinate
|
|
143
|
+
* offset in the input machine's draft: each chip before it folds to one
|
|
144
|
+
* placeholder (see draft-chips.ts), so the offset moves right by the number
|
|
145
|
+
* of chips whose segments end before it.
|
|
146
|
+
* @param segments - the typed text around the chips (draftSegments).
|
|
147
|
+
* @param freeIndex - the offset in the textarea's text.
|
|
148
|
+
* @returns the detect offset.
|
|
149
|
+
*/
|
|
150
|
+
export function detectOffset(segments, freeIndex) {
|
|
151
|
+
let remaining = freeIndex;
|
|
152
|
+
for (let k = 0; k < segments.length; k += 1) {
|
|
153
|
+
const length = segments[k].length;
|
|
154
|
+
if (remaining <= length || k === segments.length - 1)
|
|
155
|
+
return freeIndex + k;
|
|
156
|
+
remaining -= length;
|
|
157
|
+
}
|
|
158
|
+
return freeIndex;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A row's time hint: HH:MM today, else M/D.
|
|
162
|
+
* @param at - epoch ms.
|
|
163
|
+
* @param now - epoch ms of "now".
|
|
164
|
+
* @returns the hint.
|
|
165
|
+
*/
|
|
166
|
+
export function shortTime(at, now) {
|
|
167
|
+
const date = new Date(at);
|
|
168
|
+
const today = new Date(now);
|
|
169
|
+
if (date.toDateString() === today.toDateString()) {
|
|
170
|
+
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
|
171
|
+
}
|
|
172
|
+
return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
173
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference chips in the room composer's plain textarea. The draft lives in
|
|
3
|
+
* the session's official input machine (RoomComposer's module header), whose
|
|
4
|
+
* `draft` is the CLIPBOARD projection — each chip expanded to the full text
|
|
5
|
+
* it sends (a quote chip: its whole `> ` block). A textarea showing that
|
|
6
|
+
* projection shows raw wire text, and editing it through `setDraft` flattens
|
|
7
|
+
* every chip. So the composer shows chips as their own row and the textarea
|
|
8
|
+
* only the typed text BETWEEN them; an edit is mapped back onto the machine
|
|
9
|
+
* as detect-coordinate replacements (`insertText`), which never touch a chip
|
|
10
|
+
* outside the edited range.
|
|
11
|
+
*
|
|
12
|
+
* Coordinates: the machine's spans speak the DETECT projection, where each
|
|
13
|
+
* chip folds to one placeholder character. With chips at clipboard offsets,
|
|
14
|
+
* the typed text splits into segments around them; free-text index `i` in
|
|
15
|
+
* segment `k` sits at detect index `i + k` (k chips precede it).
|
|
16
|
+
*
|
|
17
|
+
* Pure and DOM-free — unit-tested on its own.
|
|
18
|
+
* @module @khorsheed/dsh-room/client/draft-chips
|
|
19
|
+
*/
|
|
20
|
+
/** One reference chip of the input machine (the host's Occurrence, mirrored to the fields used). */
|
|
21
|
+
export interface DraftChip {
|
|
22
|
+
/** Shell-assigned stable identity. */
|
|
23
|
+
readonly occurrenceId: number;
|
|
24
|
+
/** Owning trigger source (the preview route's key). */
|
|
25
|
+
readonly source: string;
|
|
26
|
+
/** Owner-scoped reference payload. */
|
|
27
|
+
readonly ref: string;
|
|
28
|
+
/** Offset in the clipboard projection. */
|
|
29
|
+
readonly offset: number;
|
|
30
|
+
/** Length in the clipboard projection. */
|
|
31
|
+
readonly length: number;
|
|
32
|
+
/** Inline display label. */
|
|
33
|
+
readonly label: string;
|
|
34
|
+
}
|
|
35
|
+
/** One detect-coordinate replacement (empty text deletes). */
|
|
36
|
+
export interface DraftEdit {
|
|
37
|
+
readonly start: number;
|
|
38
|
+
readonly end: number;
|
|
39
|
+
readonly text: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The typed text around the chips: one segment before each chip plus the
|
|
43
|
+
* tail, in order (so `segments.length === chips.length + 1`).
|
|
44
|
+
* @param draft - the clipboard projection.
|
|
45
|
+
* @param chips - the machine's chips, sorted by offset.
|
|
46
|
+
*/
|
|
47
|
+
export declare function draftSegments(draft: string, chips: readonly DraftChip[]): string[];
|
|
48
|
+
/** The textarea's text: the draft with every chip's clipboard text removed. */
|
|
49
|
+
export declare function freeText(draft: string, chips: readonly DraftChip[]): string;
|
|
50
|
+
/** The detect index of chip `k` (its single placeholder character). */
|
|
51
|
+
export declare function chipDetectIndex(draft: string, chips: readonly DraftChip[], k: number): number;
|
|
52
|
+
/**
|
|
53
|
+
* Plan the machine edits turning the current free text into `next` while
|
|
54
|
+
* keeping every chip. The change is the span between the common prefix and
|
|
55
|
+
* suffix; its replacement lands in the segment where it starts (a caret on a
|
|
56
|
+
* chip boundary types BEFORE the chip, which keeps a quote block's
|
|
57
|
+
* attribution line closed), and any part of the old span in later segments
|
|
58
|
+
* is deleted segment by segment — a range swept across a chip never deletes
|
|
59
|
+
* the chip itself. Edits are ordered last-first, so applying them in order
|
|
60
|
+
* keeps every earlier coordinate valid.
|
|
61
|
+
* @param draft - the clipboard projection.
|
|
62
|
+
* @param chips - the machine's chips, sorted by offset.
|
|
63
|
+
* @param next - the textarea's new value.
|
|
64
|
+
* @returns the edits to apply in order (empty when nothing changed).
|
|
65
|
+
*/
|
|
66
|
+
export declare function planFreeEdit(draft: string, chips: readonly DraftChip[], next: string): DraftEdit[];
|
|
67
|
+
//# sourceMappingURL=draft-chips.d.ts.map
|