kiokuko-dsh 0.1.91 → 0.1.92
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/dist/client.cjs +452 -204
- package/dist/client.d.ts.map +1 -1
- package/dist/dsh/core/host.d.ts.map +1 -1
- package/dist/dsh/core/host.js +9 -2
- package/dist/dsh/core/host.js.map +1 -1
- package/dist/dsh/host-adapter/admission.d.ts.map +1 -1
- package/dist/dsh/host-adapter/admission.js +3 -2
- package/dist/dsh/host-adapter/admission.js.map +1 -1
- package/dist/dsh/lisp/coding-choice.d.ts.map +1 -1
- package/dist/dsh/lisp/coding-choice.js +7 -1
- package/dist/dsh/lisp/coding-choice.js.map +1 -1
- package/docs/keyboard-shortcuts.md +34 -0
- package/package.json +4 -2
package/dist/client.cjs
CHANGED
|
@@ -338,9 +338,9 @@ window.__ModuleLoader__.load({
|
|
|
338
338
|
...(error ? [jsx('p', { role: 'alert', children: error })] : []),
|
|
339
339
|
...(details ? [jsx('pre', { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: details })] : []),
|
|
340
340
|
jsx('p', { children: '未確定の変更は自動で再実行・復元しません。詳細は /kioku-lisp diagnostics でも確認できます。' }),
|
|
341
|
-
jsx(
|
|
342
|
-
|
|
343
|
-
|
|
341
|
+
jsx(NumberedPicker, { label: 'Lisp の操作', disabled: busy,
|
|
342
|
+
options: [{ value: 'cancel', label: '停止する' }, { value: 'recover', label: '照合して新しい Lisp を起動', disabled: ['STOP_UNCONFIRMED', 'SUSPENDED'].includes(status?.state ?? '') }, { value: 'close', label: '閉じる' }],
|
|
343
|
+
onChoose: (value) => value === 'close' ? setOpen(false) : void act(value) }),
|
|
344
344
|
] }) })] });
|
|
345
345
|
}
|
|
346
346
|
/** Read-only, Session-bound display with explicit delivery acknowledgement after render. */
|
|
@@ -447,17 +447,104 @@ window.__ModuleLoader__.load({
|
|
|
447
447
|
] });
|
|
448
448
|
}
|
|
449
449
|
const intakeDrafts = new WeakMap();
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
450
|
+
// Named native cards may be reconstructed when reopened. Scope drafts to both
|
|
451
|
+
// native session and request identity, with bounded retention for abandoned cards.
|
|
452
|
+
const namedIntakeDrafts = new Map();
|
|
453
|
+
const emptyDraft = () => ({ selected: null, checked: [], custom: '', ordinal: '' });
|
|
454
|
+
function draftKey(pending) {
|
|
455
|
+
return pending.sessionId && pending.dismissal === 'hide' ? JSON.stringify([pending.sessionId, pending.key]) : undefined;
|
|
456
|
+
}
|
|
457
|
+
function readIntakeDraft(pending) {
|
|
458
|
+
return intakeDrafts.get(pending) ?? namedIntakeDrafts.get(draftKey(pending) ?? '') ?? { page: 0, drafts: pending.questions.map(emptyDraft) };
|
|
459
|
+
}
|
|
460
|
+
function saveIntakeDraft(pending, batch) {
|
|
461
|
+
const key = draftKey(pending);
|
|
462
|
+
if (!batch) {
|
|
463
|
+
intakeDrafts.delete(pending);
|
|
464
|
+
if (key)
|
|
465
|
+
namedIntakeDrafts.delete(key);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
intakeDrafts.set(pending, batch);
|
|
469
|
+
if (key) {
|
|
470
|
+
namedIntakeDrafts.delete(key);
|
|
471
|
+
namedIntakeDrafts.set(key, batch);
|
|
472
|
+
if (namedIntakeDrafts.size > 100)
|
|
473
|
+
namedIntakeDrafts.delete(namedIntakeDrafts.keys().next().value);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function pendingState(pending) {
|
|
477
|
+
return pending.snapshot?.() ?? { state: 'open', channel: 'waterfall', closed: false };
|
|
478
|
+
}
|
|
479
|
+
function dismissQuestion(pending) {
|
|
480
|
+
// Prefer the host's current semantics; a failed hide must never cancel instead.
|
|
481
|
+
return pending.dismiss ? pending.dismiss() : pending.cancel();
|
|
482
|
+
}
|
|
483
|
+
function composingKey(event) {
|
|
484
|
+
return !!(event.repeat || event.isComposing || event.keyCode === 229 || event.nativeEvent?.isComposing || event.nativeEvent?.keyCode === 229);
|
|
485
|
+
}
|
|
486
|
+
function editingKey(event) {
|
|
487
|
+
const target = event.target;
|
|
488
|
+
return !!(target?.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target?.tagName ?? ''));
|
|
489
|
+
}
|
|
490
|
+
function shortcutPlatform() {
|
|
491
|
+
const mac = /Mac|iPhone|iPad|iPod/u.test(globalThis.navigator?.platform ?? '');
|
|
492
|
+
const ua = globalThis.navigator?.userAgent ?? '';
|
|
493
|
+
const control = !mac || /Safari\//u.test(ua) && !/(?:Chrome|Chromium|CriOS|Edg|OPR)\//u.test(ua);
|
|
494
|
+
return { control, label: control ? 'Ctrl' : 'Cmd', aria: control ? 'Control' : 'Meta' };
|
|
495
|
+
}
|
|
496
|
+
function modifiedKey(event) {
|
|
497
|
+
return !!(shortcutPlatform().control ? event.ctrlKey && !event.metaKey : event.metaKey && !event.ctrlKey);
|
|
498
|
+
}
|
|
499
|
+
function shortcutLabel(index, multi = false) {
|
|
500
|
+
return index < 9 ? `${shortcutPlatform().label}+${index + 1}` : `${index + 1} → ${multi ? 'Space' : 'Enter'}`;
|
|
501
|
+
}
|
|
502
|
+
function shortcutAria(index, count) {
|
|
503
|
+
return index < 9 ? { 'aria-keyshortcuts': `${count <= 9 ? `${index + 1} ` : ''}${shortcutPlatform().aria}+${index + 1}` } : {};
|
|
504
|
+
}
|
|
505
|
+
const shortcutOwners = new WeakMap();
|
|
506
|
+
/** Only the focused group (or the most recently focused visible group) owns a key. */
|
|
507
|
+
function useShortcutGroup(ref, handler) {
|
|
508
|
+
const latest = useRef(handler);
|
|
509
|
+
latest.current = handler;
|
|
510
|
+
useEffect(() => {
|
|
511
|
+
const element = ref.current, doc = element?.ownerDocument;
|
|
512
|
+
if (!element || !doc)
|
|
513
|
+
return;
|
|
514
|
+
const stack = shortcutOwners.get(doc) ?? [];
|
|
515
|
+
shortcutOwners.set(doc, stack);
|
|
516
|
+
stack.push(element);
|
|
517
|
+
const claim = () => { const i = stack.indexOf(element); if (i >= 0)
|
|
518
|
+
stack.splice(i, 1); stack.push(element); };
|
|
519
|
+
const listener = (event) => {
|
|
520
|
+
if (event.defaultPrevented || !element.isConnected || !element.getClientRects().length)
|
|
521
|
+
return;
|
|
522
|
+
const target = event.target;
|
|
523
|
+
if (target?.nodeType && !element.contains(target))
|
|
524
|
+
return;
|
|
525
|
+
const group = target?.closest?.('[data-kiokuko-shortcuts]');
|
|
526
|
+
if (group ? group !== element : stack.filter(item => item.isConnected && item.getClientRects().length).at(-1) !== element)
|
|
527
|
+
return;
|
|
528
|
+
const dialog = target?.closest?.('dialog,[role="dialog"]');
|
|
529
|
+
if (dialog && !dialog.contains(element))
|
|
530
|
+
return;
|
|
531
|
+
if (group === element)
|
|
532
|
+
claim();
|
|
533
|
+
latest.current(event);
|
|
534
|
+
};
|
|
535
|
+
element.addEventListener?.('focusin', claim);
|
|
536
|
+
element.addEventListener?.('pointerdown', claim);
|
|
537
|
+
doc.addEventListener('keydown', listener, true);
|
|
538
|
+
return () => {
|
|
539
|
+
element.removeEventListener?.('focusin', claim);
|
|
540
|
+
element.removeEventListener?.('pointerdown', claim);
|
|
541
|
+
doc.removeEventListener('keydown', listener, true);
|
|
542
|
+
const i = stack.indexOf(element);
|
|
543
|
+
if (i >= 0)
|
|
544
|
+
stack.splice(i, 1);
|
|
545
|
+
};
|
|
546
|
+
}, [ref]);
|
|
547
|
+
}
|
|
461
548
|
function isEnnoSearchQuestion(question) {
|
|
462
549
|
return question.header === '実行方式とモデル' && /^enno-(?:provider|model)-(?:ideal|zenki|goki|worker|check)$/u.test(question.id);
|
|
463
550
|
}
|
|
@@ -468,230 +555,285 @@ window.__ModuleLoader__.load({
|
|
|
468
555
|
return 'value';
|
|
469
556
|
return 'choice';
|
|
470
557
|
}
|
|
471
|
-
/**
|
|
472
|
-
* Decide whether this plugin's numbered card owns a native question carrier.
|
|
473
|
-
*
|
|
474
|
-
* The card claims every single-select question carrying one to nine options,
|
|
475
|
-
* whoever asked it — Kiokuko's own intake, an Enno or Deep selection, another
|
|
476
|
-
* plugin, or a question the model composed in chat — so a number-key plus Enter
|
|
477
|
-
* shortcut is never missing from a question this composer shows. Longer catalogs
|
|
478
|
-
* are claimed only by the Enno and Deep selection questions, whose typed search
|
|
479
|
-
* and value flows own their own addressing. Multi-select batches, optionless
|
|
480
|
-
* prompts, and multi-question batches have no single number per answer and stay
|
|
481
|
-
* with the native composer.
|
|
482
|
-
*/
|
|
483
|
-
function isSupportedQuestion(question) {
|
|
484
|
-
if (question === undefined || question.multiSelect === true)
|
|
485
|
-
return false;
|
|
486
|
-
const options = question.options?.length ?? 0;
|
|
487
|
-
if (options < 1)
|
|
488
|
-
return false;
|
|
489
|
-
if (options <= DIGIT_ADDRESSABLE_OPTIONS)
|
|
490
|
-
return true;
|
|
491
|
-
if (question.header === '実行方式とモデル' && (ENNO_SELECTION_QUESTIONS.includes(question.id) || isEnnoSearchQuestion(question)))
|
|
492
|
-
return true;
|
|
493
|
-
if (question.header === 'Deep planning' && DEEP_SELECTION_QUESTIONS.includes(question.id))
|
|
494
|
-
return true;
|
|
495
|
-
return false;
|
|
496
|
-
}
|
|
497
558
|
function intakePending(props) {
|
|
498
|
-
const
|
|
499
|
-
if (!
|
|
559
|
+
const p = props.pendingInteraction;
|
|
560
|
+
if (!p || !Array.isArray(p.questions) || !p.questions.length || typeof p.answer !== 'function'
|
|
561
|
+
|| typeof p.dismiss !== 'function' && typeof p.cancel !== 'function')
|
|
562
|
+
return null;
|
|
563
|
+
if (p.sessionId && typeof props.sessionId === 'string' && props.sessionId !== p.sessionId)
|
|
500
564
|
return null;
|
|
501
|
-
|
|
502
|
-
|
|
565
|
+
if (!p.questions.every(q => typeof q.id === 'string' && typeof q.question === 'string'
|
|
566
|
+
&& (q.options === undefined || Array.isArray(q.options) && q.options.every((o) => typeof o.label === 'string'))))
|
|
503
567
|
return null;
|
|
504
|
-
if (
|
|
505
|
-
return
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
&&
|
|
568
|
+
if (p.kind === 'question')
|
|
569
|
+
return p;
|
|
570
|
+
const q = p.questions[0];
|
|
571
|
+
return p.kind === 'plan-review' && p.questions.length === 1 && !q.multiSelect && q.intent?.kind === 'plan-review'
|
|
572
|
+
&& typeof q.detail === 'string' && (q.options?.length ?? 0) <= 2 && q.options?.some((o) => o.label === q.intent.approve) ? p : null;
|
|
509
573
|
}
|
|
510
|
-
/**
|
|
511
|
-
* Native pending carrier, plugin-only presentation. The card answers through the
|
|
512
|
-
* carrier's own protocol, so claiming a question does not change what the asker
|
|
513
|
-
* receives — only how the option can be chosen.
|
|
514
|
-
*/
|
|
515
574
|
function IntakeQuestion(props) {
|
|
516
|
-
const pending = props.matched;
|
|
517
|
-
|
|
518
|
-
return jsx(IntakeQuestionCard, { key: pending.key, pending,
|
|
575
|
+
const pending = props.matched, t = props.t;
|
|
576
|
+
return jsx(IntakeQuestionCard, { key: `${pending.sessionId ?? ''}:${pending.key}`, pending,
|
|
519
577
|
...(pending.kind === 'plan-review' ? { reviewCopy: { discuss: t('review.discuss'),
|
|
520
578
|
labels: { code: { copyLabel: t('review.copy'), copiedLabel: t('review.copied') }, footnotes: t('review.footnotes') } } } : {}),
|
|
521
579
|
});
|
|
522
580
|
}
|
|
523
581
|
function IntakeQuestionCard(props) {
|
|
524
582
|
const pending = props.pending;
|
|
525
|
-
const reviewing = pending.kind === 'plan-review';
|
|
526
583
|
const reviewCopy = props.reviewCopy;
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
const
|
|
541
|
-
const [draft, setDraft] = useState(() => intakeDrafts.get(pending) ?? { selected: null, custom: '' });
|
|
542
|
-
const [busy, setBusy] = useState(false);
|
|
543
|
-
const [error, setError] = useState('');
|
|
544
|
-
const inFlight = useRef(false);
|
|
545
|
-
const mounted = useRef(false);
|
|
546
|
-
const card = useRef(null);
|
|
547
|
-
const optionElements = useRef([]);
|
|
584
|
+
const reviewing = pending.kind === 'plan-review';
|
|
585
|
+
const [batch, setBatch] = useState(() => readIntakeDraft(pending));
|
|
586
|
+
const [snapshot, setSnapshot] = useState(() => pendingState(pending));
|
|
587
|
+
const [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
588
|
+
const inFlight = useRef(false), mounted = useRef(false), sent = useRef(false);
|
|
589
|
+
const generation = useRef(0);
|
|
590
|
+
const sentChannel = useRef('none');
|
|
591
|
+
const card = useRef(null), optionElements = useRef([]);
|
|
592
|
+
const original = pending.questions[batch.page];
|
|
593
|
+
const options = original.options ?? [];
|
|
594
|
+
const question = { ...original, options: reviewing ? [{ label: reviewCopy.discuss },
|
|
595
|
+
...options.filter(o => o.label !== original.intent.approve), ...options.filter(o => o.label === original.intent.approve)] : options };
|
|
596
|
+
const draft = batch.drafts[batch.page], multi = question.multiSelect === true, inputKind = questionInputKind(question);
|
|
597
|
+
const editable = () => !pending.review && !pendingState(pending).closed && pendingState(pending).channel !== 'none';
|
|
548
598
|
useEffect(() => {
|
|
599
|
+
generation.current++;
|
|
549
600
|
mounted.current = true;
|
|
601
|
+
inFlight.current = false;
|
|
602
|
+
sent.current = false;
|
|
603
|
+
setBusy(false);
|
|
604
|
+
setError('');
|
|
605
|
+
setBatch(readIntakeDraft(pending));
|
|
606
|
+
const sync = () => {
|
|
607
|
+
const next = pendingState(pending);
|
|
608
|
+
setSnapshot(next);
|
|
609
|
+
if (next.closed) {
|
|
610
|
+
saveIntakeDraft(pending);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (sent.current && sentChannel.current === 'waterfall' && next.channel === 'rpc') {
|
|
614
|
+
sent.current = false;
|
|
615
|
+
inFlight.current = false;
|
|
616
|
+
setBusy(false);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
const unsubscribe = pending.subscribe?.(sync);
|
|
620
|
+
sync();
|
|
550
621
|
card.current?.focus();
|
|
551
|
-
|
|
622
|
+
pending.holdFocus?.();
|
|
623
|
+
const win = card.current?.ownerDocument?.defaultView;
|
|
624
|
+
const release = () => pending.releaseFocus?.();
|
|
625
|
+
const hold = () => { if (card.current?.contains(card.current.ownerDocument.activeElement))
|
|
626
|
+
pending.holdFocus?.(); };
|
|
627
|
+
win?.addEventListener('blur', release);
|
|
628
|
+
win?.addEventListener('focus', hold);
|
|
629
|
+
return () => { generation.current++; mounted.current = false; unsubscribe?.(); pending.releaseFocus?.(); win?.removeEventListener('blur', release); win?.removeEventListener('focus', hold); };
|
|
552
630
|
}, [pending]);
|
|
553
|
-
useEffect(() => {
|
|
554
|
-
|
|
555
|
-
optionElements.current[draft.selected]?.scrollIntoView({ block: 'nearest' });
|
|
556
|
-
}, [draft.selected]);
|
|
631
|
+
useEffect(() => { if (draft.selected !== null)
|
|
632
|
+
optionElements.current[draft.selected]?.scrollIntoView({ block: 'nearest' }); }, [draft.selected]);
|
|
557
633
|
const update = (value) => {
|
|
558
|
-
if (inFlight.current)
|
|
634
|
+
if (inFlight.current || !editable())
|
|
559
635
|
return;
|
|
560
|
-
|
|
561
|
-
|
|
636
|
+
const current = readIntakeDraft(pending), next = { ...current, drafts: current.drafts.map((d, i) => i === current.page ? value : d) };
|
|
637
|
+
saveIntakeDraft(pending, next);
|
|
638
|
+
setBatch(next);
|
|
562
639
|
setError('');
|
|
640
|
+
pending.engage?.();
|
|
563
641
|
};
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (!cancel && selected === null && custom === '') {
|
|
572
|
-
setError(reviewing ? '選択肢を選んでから確定してください。' : '選択肢を選ぶか、自由入力してください。');
|
|
642
|
+
const choose = (index) => {
|
|
643
|
+
const d = readIntakeDraft(pending).drafts[batch.page];
|
|
644
|
+
update({ selected: index, checked: multi ? d.checked.includes(index) ? d.checked.filter(n => n !== index) : [...d.checked, index] : [], custom: '', ordinal: '' });
|
|
645
|
+
};
|
|
646
|
+
const page = (index) => { const next = { ...readIntakeDraft(pending), page: index }; saveIntakeDraft(pending, next); setBatch(next); setError(''); card.current?.focus(); };
|
|
647
|
+
const settle = (cancel = false, all = false) => {
|
|
648
|
+
if (inFlight.current || !mounted.current || pendingState(pending).closed || !cancel && !editable())
|
|
573
649
|
return;
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
650
|
+
const current = readIntakeDraft(pending), d = current.drafts[current.page];
|
|
651
|
+
const answers = [];
|
|
652
|
+
if (!cancel) {
|
|
653
|
+
for (let i = 0; i <= (all ? pending.questions.length - 1 : current.page); i++) {
|
|
654
|
+
const q = pending.questions[i], value = current.drafts[i], opts = reviewing ? question.options : q.options ?? [];
|
|
655
|
+
let selected = q.multiSelect ? value.checked : value.selected === null ? [] : [value.selected];
|
|
656
|
+
let custom = value.custom.trim();
|
|
657
|
+
if (value.ordinal && (Number(value.ordinal) < 1 || Number(value.ordinal) > opts.length || q.multiSelect)) {
|
|
658
|
+
setError(q.multiSelect ? '入力した番号はSpaceでチェックしてください。' : `番号は1〜${opts.length}で入力してください。`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (!reviewing && questionInputKind(q) === 'choice' && opts.length && /^[0-90-9]+$/u.test(custom)) {
|
|
662
|
+
const n = Number(custom.normalize('NFKC'));
|
|
663
|
+
if (n < 1 || n > opts.length) {
|
|
664
|
+
setError(`番号は1〜${opts.length}で入力してください。`);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
selected = q.multiSelect ? [...new Set([...selected, n - 1])] : [n - 1];
|
|
668
|
+
custom = '';
|
|
669
|
+
}
|
|
670
|
+
if (!selected.length && !custom) {
|
|
671
|
+
page(i);
|
|
672
|
+
setError('選択肢を選ぶか、自由入力してください。');
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
answers.push({ id: q.id, selected: selected.map(n => opts[n].label), ...(custom ? { custom } : {}) });
|
|
676
|
+
}
|
|
677
|
+
if (!all && current.page < pending.questions.length - 1) {
|
|
678
|
+
page(current.page + 1);
|
|
579
679
|
return;
|
|
580
680
|
}
|
|
581
|
-
selected = ordinal - 1;
|
|
582
|
-
custom = '';
|
|
583
681
|
}
|
|
584
682
|
inFlight.current = true;
|
|
585
683
|
setBusy(true);
|
|
586
684
|
setError('');
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
685
|
+
const epoch = generation.current;
|
|
686
|
+
const currentCard = () => mounted.current && generation.current === epoch;
|
|
687
|
+
const dismissing = cancel || reviewing && d.selected === 0;
|
|
688
|
+
void Promise.resolve().then(() => {
|
|
689
|
+
if (!currentCard() || pendingState(pending).closed || !dismissing && !editable())
|
|
690
|
+
throw new Error('この質問には現在回答できません。');
|
|
691
|
+
sentChannel.current = pendingState(pending).channel;
|
|
692
|
+
return dismissing ? dismissQuestion(pending) : pending.answer({ answers });
|
|
693
|
+
}).then(() => {
|
|
694
|
+
if (!currentCard())
|
|
594
695
|
return;
|
|
696
|
+
if (!pending.snapshot || pendingState(pending).closed || dismissing && pending.dismissal !== 'hide')
|
|
697
|
+
saveIntakeDraft(pending);
|
|
698
|
+
if (pending.snapshot && !dismissing && !pendingState(pending).closed) {
|
|
699
|
+
sent.current = true;
|
|
700
|
+
if (sentChannel.current === 'waterfall' && pendingState(pending).channel === 'rpc') {
|
|
701
|
+
sent.current = false;
|
|
702
|
+
inFlight.current = false;
|
|
703
|
+
if (mounted.current)
|
|
704
|
+
setBusy(false);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}).catch(cause => { if (currentCard()) {
|
|
595
708
|
inFlight.current = false;
|
|
596
709
|
setBusy(false);
|
|
597
710
|
setError(cause instanceof Error ? cause.message : String(cause));
|
|
598
|
-
});
|
|
711
|
+
} });
|
|
599
712
|
};
|
|
600
|
-
const selectShortcut = (event) => {
|
|
601
|
-
if (busy || inFlight.current || event.repeat || event.isComposing || event.keyCode === 229
|
|
602
|
-
|| event.nativeEvent?.isComposing || event.nativeEvent?.keyCode === 229 || event.altKey || event.shiftKey
|
|
603
|
-
|| !(controlShortcut ? event.ctrlKey && !event.metaKey : event.metaKey && !event.ctrlKey))
|
|
604
|
-
return false;
|
|
605
|
-
const digit = /^(?:Digit|Numpad)([1-9])$/u.exec(event.code ?? '')?.[1] ?? event.key;
|
|
606
|
-
if (!/^[1-9]$/u.test(digit) || Number(digit) > question.options.length)
|
|
607
|
-
return false;
|
|
608
|
-
event.preventDefault();
|
|
609
|
-
event.stopPropagation();
|
|
610
|
-
update({ selected: Number(digit) - 1, custom: '' });
|
|
611
|
-
card.current?.focus();
|
|
612
|
-
return true;
|
|
613
|
-
};
|
|
614
|
-
useEffect(() => {
|
|
615
|
-
const element = card.current;
|
|
616
|
-
const owner = element?.ownerDocument;
|
|
617
|
-
if (!owner)
|
|
618
|
-
return;
|
|
619
|
-
// Capture before the host's composer handlers, even if it has moved focus.
|
|
620
|
-
// Only the visible, mounted question owns these modified shortcuts.
|
|
621
|
-
const listener = (event) => {
|
|
622
|
-
if (element.isConnected && element.getClientRects().length > 0)
|
|
623
|
-
selectShortcut(event);
|
|
624
|
-
};
|
|
625
|
-
owner.addEventListener('keydown', listener, true);
|
|
626
|
-
return () => owner.removeEventListener('keydown', listener, true);
|
|
627
|
-
}, [pending, busy, controlShortcut]);
|
|
628
713
|
const keyDown = (event) => {
|
|
629
|
-
if (
|
|
714
|
+
if (event.defaultPrevented || inFlight.current || composingKey(event) || event.altKey || event.shiftKey)
|
|
715
|
+
return;
|
|
716
|
+
if (event.key === 'Escape') {
|
|
717
|
+
event.preventDefault();
|
|
718
|
+
event.stopPropagation();
|
|
719
|
+
void settle(true);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (pending.review && event.key === 'Enter' && !event.ctrlKey && !event.metaKey && event.target?.tagName !== 'BUTTON' && batch.page < pending.questions.length - 1) {
|
|
723
|
+
event.preventDefault();
|
|
724
|
+
event.stopPropagation();
|
|
725
|
+
page(batch.page + 1);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (!editable())
|
|
630
729
|
return;
|
|
631
|
-
if (
|
|
632
|
-
|
|
730
|
+
if (event.key === 'Enter' && modifiedKey(event)) {
|
|
731
|
+
event.preventDefault();
|
|
732
|
+
event.stopPropagation();
|
|
733
|
+
settle(false, true);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (editingKey(event)) {
|
|
737
|
+
if (event.key === 'Enter' && !event.ctrlKey && !event.metaKey && !event.target?.isContentEditable) {
|
|
738
|
+
event.preventDefault();
|
|
739
|
+
event.stopPropagation();
|
|
740
|
+
if (!multi || batch.page < pending.questions.length - 1)
|
|
741
|
+
settle();
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const modified = modifiedKey(event);
|
|
746
|
+
if ((event.ctrlKey || event.metaKey) && !modified)
|
|
747
|
+
return;
|
|
748
|
+
const digit = /^(?:Digit|Numpad)([0-9])$/u.exec(event.code ?? '')?.[1] ?? event.key;
|
|
749
|
+
const d = readIntakeDraft(pending).drafts[batch.page];
|
|
750
|
+
if (modified && /^[1-9]$/u.test(digit) && Number(digit) <= question.options.length) {
|
|
751
|
+
event.preventDefault();
|
|
752
|
+
event.stopPropagation();
|
|
753
|
+
choose(Number(digit) - 1);
|
|
754
|
+
card.current?.focus();
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (modified)
|
|
633
758
|
return;
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (!editing && (/^[0-9]$/u.test(event.key) || (event.key === 'Backspace' && current.ordinal))) {
|
|
637
|
-
const multipleDigits = question.options.length > 9;
|
|
638
|
-
if (!multipleDigits && (!/^[1-9]$/u.test(event.key) || Number(event.key) > question.options.length))
|
|
759
|
+
if (question.options.length && (/^[0-9]$/u.test(digit) || event.key === 'Backspace' && d.ordinal)) {
|
|
760
|
+
if (question.options.length <= 9 && (!/^[1-9]$/u.test(digit) || Number(digit) > question.options.length))
|
|
639
761
|
return;
|
|
640
762
|
event.preventDefault();
|
|
641
763
|
event.stopPropagation();
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
764
|
+
if (question.options.length <= 9)
|
|
765
|
+
choose(Number(digit) - 1);
|
|
766
|
+
else {
|
|
767
|
+
const ordinal = event.key === 'Backspace' ? d.ordinal.slice(0, -1) : (d.ordinal + digit).slice(0, 9);
|
|
768
|
+
const n = Number(ordinal), valid = n >= 1 && n <= question.options.length;
|
|
769
|
+
update({ ...d, ordinal, custom: '', selected: !multi && valid ? n - 1 : multi ? d.selected : null });
|
|
770
|
+
if (ordinal && !valid)
|
|
771
|
+
setError(`番号「${ordinal}」は範囲外です。Backspaceで訂正してください。`);
|
|
772
|
+
}
|
|
648
773
|
card.current?.focus();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (event.key === ' ' && multi && d.ordinal) {
|
|
777
|
+
event.preventDefault();
|
|
778
|
+
event.stopPropagation();
|
|
779
|
+
const n = Number(d.ordinal);
|
|
780
|
+
if (n >= 1 && n <= question.options.length)
|
|
781
|
+
choose(n - 1);
|
|
782
|
+
return;
|
|
649
783
|
}
|
|
650
|
-
|
|
784
|
+
if (event.key === 'Enter' && event.target?.tagName !== 'BUTTON') {
|
|
651
785
|
event.preventDefault();
|
|
652
786
|
event.stopPropagation();
|
|
653
|
-
|
|
787
|
+
if (!multi || batch.page < pending.questions.length - 1)
|
|
788
|
+
settle();
|
|
654
789
|
}
|
|
655
790
|
};
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
791
|
+
useShortcutGroup(card, keyDown);
|
|
792
|
+
const disabled = busy || !editable(), titleId = `kiokuko-intake-${pending.key}`;
|
|
793
|
+
const hint = question.options.length ? `${shortcutPlatform().label}+1〜${Math.min(9, question.options.length)}、または数字で${multi ? 'チェック切替' : '選択'}。${question.options.length > 9 ? `10以上は番号を続けて入力、Backspaceで訂正${multi ? '、Spaceでチェック' : ''}。` : ''}${multi ? `${shortcutPlatform().label}+Enterで全回答を送信。` : 'Enterで確定。'}` : `自由入力後、${multi ? shortcutPlatform().label + '+' : ''}Enterで確定。Shift+Enterで改行。`;
|
|
794
|
+
return jsxs('section', { className: 'kiokuko-intake', tabIndex: 0, 'data-kiokuko-shortcuts': '',
|
|
795
|
+
ref: (el) => { card.current = el; }, 'aria-labelledby': titleId, 'aria-busy': busy, onKeyDown: keyDown,
|
|
796
|
+
onFocusCapture: () => pending.holdFocus?.(),
|
|
797
|
+
onBlurCapture: (event) => { if (!card.current?.contains(event.relatedTarget))
|
|
798
|
+
pending.releaseFocus?.(); },
|
|
660
799
|
children: [
|
|
661
800
|
jsxs('header', { children: [jsx('h2', { id: titleId, children: question.question }),
|
|
662
|
-
jsx('button', { type: 'button', disabled: busy, onClick: () => settle(true), 'aria-label': '質問を閉じる', children: '
|
|
801
|
+
jsx('button', { type: 'button', disabled: busy || snapshot.closed, onClick: () => settle(true), 'aria-label': '質問を閉じる', children: '閉じる(Esc)' })] }),
|
|
802
|
+
jsx('p', { id: `${titleId}-keys`, children: pending.review ? '回答済みの内容です。Enterで次の質問を表示、Escで閉じます。' : hint }),
|
|
663
803
|
jsxs('div', { className: 'kiokuko-intake-body', children: [
|
|
664
|
-
reviewing ? jsx(MarkdownText, { text: question.detail, labels: reviewCopy.labels })
|
|
665
|
-
|
|
666
|
-
jsx('p', { children:
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
jsx('div', { 'aria-label': '選択肢', children: question.options.map((option, index) => jsxs('button', {
|
|
670
|
-
|
|
671
|
-
ref: (
|
|
672
|
-
'aria-pressed': draft.selected === index,
|
|
673
|
-
...(
|
|
674
|
-
onClick: () =>
|
|
804
|
+
reviewing ? jsx(MarkdownText, { text: question.detail, labels: reviewCopy.labels }) : question.detail ? jsx('p', { children: question.detail }) : null,
|
|
805
|
+
!pending.review && !snapshot.closed && snapshot.state === 'continued' ? jsx('p', { children: '処理は継続中です。この質問には後から回答できます。' }) : null,
|
|
806
|
+
snapshot.countdown ? jsx('p', { children: `回答待ち: ${Math.ceil(snapshot.countdown.remainingMs / 1000)}秒${snapshot.countdown.running ? '' : '(操作中は停止)'}` }) : null,
|
|
807
|
+
pending.review ? jsx('p', { children: `回答済み: ${pending.review.find(a => a.id === question.id)?.selected.join('、') ?? ''} ${pending.review.find(a => a.id === question.id)?.custom ?? ''}` }) : null,
|
|
808
|
+
!pending.review && !editable() ? jsx('p', { role: 'status', children: snapshot.closed ? 'この質問は終了しました。' : '回答経路の接続を待っています。' }) : null,
|
|
809
|
+
jsx('div', { 'aria-label': '選択肢', 'aria-describedby': `${titleId}-keys`, children: question.options.map((option, index) => jsxs('button', {
|
|
810
|
+
type: 'button', className: 'kiokuko-intake-option', disabled,
|
|
811
|
+
ref: (el) => { optionElements.current[index] = el; },
|
|
812
|
+
...(multi ? { role: 'checkbox', 'aria-checked': draft.checked.includes(index) } : { 'aria-pressed': draft.selected === index }),
|
|
813
|
+
...(!pending.review ? shortcutAria(index, question.options.length) : {}),
|
|
814
|
+
onClick: () => choose(index),
|
|
675
815
|
onKeyDown: (event) => {
|
|
676
|
-
if (event.key
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
816
|
+
if (event.key === 'Enter' && !multi && !composingKey(event) && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey
|
|
817
|
+
&& readIntakeDraft(pending).drafts[batch.page].selected === index) {
|
|
818
|
+
event.preventDefault();
|
|
819
|
+
event.stopPropagation();
|
|
820
|
+
settle();
|
|
821
|
+
}
|
|
681
822
|
},
|
|
682
823
|
children: [jsx('strong', { children: `${index + 1}. ${option.label}` }), option.description ? jsx('span', { children: option.description }) : null,
|
|
683
|
-
jsx('kbd', { className: 'kiokuko-intake-shortcut', 'aria-hidden': true, children: index
|
|
684
|
-
})) }),
|
|
685
|
-
!reviewing ? jsx('label', { htmlFor: `${titleId}-custom`, children: inputKind === 'search' ? '
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
!reviewing ? jsx('textarea', { id: `${titleId}-custom`, rows: 1, disabled: busy, value: draft.custom,
|
|
689
|
-
onChange: (event) => update({ selected: null, custom: event.target.value }),
|
|
824
|
+
!pending.review ? jsx('kbd', { className: 'kiokuko-intake-shortcut', 'aria-hidden': true, children: shortcutLabel(index, multi) }) : null],
|
|
825
|
+
}, index)) }),
|
|
826
|
+
!reviewing ? jsx('label', { htmlFor: `${titleId}-custom`, children: inputKind === 'search' ? '検索(数字も検索語として入力できます)' : inputKind === 'value' ? '値を入力' : '自由入力(任意・Shift+Enterで改行)' }) : null,
|
|
827
|
+
!reviewing ? jsx('textarea', { id: `${titleId}-custom`, rows: 1, disabled, value: draft.custom,
|
|
828
|
+
onChange: (e) => update({ ...emptyDraft(), checked: draft.checked, custom: e.target.value }),
|
|
690
829
|
}) : null,
|
|
691
830
|
] }),
|
|
692
831
|
jsxs('footer', { children: [
|
|
693
|
-
jsx('
|
|
694
|
-
jsx('
|
|
832
|
+
batch.page > 0 ? jsx('button', { type: 'button', disabled: busy, onClick: () => page(batch.page - 1), children: '前の質問' }) : null,
|
|
833
|
+
pending.questions.length > 1 ? jsx('span', { children: `${batch.page + 1}/${pending.questions.length}` }) : null,
|
|
834
|
+
jsx('span', { role: 'status', 'aria-live': 'polite', children: error || (busy ? '送信中…' : draft.ordinal ? `入力番号: ${draft.ordinal}` : multi ? `${draft.checked.length}件を選択中` : draft.selected === null ? '' : `${draft.selected + 1}. ${question.options[draft.selected].label}を選択中`) }),
|
|
835
|
+
pending.review ? batch.page < pending.questions.length - 1 ? jsx('button', { type: 'button', onClick: () => page(batch.page + 1), children: '次の質問(Enter)' }) : null
|
|
836
|
+
: jsx('button', { type: 'button', disabled: disabled || (multi ? !draft.checked.length && !draft.custom.trim() : draft.selected === null && !draft.custom.trim()), onClick: () => settle(), children: batch.page < pending.questions.length - 1 ? '次の質問(Enter)' : `確定(${multi ? shortcutPlatform().label + '+' : ''}Enter)` }),
|
|
695
837
|
] }),
|
|
696
838
|
],
|
|
697
839
|
});
|
|
@@ -701,6 +843,13 @@ window.__ModuleLoader__.load({
|
|
|
701
843
|
return undefined;
|
|
702
844
|
const style = document.createElement('style');
|
|
703
845
|
style.textContent = `
|
|
846
|
+
.kiokuko-numbered-picker{min-width:0;position:relative}
|
|
847
|
+
.kiokuko-numbered-options{border:1px solid var(--dsw-alias-border-l4,#bbb);padding:8px;border-radius:8px;max-height:50dvh;overflow:auto;min-width:0}
|
|
848
|
+
.kiokuko-numbered-options[hidden]{display:none}
|
|
849
|
+
.kiokuko-numbered-options button{display:flex;gap:12px;align-items:center;justify-content:space-between;overflow-wrap:anywhere;text-align:left;min-height:40px;width:100%}
|
|
850
|
+
.kiokuko-numbered-options kbd{white-space:nowrap;font:inherit;font-size:11px}
|
|
851
|
+
.kiokuko-numbered-options [aria-pressed=true]{outline:1px solid currentColor}
|
|
852
|
+
.kiokuko-numbered-picker :focus-visible{outline:2px solid Highlight;outline-offset:2px}
|
|
704
853
|
.kiokuko-intake{box-sizing:border-box;width:100%;max-width:680px;align-self:center;margin-inline:auto;border:1px solid var(--dsw-alias-border-l4,#bbb);border-radius:12px;padding:12px;background:var(--dsw-alias-background-primary,Canvas);color:var(--dsw-alias-label-primary,CanvasText);display:flex;flex-direction:column;min-height:0;max-height:min(480px,70dvh);gap:8px;font-size:14px}
|
|
705
854
|
.kiokuko-intake header,.kiokuko-intake footer{display:flex;gap:8px;align-items:center;flex-wrap:wrap;flex-shrink:0}
|
|
706
855
|
.kiokuko-intake h2{font-size:16px;line-height:1.4;margin:0;flex:1}
|
|
@@ -712,12 +861,13 @@ window.__ModuleLoader__.load({
|
|
|
712
861
|
.kiokuko-intake-option strong,.kiokuko-intake-option span{grid-column:1}
|
|
713
862
|
.kiokuko-intake-shortcut{grid-column:2;grid-row:1 / span 2;align-self:center;font-family:inherit;font-size:11px;line-height:1.4;white-space:nowrap}
|
|
714
863
|
.kiokuko-intake-option:last-child{margin-bottom:0}
|
|
715
|
-
.kiokuko-intake-option[aria-pressed=true]{border-color:var(--dsw-alias-label-primary,CanvasText);box-shadow:inset 0 0 0 1px currentColor;background:var(--dsw-alias-interactive-bg-hover,#eee)}
|
|
864
|
+
.kiokuko-intake-option[aria-pressed=true],.kiokuko-intake-option[aria-checked=true]{border-color:var(--dsw-alias-label-primary,CanvasText);box-shadow:inset 0 0 0 1px currentColor;background:var(--dsw-alias-interactive-bg-hover,#eee)}
|
|
716
865
|
.kiokuko-intake-option span{font-size:12px;line-height:1.4}
|
|
717
866
|
.kiokuko-intake label{font-size:12px}
|
|
718
867
|
.kiokuko-intake textarea{box-sizing:border-box;width:100%;min-height:36px;max-height:96px;resize:vertical;font:inherit;line-height:1.4;color:inherit;background:transparent;border:1px solid var(--dsw-alias-border-l4,#bbb);padding:8px;border-radius:8px}
|
|
719
868
|
.kiokuko-intake :focus-visible,.kiokuko-intake:focus-visible{outline:2px solid Highlight;outline-offset:2px}
|
|
720
869
|
.kiokuko-intake footer [role=status]{flex:1;min-width:0;font-size:12px;overflow-wrap:anywhere}
|
|
870
|
+
@media(max-width:640px){.kiokuko-intake{position:fixed;inset:auto 8px 8px;width:auto;max-width:none;max-height:min(480px,75dvh);z-index:10}}
|
|
721
871
|
`;
|
|
722
872
|
document.head.appendChild(style);
|
|
723
873
|
return () => style.remove();
|
|
@@ -979,6 +1129,97 @@ window.__ModuleLoader__.load({
|
|
|
979
1129
|
] }, hunk.id);
|
|
980
1130
|
}) });
|
|
981
1131
|
}
|
|
1132
|
+
/** A local selection surface: choosing never starts the consumer's operation. */
|
|
1133
|
+
function NumberedPicker(props) {
|
|
1134
|
+
const [open, setOpen] = useState(false), [index, setIndex] = useState(-1), [ordinal, setOrdinal] = useState('');
|
|
1135
|
+
const buffer = useRef(''), selected = useRef(-1), group = useRef(null), trigger = useRef(null);
|
|
1136
|
+
const shown = props.inline || open;
|
|
1137
|
+
const close = () => { setOpen(false); buffer.current = ''; setOrdinal(''); trigger.current?.focus(); };
|
|
1138
|
+
useEffect(() => { if (shown && !props.inline)
|
|
1139
|
+
group.current?.focus(); }, [shown, props.inline]);
|
|
1140
|
+
const pick = (n, confirm) => {
|
|
1141
|
+
const option = props.options[n];
|
|
1142
|
+
if (!option || option.disabled && !props.onToggle)
|
|
1143
|
+
return;
|
|
1144
|
+
selected.current = n;
|
|
1145
|
+
setIndex(n);
|
|
1146
|
+
if (props.inline || confirm)
|
|
1147
|
+
props.onChoose(option.value);
|
|
1148
|
+
if (confirm && !props.inline)
|
|
1149
|
+
close();
|
|
1150
|
+
};
|
|
1151
|
+
const keys = (e) => {
|
|
1152
|
+
if (!shown || props.disabled || composingKey(e) || editingKey(e) || e.altKey || e.shiftKey || e.defaultPrevented)
|
|
1153
|
+
return;
|
|
1154
|
+
if (e.key === 'Escape' && !props.inline) {
|
|
1155
|
+
e.preventDefault();
|
|
1156
|
+
e.stopPropagation();
|
|
1157
|
+
close();
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
const optionButton = e.target?.closest?.('button[data-kiokuko-option]');
|
|
1161
|
+
if (e.key === ' ' && props.onToggle && optionButton && !e.ctrlKey && !e.metaKey) {
|
|
1162
|
+
e.preventDefault();
|
|
1163
|
+
e.stopPropagation();
|
|
1164
|
+
const option = props.options[Number(optionButton.dataset.kiokukoOption)];
|
|
1165
|
+
if (option && !option.disabled)
|
|
1166
|
+
props.onToggle(option.value);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
if ((e.key === 'Enter' || e.key === ' ') && e.target?.tagName === 'BUTTON')
|
|
1170
|
+
return;
|
|
1171
|
+
const modified = modifiedKey(e);
|
|
1172
|
+
if ((e.ctrlKey || e.metaKey) && !modified)
|
|
1173
|
+
return;
|
|
1174
|
+
const digit = /^(?:Digit|Numpad)([0-9])$/u.exec(e.code ?? '')?.[1] ?? e.key;
|
|
1175
|
+
if (/^[0-9]$/u.test(digit) && (!modified || digit !== '0') || !modified && e.key === 'Backspace' && buffer.current) {
|
|
1176
|
+
e.preventDefault();
|
|
1177
|
+
e.stopPropagation();
|
|
1178
|
+
buffer.current = modified || props.options.length <= 9 ? digit : e.key === 'Backspace' ? buffer.current.slice(0, -1) : (buffer.current + digit).slice(0, 9);
|
|
1179
|
+
setOrdinal(buffer.current);
|
|
1180
|
+
pick(Number(buffer.current) - 1, false);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (modified)
|
|
1184
|
+
return;
|
|
1185
|
+
if (e.key === 'Enter' || e.key === ' ' && props.onToggle) {
|
|
1186
|
+
e.preventDefault();
|
|
1187
|
+
e.stopPropagation();
|
|
1188
|
+
const n = buffer.current ? Number(buffer.current) - 1 : selected.current;
|
|
1189
|
+
const item = props.options[n];
|
|
1190
|
+
if (!item)
|
|
1191
|
+
return;
|
|
1192
|
+
if (e.key === ' ') {
|
|
1193
|
+
if (!item.disabled)
|
|
1194
|
+
props.onToggle?.(item.value);
|
|
1195
|
+
}
|
|
1196
|
+
else
|
|
1197
|
+
pick(n, true);
|
|
1198
|
+
buffer.current = '';
|
|
1199
|
+
setOrdinal('');
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
useShortcutGroup(group, keys);
|
|
1203
|
+
// Keep the group mounted so a popup opened after its parent mounts acquires
|
|
1204
|
+
// the same document listener and relinquishes ownership when hidden.
|
|
1205
|
+
return jsxs('div', { className: 'kiokuko-numbered-picker', children: [
|
|
1206
|
+
!props.inline ? jsx('button', { type: 'button', ref: trigger, disabled: props.disabled, 'aria-haspopup': 'dialog', 'aria-expanded': open,
|
|
1207
|
+
onClick: () => setOpen(!open), children: `${props.label}: ${props.options.find(o => o.value === props.value)?.label ?? '選択'}` }) : null,
|
|
1208
|
+
jsxs('section', { hidden: !shown, tabIndex: 0, ref: (el) => { group.current = el; },
|
|
1209
|
+
'data-kiokuko-shortcuts': '', role: props.inline ? 'group' : 'dialog', 'aria-label': props.label, className: 'kiokuko-numbered-options', onKeyDown: keys,
|
|
1210
|
+
children: [jsx('p', { children: `${props.label} — 数字 / ${shortcutPlatform().label}+1〜9で選択。${props.inline ? 'Spaceでチェック切替。' : 'Enterで確定、Escで閉じる。'}10以上は連続入力、Backspaceで訂正。` }),
|
|
1211
|
+
jsx('p', { role: 'status', children: ordinal ? `入力番号: ${ordinal}${Number(ordinal) < 1 || Number(ordinal) > props.options.length ? '(範囲外)' : ''}` : '' }),
|
|
1212
|
+
...props.options.map((o, n) => jsxs('div', { className: 'kiokuko-review-file-row', children: [
|
|
1213
|
+
props.onToggle ? jsx('input', { type: 'checkbox', checked: props.checked?.includes(o.value) ?? false, disabled: props.disabled || o.disabled,
|
|
1214
|
+
'aria-label': `${o.label} を選択対象にする`, onChange: () => props.onToggle?.(o.value) }) : null,
|
|
1215
|
+
jsxs('button', { type: 'button', 'data-kiokuko-option': n, disabled: props.disabled || !props.onToggle && o.disabled, 'aria-pressed': (index < 0 ? props.value === o.value : index === n), ...shortcutAria(n, props.options.length),
|
|
1216
|
+
onClick: () => { buffer.current = ''; setOrdinal(''); pick(n, !props.inline); },
|
|
1217
|
+
children: [jsx('span', { children: `${n + 1}. ${o.label}` }), jsx('kbd', { children: shortcutLabel(n, !!props.onToggle) })] }),
|
|
1218
|
+
] }, o.value)),
|
|
1219
|
+
!props.inline ? jsx('button', { type: 'button', onClick: close, children: '閉じる(Esc)' }) : null,
|
|
1220
|
+
] }),
|
|
1221
|
+
] });
|
|
1222
|
+
}
|
|
982
1223
|
function DiffReviewTab(props) {
|
|
983
1224
|
const sessionId = String(props.sessionId);
|
|
984
1225
|
const useReview = props.useDiffReview;
|
|
@@ -1033,23 +1274,30 @@ window.__ModuleLoader__.load({
|
|
|
1033
1274
|
return jsxs('section', { className: 'kiokuko-review', 'aria-label': 'Diff レビュー', children: [
|
|
1034
1275
|
jsx('h2', { children: 'Diff レビュー' }),
|
|
1035
1276
|
jsxs('div', { className: 'kiokuko-review-controls', children: [
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1277
|
+
jsx(NumberedPicker, { label: '比較対象', value: state.mode, options: [
|
|
1278
|
+
{ value: 'current', label: '未コミット全体' }, { value: 'staged', label: 'ステージ済み' },
|
|
1279
|
+
{ value: 'unstaged', label: '未ステージ' }, { value: 'turn', label: 'このターン' }
|
|
1280
|
+
],
|
|
1281
|
+
onChoose: (value) => controller.change(sessionId, { mode: value }) }),
|
|
1282
|
+
...(state.mode === 'turn' ? [jsx(NumberedPicker, { label: 'ターン差分', value: String(state.turnSeq ?? ''),
|
|
1283
|
+
options: [{ value: '', label: '選択' }, ...state.turns.map(seq => ({ value: String(seq), label: `seq ${seq}` }))],
|
|
1284
|
+
onChoose: (value) => controller.change(sessionId, { turnSeq: value === '' ? undefined : Number(value) }) }, 'turn')] : []),
|
|
1042
1285
|
jsx('button', { type: 'button', disabled: state.busy || state.loading || state.cancelling || review?.state === 'analyzing' || state.mode === 'turn' && state.turnSeq === undefined, onClick: () => void controller.capture(sessionId), children: review ? '取得し直す' : '差分を取得' }),
|
|
1043
1286
|
review ? jsx('button', { type: 'button', disabled: state.loading || state.cancelling || state.busy && review.state !== 'analyzing', onClick: () => void controller.refreshReview(sessionId, review.reviewId), children: '状態を確認' }) : null,
|
|
1044
1287
|
] }),
|
|
1045
|
-
state.untracked.length ?
|
|
1288
|
+
state.untracked.length ? jsx(NumberedPicker, { label: '未追跡ファイル(明示選択)', inline: true,
|
|
1289
|
+
options: state.untracked.map(path => ({ value: path, label: path })), checked: state.selectedUntracked,
|
|
1290
|
+
onChoose: () => { }, onToggle: (path) => controller.change(sessionId, { selectedUntracked: state.selectedUntracked.includes(path)
|
|
1291
|
+
? state.selectedUntracked.filter(item => item !== path) : [...state.selectedUntracked, path] }) }) : null,
|
|
1046
1292
|
jsx('p', { role: 'status', 'aria-live': 'polite', children: state.cancelling ? '分析を停止中' : state.loading ? '確認中' : state.busy ? '処理中' : state.error || (state.availability === 'repo_unavailable' ? 'Git リポジトリを確認できません' : '') }),
|
|
1047
1293
|
review ? jsxs(Fragment, { children: [
|
|
1048
1294
|
jsx('p', { children: `${review.snapshot.capturedAt} / ${review.snapshot.mode} / ${REVIEW_STATES[review.state] ?? review.state} / 鮮度: ${REVIEW_FRESHNESS[review.freshness] ?? review.freshness}` }),
|
|
1049
1295
|
jsx('p', { children: `文脈: ${REVIEW_CONTEXT_SOURCES[review.context.source] ?? review.context.source} / メモリ: ${REVIEW_MEMORY_STATES[review.context.memory] ?? review.context.memory}${review.context.reason ? ` (${review.context.reason})` : ''}` }),
|
|
1050
1296
|
review.context.task ? jsx('p', { children: `タスク: ${review.context.task}` }) : null,
|
|
1051
1297
|
review.context.reviewInput ? jsx('p', { children: `このレビューの目的(利用者入力): ${review.context.reviewInput}` }) : null,
|
|
1052
|
-
...(review.context.candidates?.length ? [
|
|
1298
|
+
...(review.context.candidates?.length ? [jsx(NumberedPicker, { label: 'タスク', value: state.runId,
|
|
1299
|
+
options: [{ value: '', label: '指定なし' }, ...review.context.candidates.map(item => ({ value: item.runId, label: `${item.task} (${item.status})` }))],
|
|
1300
|
+
onChoose: (value) => controller.change(sessionId, { runId: value }) }, 'runs')] : []),
|
|
1053
1301
|
jsx('p', { children: `変更ファイル: ${review.snapshot.files.length} / ${review.snapshot.totalFiles}` }),
|
|
1054
1302
|
review.snapshot.totalFiles > review.snapshot.files.length ? jsx('p', { role: 'status', children: `${review.snapshot.totalFiles - review.snapshot.files.length} 件は取得上限で未収集です。全体リスクは未確定です。` }) : null,
|
|
1055
1303
|
jsx('p', { children: `送信対象: ${state.selected.length} ファイル / ${review.snapshot.files.filter(file => file.kind !== 'text').length} ファイルはテキスト分析対象外` }),
|
|
@@ -1057,8 +1305,9 @@ window.__ModuleLoader__.load({
|
|
|
1057
1305
|
!state.selected.length ? jsx('p', { role: 'status', children: review.snapshot.files.some(file => file.kind === 'text')
|
|
1058
1306
|
? '分析するテキストファイルを変更ファイル一覧で選択してください。' : '分析できるテキストファイルがありません。差分は引き続き確認できます。' }) : null,
|
|
1059
1307
|
jsxs('div', { className: 'kiokuko-review-analysis-controls', children: [
|
|
1060
|
-
|
|
1061
|
-
|
|
1308
|
+
jsx(NumberedPicker, { label: 'モデル', value: state.modelKey, disabled: state.busy || state.loading || state.cancelling,
|
|
1309
|
+
options: [{ value: '', label: '選択' }, ...state.models.map(item => ({ value: JSON.stringify([item.provider, item.model]), label: `${item.provider}/${item.model}` }))],
|
|
1310
|
+
onChoose: (value) => controller.change(sessionId, { modelKey: value }) }),
|
|
1062
1311
|
jsxs('label', { children: ['レビューの目的(任意)', jsx('textarea', { value: state.purpose, disabled: state.busy || state.loading || state.cancelling, maxLength: 4000, onChange: (event) => controller.change(sessionId, { purpose: event.target.value }) })] }),
|
|
1063
1312
|
jsx('button', { type: 'button', disabled: state.busy || state.loading || state.cancelling || review.state === 'analyzing' || !reviewMatchesSelection || !state.selected.length || !state.modelKey, onClick: () => void controller.analyze(sessionId), children: '分析する' }),
|
|
1064
1313
|
review.state === 'analyzing' ? jsx('button', { type: 'button', disabled: state.cancelling, onClick: () => void controller.cancel(sessionId, review.reviewId), children: state.cancelling ? '停止中' : '停止' }) : null,
|
|
@@ -1072,17 +1321,16 @@ window.__ModuleLoader__.load({
|
|
|
1072
1321
|
] }) : null,
|
|
1073
1322
|
jsx('button', { type: 'button', className: 'kiokuko-review-list-toggle', onClick: () => {
|
|
1074
1323
|
controller.change(sessionId, { fileListOpen: !state.fileListOpen });
|
|
1075
|
-
requestAnimationFrame(() =>
|
|
1324
|
+
requestAnimationFrame(() => {
|
|
1325
|
+
const pane = document.getElementById(`kiokuko-review-${review.reviewId}-${state.fileListOpen ? 'detail' : 'files'}`);
|
|
1326
|
+
(pane?.querySelector('[data-kiokuko-shortcuts]') ?? pane)?.focus();
|
|
1327
|
+
});
|
|
1076
1328
|
},
|
|
1077
1329
|
children: state.fileListOpen ? '差分に戻る' : '変更ファイル一覧' }),
|
|
1078
1330
|
jsxs('div', { className: 'kiokuko-review-main', 'data-list-open': String(state.fileListOpen), children: [
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
controller.change(sessionId, { activeFileId: file.fileId, fileListOpen: false });
|
|
1083
|
-
requestAnimationFrame(() => document.getElementById(`kiokuko-review-${review.reviewId}-detail`)?.focus());
|
|
1084
|
-
}, children: `${file.displayPath} (${file.layer}, ${file.kind})` }),
|
|
1085
|
-
] }, file.fileId))] }),
|
|
1331
|
+
jsx('nav', { id: `kiokuko-review-${review.reviewId}-files`, tabIndex: -1, 'aria-label': '変更ファイル', children: jsx(NumberedPicker, { label: '変更ファイル', inline: true, value: active?.fileId, checked: state.selected,
|
|
1332
|
+
options: review.snapshot.files.map(file => ({ value: file.fileId, label: `${file.displayPath} (${file.layer}, ${file.kind})`, disabled: file.kind !== 'text' })),
|
|
1333
|
+
onChoose: (fileId) => controller.change(sessionId, { activeFileId: fileId }), onToggle: selectFile }) }),
|
|
1086
1334
|
active ? jsxs('article', { id: `kiokuko-review-${review.reviewId}-detail`, tabIndex: -1, children: [jsx('h3', { children: active.displayPath }),
|
|
1087
1335
|
active.kind === 'text' && active.newDigest && review.snapshot.mode !== 'turn' ? jsx('button', { type: 'button',
|
|
1088
1336
|
onClick: () => void controller.openCurrentFile(sessionId, review.reviewId, active.fileId, openFile), children: '現在のファイルへ' }) : null,
|