create-pmos 0.133.0 → 0.134.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/notes.md +19 -1
- package/workspace/lib/notes-audio.js +28 -9
- package/workspace/lib/notes-capture.js +128 -0
- package/workspace/lib/notes-live-guard.js +117 -0
- package/workspace/lib/notes-whisper.js +132 -16
- package/workspace/lib/notes.js +4 -2
- package/workspace/lib/routes-notes.js +60 -8
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/calendar-views.js +25 -12
- package/workspace/public/pm/console3.css +19 -0
- package/workspace/public/pm/shell-recording.js +122 -12
- package/workspace/public/pm/view-notes.js +7 -2
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/meeting-notes.json +5 -3
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
/* pm/shell-recording.js — the
|
|
1
|
+
/* pm/shell-recording.js — the Record control in console3's top bar (console3 wave E2b; the Record control port).
|
|
2
2
|
*
|
|
3
3
|
* mountRecording(host) / disposeRecording(host)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* The control is always there, idle included, with the same capabilities as the main app's Record control (pm/topbar.js, pm/note-dock.js):
|
|
6
|
+
* New (an untitled note that records at once), an arrow that lists today's meetings that have not started (a pick makes a note linked to that
|
|
7
|
+
* calendar event and records it), and, while a meeting records or is paused, a chip that names it with Highlight, Pause or Resume, and Stop
|
|
8
|
+
* (Stop asks first and offers "Pause instead"). All of it goes through pm/note-dock.js, which owns the real recorder, so the audio capture of
|
|
9
|
+
* pm/view-notes.js runs here without the main app's Notes place on screen. The chip opens the note in the main app, because console3 has no
|
|
10
|
+
* Notes place. While one meeting records, a red dot sits on the Calendar tab (`[data-c3-tab="cal"]`) and on a Notes tab if a frame draws one.
|
|
11
|
+
* On /c3.html?demo=1 the control drives a SIMULATED recorder (see below): it opens no microphone and saves nothing, and it shows only while
|
|
12
|
+
* the simulation runs. Lifted: console3.html `.recg` and `recCtl` (lines 916 to 924, 4385 to 4400). The clock is the recorder's own start time.
|
|
10
13
|
*/
|
|
11
14
|
import { el } from './dom.js';
|
|
15
|
+
import { button } from './primitives.js';
|
|
12
16
|
import { ensureC3Css, c3ic } from './pm-work-view.js';
|
|
13
17
|
import * as dock from './note-dock.js';
|
|
14
18
|
import { recView, clockOf } from './shell-model.js';
|
|
@@ -20,10 +24,10 @@ import { toast } from './c3-toast.js';
|
|
|
20
24
|
const TAB = '[data-fr-dest="notes"], #mobNav [data-dest="notes"], [data-c3-tab="cal"]';
|
|
21
25
|
|
|
22
26
|
/* ── demo mode: a SIMULATED recorder ──────────────────────────────────────────────────────────────────────────────────────────────
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
+
* On /c3.html?demo=1 the Record button of the Calendar's During and After stages drives this stand-in, so the top bar shows the mockup's
|
|
28
|
+
* recording group: a pulsing dot, the clock, mark moment, pause or resume, and stop. It keeps a clock (performance.now: the demo freezes
|
|
29
|
+
* Date) and nothing else. No microphone is opened and nothing is saved. Real mode never reaches it: `isDemo()` reads the page's
|
|
30
|
+
* data-c3-demo mark, which only the demo sets. */
|
|
27
31
|
export const isDemo = () => typeof document !== 'undefined' && !!document.querySelector('[data-c3-demo]');
|
|
28
32
|
const SIM = { mode: 'idle', id: 'sim', title: '', started: 0, pausedAt: 0 };
|
|
29
33
|
const tick = () => performance.now();
|
|
@@ -50,7 +54,8 @@ export function mountRecording(host, opts) {
|
|
|
50
54
|
if (!host) return;
|
|
51
55
|
disposeRecording(host);
|
|
52
56
|
ensureC3Css();
|
|
53
|
-
|
|
57
|
+
if (!(opts && opts.dock) && !isDemo()) return mountReal(host);
|
|
58
|
+
const d = (opts && opts.dock) || simDock;
|
|
54
59
|
const nowMs = () => (d.now ? d.now() : Date.now());
|
|
55
60
|
const grp = el('span', { class: 'recg', role: 'group', 'aria-label': 'Recording', hidden: true, 'data-shell': 'recording' });
|
|
56
61
|
host.insertBefore(grp, host.firstChild);
|
|
@@ -80,7 +85,8 @@ export function mountRecording(host, opts) {
|
|
|
80
85
|
else {
|
|
81
86
|
rec.clock = el('span', { class: 'recclk', 'data-shell-rec-clock': '' });
|
|
82
87
|
const open = el('button', { type: 'button', title: (v.mode === 'recording' ? 'Recording: ' : 'Paused: ') + v.title + '. Open the live note', 'aria-label': 'Open the note: ' + v.title, 'data-shell-rec-open': '', onclick: () => d.openDock(st.id) },
|
|
83
|
-
el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title)
|
|
88
|
+
el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title),
|
|
89
|
+
...(eng.text ? [el('span', { class: 'c3rec__eng', 'data-fr-rec-engine': '', title: 'The live transcript: engine and language (Notes settings)' }, eng.text)] : []));
|
|
84
90
|
const kids = [open];
|
|
85
91
|
if (v.canMark) kids.push(btn('Mark this moment', 'highlighter', () => d.highlightNow(), 'data-shell-rec-mark'));
|
|
86
92
|
if (v.canPause) kids.push(btn('Pause', 'pause', () => d.pauseNow(), 'data-shell-rec-pause'));
|
|
@@ -103,8 +109,112 @@ export function disposeRecording(host) {
|
|
|
103
109
|
const rec = host && live.get(host);
|
|
104
110
|
if (!rec) return;
|
|
105
111
|
clearInterval(rec.timer);
|
|
112
|
+
if (rec.off) rec.off();
|
|
106
113
|
if (rec.onSim) window.removeEventListener('c3-sim-rec', rec.onSim);
|
|
107
114
|
rec.grp.remove();
|
|
108
115
|
for (const t of document.querySelectorAll(TAB)) { const dot = t.querySelector('.shell-recdot'); if (dot) dot.remove(); }
|
|
109
116
|
live.delete(host);
|
|
110
117
|
}
|
|
118
|
+
|
|
119
|
+
/* ── real mode: the always-visible Record control ─────────────────────────────────────────────────────────────────────────────────── */
|
|
120
|
+
|
|
121
|
+
const NS_SVG = 'http://www.w3.org/2000/svg';
|
|
122
|
+
const glyph = (d) => { const s = document.createElementNS(NS_SVG, 'svg'); s.setAttribute('class', 'ic sm'); s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('aria-hidden', 'true'); const p = document.createElementNS(NS_SVG, 'path'); p.setAttribute('d', d); s.appendChild(p); return s; };
|
|
123
|
+
const RGL = { highlighter: () => c3ic('flag', 'sm'), pause: () => glyph('M8 5v14M16 5v14'), play: () => c3ic('play', 'sm'), square: () => c3ic('stop', 'sm'), mic: () => c3ic('mic', 'sm'), down: () => glyph('M6 9l6 6 6-6') };
|
|
124
|
+
|
|
125
|
+
/** The chip opens the note. console3 has no Notes place, so it opens the main app, where the full note is. */
|
|
126
|
+
const openNote = () => { try { window.open('/', '_blank', 'noopener'); } catch { /* a blocked pop-up */ } };
|
|
127
|
+
|
|
128
|
+
/* The engine and language the live transcript uses, on the chip (notes-recording-reliability): a meeting transcribed in the wrong
|
|
129
|
+
* language because of a setting is visible while it records. Read once per recording from GET /api/notes/engine. */
|
|
130
|
+
const ENGINE = { Whisper: 'whisper', Qwen: 'qwen' };
|
|
131
|
+
async function engineLabel() {
|
|
132
|
+
try {
|
|
133
|
+
const { pmGet } = await import('./http.js');
|
|
134
|
+
const r = await pmGet('/api/notes/engine');
|
|
135
|
+
const w = r && r.ok ? r.whisper : null;
|
|
136
|
+
if (!w || !w.live) return '';
|
|
137
|
+
const name = Object.keys(ENGINE).find((k) => ENGINE[k] === w.live) || w.live;
|
|
138
|
+
const lang = ((w.languages || []).find((x) => x.value === w.language) || {}).label || w.language || '';
|
|
139
|
+
return name + (lang ? ' · ' + lang : '');
|
|
140
|
+
} catch { return ''; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function mountReal(host) {
|
|
144
|
+
ensureC3Css();
|
|
145
|
+
let ready = false;
|
|
146
|
+
const wrap = el('span', { class: 'c3rec', 'data-fr': 'record', 'data-shell': 'recording' });
|
|
147
|
+
host.insertBefore(wrap, host.firstChild);
|
|
148
|
+
const rec = { grp: wrap, key: '', clock: null };
|
|
149
|
+
const tabDot = (on) => {
|
|
150
|
+
for (const t of document.querySelectorAll(TAB)) {
|
|
151
|
+
let dot = t.querySelector('.shell-recdot');
|
|
152
|
+
if (!dot) { dot = el('span', { class: 'shell-recdot', 'aria-hidden': 'true' }); t.appendChild(dot); }
|
|
153
|
+
dot.hidden = !on;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const btn = (label, name, fn, attrs) => button({ label: RGL[name](), variant: 'ghost', onClick: fn, attrs: { title: label, 'aria-label': label, ...attrs } });
|
|
157
|
+
const off = (b, why) => { b.setAttribute('aria-disabled', 'true'); b.classList.add('c3rec__off'); b.title = why; b.setAttribute('aria-label', why); return b; };
|
|
158
|
+
const chipText = (st) => (st.mode === 'recording' ? (st.started ? clockOf(Date.now() - st.started) : '') : st.until ? 'paused · ' + Math.max(1, Math.ceil((st.until - Date.now()) / 60000)) + ' min left' : 'paused');
|
|
159
|
+
|
|
160
|
+
/** The arrow beside New: Untitled note, then today's meetings that have not started and have no note, the next one marked. */
|
|
161
|
+
async function openUpcoming(anchor) {
|
|
162
|
+
const { menu } = await import('./primitives.js');
|
|
163
|
+
const links = await import('./notes-links.js');
|
|
164
|
+
const evs = links.upcomingWithoutNote(await links.upcomingToday());
|
|
165
|
+
const fmt = (e, i) => {
|
|
166
|
+
const mins = Math.max(1, Math.round((Date.parse(e.start) - Date.now()) / 60000));
|
|
167
|
+
const inText = mins < 60 ? 'in ' + mins + ' min' : 'in ' + Math.floor(mins / 60) + ' h' + (mins % 60 ? ' ' + (mins % 60) + ' min' : '');
|
|
168
|
+
return { label: e.title, hint: e.time || '', detail: [i === 0 ? 'Next' : null, inText, e.with || null].filter(Boolean).join(' · '), attrs: { 'data-fr-rec-event': e.id || '' }, onSelect: () => dock.recordEvent(e) };
|
|
169
|
+
};
|
|
170
|
+
menu({ anchor, ariaLabel: 'Record a new meeting', items: [{ label: 'Untitled note', icon: 'plus', detail: 'not linked to a meeting', attrs: { 'data-fr-rec-untitled': '' }, onSelect: () => dock.recordNew() }, { sep: true },
|
|
171
|
+
...(evs.length ? evs.map(fmt) : [{ label: 'No more meetings today', attrs: { 'data-fr-rec-none': '' }, onSelect: () => {} }])] });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let eng = { id: null, text: '' };
|
|
175
|
+
const sync = () => {
|
|
176
|
+
let st; try { st = dock.recordState(); } catch { st = { mode: 'idle' }; }
|
|
177
|
+
if (st.mode !== 'idle' && eng.id !== st.id) { eng = { id: st.id, text: '' }; const want = st.id; engineLabel().then((t) => { if (eng.id === want && t) { eng.text = t; rec.key = ''; sync(); } }); }
|
|
178
|
+
const hl = st.mode === 'recording' && dock.highlightBusy ? dock.highlightBusy() : null;
|
|
179
|
+
const key = [st.mode, st.id, st.title, hl ? hl.state + hl.open : '', eng.text].join(':');
|
|
180
|
+
if (key !== rec.key) {
|
|
181
|
+
rec.key = key; rec.clock = null;
|
|
182
|
+
const recording = st.mode === 'recording';
|
|
183
|
+
const kids = [];
|
|
184
|
+
if (st.mode !== 'idle') {
|
|
185
|
+
const v = recView(st, reducedMotion());
|
|
186
|
+
rec.clock = el('span', { class: 'recclk', 'data-fr-rec-clock': '' }, chipText(st));
|
|
187
|
+
const chip = button({ variant: 'ghost', onClick: () => openNote(st.id),
|
|
188
|
+
attrs: { class: 'c3rec__chip', title: (recording ? 'Recording: ' : 'Paused: ') + v.title + '. Open the note in the main app', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + v.title + '. Open the note.', 'data-fr-rec': st.mode, 'data-shell-rec-open': '' },
|
|
189
|
+
label: [el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title),
|
|
190
|
+
...(eng.text ? [el('span', { class: 'c3rec__eng', 'data-fr-rec-engine': '', title: 'The live transcript: engine and language (Notes settings)' }, eng.text)] : [])] });
|
|
191
|
+
const group = [chip];
|
|
192
|
+
if (recording) {
|
|
193
|
+
group.push(btn(hl ? 'Highlight in progress: show it' : 'Highlight the last 30 seconds and what is said next', 'highlighter', () => dock.highlightNow(), { 'data-fr-rec-highlight': hl ? hl.state : '', 'data-shell-rec-mark': '' }),
|
|
194
|
+
btn('Pause (you can resume within 1 hour)', 'pause', () => dock.pauseNow(), { 'data-fr-rec-pause': '', 'data-shell-rec-pause': '' }));
|
|
195
|
+
} else group.push(btn('Resume ' + st.title, 'play', () => dock.resumeNow(), { 'data-fr-rec-resume': '', 'data-shell-rec-resume': '' }));
|
|
196
|
+
group.push(btn('Stop for good (cannot be resumed)', 'square', () => dock.stopNow(), { 'data-fr-rec-stop': '', 'data-shell-rec-stop': '' }));
|
|
197
|
+
kids.push(el('span', { class: 'recg c3rec__live' + (recording ? '' : ' paused'), role: 'group', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + st.title }, group));
|
|
198
|
+
}
|
|
199
|
+
const why = 'Pause or stop the meeting being recorded to start a new one';
|
|
200
|
+
const mic = button({ variant: 'ghost', label: [RGL.mic(), el('span', { class: 'c3rec__lbl' }, 'Record')], onClick: (e) => { if (!recording) dock.recordNew(e.currentTarget); },
|
|
201
|
+
attrs: { title: 'Record an untitled note', 'aria-label': 'Record an untitled note', 'data-fr-rec-new': '' } });
|
|
202
|
+
const arrow = btn('Record for an upcoming meeting', 'down', (e) => { if (!recording) openUpcoming(e.currentTarget); }, { 'data-fr-rec-upcoming': '', 'aria-haspopup': 'menu' });
|
|
203
|
+
if (recording) { off(mic, why); off(arrow, why); }
|
|
204
|
+
kids.push(el('span', { class: 'recg idle', role: 'group', 'aria-label': 'New recording' }, mic, arrow));
|
|
205
|
+
const panel = recording && dock.highlightPanel ? dock.highlightPanel() : null;
|
|
206
|
+
if (panel) kids.push(el('div', { class: 'c3rec__pop' }, panel));
|
|
207
|
+
wrap.replaceChildren(...kids);
|
|
208
|
+
tabDot(st.mode !== 'idle');
|
|
209
|
+
}
|
|
210
|
+
if (rec.clock) rec.clock.textContent = chipText(st);
|
|
211
|
+
};
|
|
212
|
+
/* note-dock reads the notes (a meeting the server still holds open is found again) and re-draws on every state change */
|
|
213
|
+
try { dock.mountDock(); } catch { /* the dock is not available on this page */ }
|
|
214
|
+
let unsub = null;
|
|
215
|
+
import('./state.js').then((m) => { unsub = m.subscribe(sync); }).catch(() => {});
|
|
216
|
+
rec.timer = setInterval(sync, 1000);
|
|
217
|
+
rec.off = () => { if (unsub) unsub(); };
|
|
218
|
+
sync();
|
|
219
|
+
live.set(host, rec);
|
|
220
|
+
}
|
|
@@ -647,7 +647,7 @@ function checkSilence(ctx, a) {
|
|
|
647
647
|
ctx.setState({});
|
|
648
648
|
}
|
|
649
649
|
async function startAudio(ctx, n, want) {
|
|
650
|
-
const a = { id: n.id, state: 'starting', sources: [], streams: [], ctx: null, started: Date.now(), clock: null, msg: null, queue: {}, buf: {}, rs: {}, rs24: {}, buf24: {}, peak: {}, began: Date.now(), lost: 0, ended: 0 };
|
|
650
|
+
const a = { id: n.id, state: 'starting', sources: [], streams: [], ctx: null, started: Date.now(), clock: null, msg: null, queue: {}, buf: {}, rs: {}, rs24: {}, buf24: {}, peak: {}, began: Date.now(), lost: 0, ended: 0, cap: null, seq: {}, off: {} };
|
|
651
651
|
const opened = [];
|
|
652
652
|
// the meeting first, straight from the press: the browser lets a page share a screen only right after a click
|
|
653
653
|
for (const kind of want.slice().sort((x, y) => (x === 'meeting' ? -1 : y === 'meeting' ? 1 : 0))) {
|
|
@@ -662,6 +662,8 @@ async function startAudio(ctx, n, want) {
|
|
|
662
662
|
n.recorded_from = r.recorded_from;
|
|
663
663
|
// the clock counts the meeting time, as the line times do: Listen again carries on from where it stopped
|
|
664
664
|
a.started = Date.parse(r.clock_from || r.recorded_from) || a.started;
|
|
665
|
+
// each chunk names its capture, its sequence number and its sample offset, so the server writes it once and in its place
|
|
666
|
+
a.cap = r.capture && r.capture.token ? r.capture.token : null;
|
|
665
667
|
let ac;
|
|
666
668
|
// the device's own rate: the 16 kHz audio for transcription and the 24 kHz audio for the replay are both made from the same blocks (D92 as amended)
|
|
667
669
|
try { ac = new AudioContext(); } catch { ac = new AudioContext({ sampleRate: RATE }); }
|
|
@@ -694,13 +696,16 @@ async function startAudio(ctx, n, want) {
|
|
|
694
696
|
function sendChunk(ctx, a, kind, final) {
|
|
695
697
|
const b = a.buf[kind] || []; a.buf[kind] = [];
|
|
696
698
|
const pcm = b64(Int16Array.from(b));
|
|
699
|
+
const seq = a.seq[kind] = a.seq[kind] === undefined ? 0 : a.seq[kind] + 1;
|
|
700
|
+
const off = a.off[kind] || 0; a.off[kind] = off + b.length;
|
|
701
|
+
const where = a.cap ? { cap: a.cap, seq, off } : {};
|
|
697
702
|
const b24 = a.buf24[kind] || []; a.buf24[kind] = [];
|
|
698
703
|
const pcm24 = b24.length ? b64(Int16Array.from(b24)) : undefined;
|
|
699
704
|
const seconds = b.length / RATE;
|
|
700
705
|
a.queue[kind] = a.queue[kind].then(async () => {
|
|
701
706
|
let r = null;
|
|
702
707
|
for (let tries = 0; tries < SEND_TRIES; tries++) {
|
|
703
|
-
r = await pmPost('/api/notes/audio', { id: a.id, source: kind, pcm, pcm24, final });
|
|
708
|
+
r = await pmPost('/api/notes/audio', { id: a.id, source: kind, pcm, pcm24, final, ...where });
|
|
704
709
|
if (r && r.ok) return;
|
|
705
710
|
if (r && (r.status === 409 || r.status === 404)) break; // the server refused this note's audio: sending again changes nothing
|
|
706
711
|
if (tries + 1 < SEND_TRIES) await new Promise((res) => setTimeout(res, SEND_BACKOFF_MS * 3 ** tries));
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
"pm-live-notes-test.js",
|
|
120
120
|
"pm-notes-menu-test.js",
|
|
121
121
|
"pm-record-control-test.js",
|
|
122
|
+
"pm-c3-record-test.js",
|
|
122
123
|
"pm-r30-test.js",
|
|
123
124
|
"pm-breakdown-test.js",
|
|
124
125
|
"pm-breakdown-phone-test.js",
|
|
@@ -234,6 +235,7 @@
|
|
|
234
235
|
"pm-live-notes-test.js": 24,
|
|
235
236
|
"pm-notes-menu-test.js": 10,
|
|
236
237
|
"pm-record-control-test.js": 12,
|
|
238
|
+
"pm-c3-record-test.js": 12,
|
|
237
239
|
"pm-r30-test.js": 4,
|
|
238
240
|
"ui-test.js": 30,
|
|
239
241
|
"plan-items.test.js": 1,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "meeting-notes Runs 1 to 10: the Notes engine and routes (notes.test.js), local Whisper, line labels and repository checks (notes-listen.test.js), the final pass after Stop and the Wispr Flow fetch (notes-final.test.js), Qwen3-ASR kept loaded (notes-qwen.test.js), and the Notes view in a browser (pm-notes-test.js), the echo canceller for the replay (notes-echo.test.js, notes-audio-quality Run 2) and the optional cleaned copy of the replay (notes-polish.test.js, Run 3) and the 24 kHz replay (notes-hires.test.js, Run 4), plus the note pop-up patched in place while it listens (pm-note-dock-test.js).",
|
|
2
|
+
"_comment": "meeting-notes Runs 1 to 10: the Notes engine and routes (notes.test.js), local Whisper, line labels and repository checks (notes-listen.test.js), the final pass after Stop and the Wispr Flow fetch (notes-final.test.js), Qwen3-ASR kept loaded (notes-qwen.test.js), and the Notes view in a browser (pm-notes-test.js), the echo canceller for the replay (notes-echo.test.js, notes-audio-quality Run 2) and the optional cleaned copy of the replay (notes-polish.test.js, Run 3) and the 24 kHz replay (notes-hires.test.js, Run 4), plus the note pop-up patched in place while it listens (pm-note-dock-test.js). notes-recording-reliability adds the duplication fixture (notes-duplication.test.js): duplicated audio and transcript across retry, restart, two tabs and a late chunk, and the live guards.",
|
|
3
3
|
"modules": [
|
|
4
4
|
"notes.test.js",
|
|
5
5
|
"notes-listen.test.js",
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"notes-qwen.test.js",
|
|
8
8
|
"notes-echo.test.js",
|
|
9
9
|
"notes-polish.test.js",
|
|
10
|
-
"notes-hires.test.js"
|
|
10
|
+
"notes-hires.test.js",
|
|
11
|
+
"notes-duplication.test.js"
|
|
11
12
|
],
|
|
12
13
|
"panels": [
|
|
13
14
|
"pm-notes-test.js",
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
"pm-note-dock-test.js": 20,
|
|
23
24
|
"notes-echo.test.js": 25,
|
|
24
25
|
"notes-polish.test.js": 3,
|
|
25
|
-
"notes-hires.test.js": 10
|
|
26
|
+
"notes-hires.test.js": 10,
|
|
27
|
+
"notes-duplication.test.js": 25
|
|
26
28
|
}
|
|
27
29
|
}
|