sync: update from private repo (6fcb0d0)

This commit is contained in:
oss-sync
2026-06-04 03:03:12 +00:00
parent 21be01b699
commit 57685d995c
36 changed files with 2467 additions and 391 deletions
@@ -5,6 +5,7 @@ import { CreateLocalTaskInput, fetchMyOrgs, Visibility, listBrowserSessionProfil
import { AttachmentDropzone } from './AttachmentDropzone';
import { ScheduleFields } from './ScheduleFields';
import { usePieceList } from '../../hooks/usePieces';
import { resolvePieceOptions } from '../../lib/splitPieces';
import { useAuthState } from '../../App';
interface CreateTaskDialogProps {
@@ -80,7 +81,8 @@ export function CreateTaskDialog({ onClose, onSubmit, initialPiece, initialBody,
scheduledAt: '',
});
const selectedPiece = (pieces ?? []).find(p => p.name === form.piece);
const resolvedPieces = resolvePieceOptions(pieces ?? []);
const selectedPiece = resolvedPieces.find(p => p.name === form.piece);
const missingMcp = selectedPiece?.requiredMcp
? selectedPiece.requiredMcp.filter(
(id) => !(connections ?? []).find((c) => c.serverId === id && c.connected),
@@ -241,7 +243,7 @@ export function CreateTaskDialog({ onClose, onSubmit, initialPiece, initialBody,
className="w-full px-2.5 py-1.5 border border-slate-200 rounded-lg text-xs outline-none focus:border-accent"
>
<option value="auto"></option>
{(pieces ?? []).map(p => (
{resolvedPieces.map(p => (
<option key={p.name} value={p.name}>{p.name}</option>
))}
</select>
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { continueTaskWithPiece, fetchLocalTaskComments } from '../../api';
import { usePieceList } from '../../hooks/usePieces';
import { resolvePieceOptions } from '../../lib/splitPieces';
import { MarkdownText } from '../../lib/markdown-text';
interface PrevJobInfo {
@@ -123,7 +124,7 @@ export function ContinueWithPieceDialog({
disabled={piecesQuery.isLoading}
className="px-3 py-2 rounded-md border border-hairline text-[13px] focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent"
>
{(piecesQuery.data ?? []).map(p => (
{resolvePieceOptions(piecesQuery.data ?? []).map(p => (
<option key={p.name} value={p.name}>
{p.name}
{p.name === prevJob.pieceName ? ' (現在)' : ''}
+1 -1
View File
@@ -18,7 +18,7 @@ interface TopBarProps {
export const NAV_ITEMS: Array<{ id: PageId; label: string; adminOnly: boolean; requiresAuth: boolean }> = [
{ id: 'tasks', label: 'タスク', adminOnly: false, requiresAuth: false },
{ id: 'schedules', label: 'スケジュール', adminOnly: false, requiresAuth: false },
{ id: 'pieces', label: 'Pieces', adminOnly: true, requiresAuth: false },
{ id: 'pieces', label: 'Pieces', adminOnly: false, requiresAuth: false },
{ id: 'captcha', label: 'CAPTCHA', adminOnly: true, requiresAuth: false },
{ id: 'settings', label: '設定', adminOnly: false, requiresAuth: false },
{ id: 'users', label: 'ユーザー', adminOnly: true, requiresAuth: true },
@@ -7,9 +7,10 @@ export interface MovementAccordionProps {
onAdd: () => void;
onRemove: (index: number) => void;
onMove: (index: number, direction: 'up' | 'down') => void;
disabled?: boolean;
}
export function MovementAccordion({ movements, onChange, onAdd, onRemove, onMove }: MovementAccordionProps) {
export function MovementAccordion({ movements, onChange, onAdd, onRemove, onMove, disabled = false }: MovementAccordionProps) {
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
const movementNames = movements.map((m) => m.name ?? '');
@@ -49,40 +50,42 @@ export function MovementAccordion({ movements, onChange, onAdd, onRemove, onMove
{/* Spacer */}
<div className="flex-1" />
{/* Controls */}
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<button
type="button"
onClick={() => onMove(i, 'up')}
disabled={i === 0}
className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-sm px-1"
title="Move up"
>
&#9650;
</button>
<button
type="button"
onClick={() => onMove(i, 'down')}
disabled={i === movements.length - 1}
className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-sm px-1"
title="Move down"
>
&#9660;
</button>
<button
type="button"
onClick={() => {
if (confirm(`Movement "${movement.name}" を削除しますか?`)) {
onRemove(i);
if (expandedIndex === i) setExpandedIndex(null);
}
}}
className="text-slate-400 hover:text-red-500 text-sm px-1"
title="Delete"
>
&times;
</button>
</div>
{/* Controls — hidden in read-only mode */}
{!disabled && (
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<button
type="button"
onClick={() => onMove(i, 'up')}
disabled={i === 0}
className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-sm px-1"
title="Move up"
>
&#9650;
</button>
<button
type="button"
onClick={() => onMove(i, 'down')}
disabled={i === movements.length - 1}
className="text-slate-400 hover:text-slate-600 disabled:opacity-30 text-sm px-1"
title="Move down"
>
&#9660;
</button>
<button
type="button"
onClick={() => {
if (confirm(`Movement "${movement.name}" を削除しますか?`)) {
onRemove(i);
if (expandedIndex === i) setExpandedIndex(null);
}
}}
className="text-slate-400 hover:text-red-500 text-sm px-1"
title="Delete"
>
&times;
</button>
</div>
)}
</div>
{/* Expanded form */}
@@ -92,6 +95,7 @@ export function MovementAccordion({ movements, onChange, onAdd, onRemove, onMove
movement={movement}
movementNames={movementNames}
onChange={(field, value) => onChange(i, field, value)}
disabled={disabled}
/>
</div>
)}
@@ -100,13 +104,15 @@ export function MovementAccordion({ movements, onChange, onAdd, onRemove, onMove
})}
</div>
<button
type="button"
onClick={onAdd}
className="mt-3 text-sm text-blue-600 hover:text-blue-700"
>
+ Add Movement
</button>
{!disabled && (
<button
type="button"
onClick={onAdd}
className="mt-3 text-sm text-blue-600 hover:text-blue-700"
>
+ Add Movement
</button>
)}
</div>
);
}
+15 -6
View File
@@ -8,10 +8,12 @@ export interface MovementFormProps {
movement: any;
movementNames: string[];
onChange: (field: string, value: any) => void;
disabled?: boolean;
}
export function MovementForm({ movement, movementNames, onChange }: MovementFormProps) {
export function MovementForm({ movement, movementNames, onChange, disabled = false }: MovementFormProps) {
const nextOptions = [...movementNames.filter((n) => n !== movement.name), ...SPECIAL_TARGETS];
const disabledClass = disabled ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : '';
return (
<div className="space-y-4">
@@ -22,7 +24,8 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
type="text"
value={movement.name ?? ''}
onChange={(e) => onChange('name', e.target.value)}
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
disabled={disabled}
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${disabledClass}`}
/>
</div>
@@ -33,7 +36,8 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
type="text"
value={movement.persona ?? ''}
onChange={(e) => onChange('persona', e.target.value)}
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
disabled={disabled}
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${disabledClass}`}
/>
</div>
@@ -43,7 +47,8 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
<select
value={movement.default_next ?? 'COMPLETE'}
onChange={(e) => onChange('default_next', e.target.value)}
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white"
disabled={disabled}
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white ${disabledClass}`}
>
{nextOptions.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
@@ -58,7 +63,8 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
id={`edit-${movement.name}`}
checked={movement.edit ?? false}
onChange={(e) => onChange('edit', e.target.checked)}
className="rounded border-slate-300"
disabled={disabled}
className="rounded border-slate-300 disabled:cursor-not-allowed"
/>
<label htmlFor={`edit-${movement.name}`} className="text-xs font-medium text-slate-600">edit</label>
<HelpText> Write / Edit LLM </HelpText>
@@ -71,7 +77,8 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
value={movement.instruction ?? ''}
onChange={(e) => onChange('instruction', e.target.value)}
rows={6}
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none font-mono"
disabled={disabled}
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none font-mono ${disabledClass}`}
/>
<HelpText>LLM Markdown 使</HelpText>
</div>
@@ -80,6 +87,7 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
<ToolTagInput
value={movement.allowed_tools ?? []}
onChange={(tools) => onChange('allowed_tools', tools)}
disabled={disabled}
/>
{/* rules */}
@@ -87,6 +95,7 @@ export function MovementForm({ movement, movementNames, onChange }: MovementForm
rules={movement.rules ?? []}
movementNames={movementNames.filter((n) => n !== movement.name)}
onChange={(rules) => onChange('rules', rules)}
disabled={disabled}
/>
</div>
);
+66 -34
View File
@@ -9,10 +9,22 @@ import { MovementAccordion } from './MovementAccordion';
export interface PieceEditorProps {
name: string;
/** Whether the current user has admin role. Controls edit access for built-in/global-custom pieces. */
isAdmin?: boolean;
/**
* Source hint from URL / list selection — used for the targeted GET request.
* The read-only gate uses the SOURCE the API actually resolved (authoritative),
* so a deep link without pieceSource still renders correctly for non-admins.
*/
source?: 'builtin' | 'user-custom' | 'global-custom';
}
export function PieceEditor({ name }: PieceEditorProps) {
const { data: piece, isLoading, error } = usePiece(name);
export function PieceEditor({ name, isAdmin = true, source }: PieceEditorProps) {
const { data: fetchResult, isLoading, error } = usePiece(name, source);
// Use the server-resolved source as the authoritative value; fall back to the
// prop only while the fetch hasn't completed yet (avoids flicker on known paths).
const piece = fetchResult?.piece ?? null;
const effectiveSource = fetchResult?.source ?? source;
const queryClient = useQueryClient();
const { setUrlState } = useUrlState();
@@ -33,7 +45,7 @@ export function PieceEditor({ name }: PieceEditorProps) {
setEditMode('visual');
setYamlError(null);
}
}, [piece]);
}, [piece]); // piece is derived from fetchResult above
const showToast = (msg: string, duration = 2000) => {
setToast(msg);
@@ -159,7 +171,7 @@ export function PieceEditor({ name }: PieceEditorProps) {
setSaving(true);
try {
await updatePiece(name, saveData);
await updatePiece(name, saveData, effectiveSource);
await queryClient.invalidateQueries({ queryKey: ['piece', name] });
await queryClient.invalidateQueries({ queryKey: ['pieces'] });
setIsDirty(false);
@@ -177,7 +189,7 @@ export function PieceEditor({ name }: PieceEditorProps) {
const handleDelete = async () => {
if (!confirm(`Piece "${name}" を削除しますか?この操作は取り消せません。`)) return;
try {
await deletePiece(name);
await deletePiece(name, effectiveSource);
await queryClient.invalidateQueries({ queryKey: ['pieces'] });
setUrlState((prev) => ({ ...prev, piece: undefined, section: 'provider' as any }));
} catch (e: any) {
@@ -189,6 +201,12 @@ export function PieceEditor({ name }: PieceEditorProps) {
if (error) return <div className="text-sm text-red-500">Piece </div>;
if (!draft) return null;
// Non-admins cannot edit built-in or global-custom pieces — read-only view.
// user-custom stays editable for its owner. Admins can edit all sources.
// Use the SERVER-RESOLVED source (effectiveSource) as the authoritative value so
// that deep links without a ?pieceSource param still render read-only for non-admins.
const readonly = !isAdmin && (effectiveSource === 'builtin' || effectiveSource === 'global-custom');
const movementNames = (draft.movements ?? []).map((m: any) => m.name ?? '');
return (
@@ -201,13 +219,21 @@ export function PieceEditor({ name }: PieceEditorProps) {
<p className="text-sm text-slate-500 mt-0.5 line-clamp-2">{String(draft.description).split('\n')[0]}</p>
)}
</div>
<button
type="button"
onClick={handleDelete}
className="px-3 py-1.5 text-xs text-red-600 hover:bg-red-50 rounded-lg border border-red-200"
>
Delete
</button>
{/* Delete is hidden for ALL built-in pieces (non-deletable by everyone, including admins). */}
{!readonly && effectiveSource !== 'builtin' && (
<button
type="button"
onClick={handleDelete}
className="px-3 py-1.5 text-xs text-red-600 hover:bg-red-50 rounded-lg border border-red-200"
>
Delete
</button>
)}
{readonly && (
<span className="px-2 py-1 text-xs text-slate-400 bg-slate-100 rounded border border-slate-200">
</span>
)}
</div>
{/* Mode toggle */}
@@ -242,6 +268,7 @@ export function PieceEditor({ name }: PieceEditorProps) {
piece={draft}
onChange={handleMetaChange}
movementNames={movementNames}
disabled={readonly}
/>
</div>
@@ -253,6 +280,7 @@ export function PieceEditor({ name }: PieceEditorProps) {
onAdd={handleAddMovement}
onRemove={handleRemoveMovement}
onMove={handleMoveMovement}
disabled={readonly}
/>
</div>
</>
@@ -268,7 +296,9 @@ export function PieceEditor({ name }: PieceEditorProps) {
value={yamlText}
onChange={(e) => handleYamlChange(e.target.value)}
spellCheck={false}
className="w-full px-4 py-3 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none font-mono bg-slate-50 leading-relaxed resize-y"
readOnly={readonly}
disabled={readonly}
className={`w-full px-4 py-3 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none font-mono leading-relaxed resize-y ${readonly ? 'bg-slate-100 text-slate-500 cursor-not-allowed' : 'bg-slate-50'}`}
style={{ minHeight: '500px', tabSize: 2 }}
/>
<p className="text-xs text-slate-400 mt-1">
@@ -278,27 +308,29 @@ export function PieceEditor({ name }: PieceEditorProps) {
)}
{/* Footer */}
<div className="flex items-center justify-end gap-3 pt-4 mt-6 border-t border-slate-200">
{toast && (
<span className={`text-xs mr-auto ${toast.startsWith('エラー') ? 'text-red-500' : 'text-green-600'}`}>
{toast}
</span>
)}
<button
onClick={handleDiscard}
disabled={!isDirty}
className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg disabled:opacity-50"
>
Discard Changes
</button>
<button
onClick={handleSave}
disabled={!isDirty || saving}
className="px-4 py-2 text-sm bg-accent text-accent-fg rounded-lg hover:bg-accent-deep disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
{!readonly && (
<div className="flex items-center justify-end gap-3 pt-4 mt-6 border-t border-slate-200">
{toast && (
<span className={`text-xs mr-auto ${toast.startsWith('エラー') ? 'text-red-500' : 'text-green-600'}`}>
{toast}
</span>
)}
<button
onClick={handleDiscard}
disabled={!isDirty}
className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg disabled:opacity-50"
>
Discard Changes
</button>
<button
onClick={handleSave}
disabled={!isDirty || saving}
className="px-4 py-2 text-sm bg-accent text-accent-fg rounded-lg hover:bg-accent-deep disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
)}
</div>
);
}
+11 -5
View File
@@ -4,10 +4,12 @@ export interface PieceMetaFormProps {
piece: any;
onChange: (field: string, value: any) => void;
movementNames: string[];
disabled?: boolean;
}
export function PieceMetaForm({ piece, onChange, movementNames }: PieceMetaFormProps) {
export function PieceMetaForm({ piece, onChange, movementNames, disabled = false }: PieceMetaFormProps) {
const triggersText = (piece.triggers?.keywords ?? []).join(', ');
const disabledClass = disabled ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : '';
return (
<div className="space-y-4">
@@ -30,7 +32,8 @@ export function PieceMetaForm({ piece, onChange, movementNames }: PieceMetaFormP
type="text"
value={piece.description ?? ''}
onChange={(e) => onChange('description', e.target.value)}
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
disabled={disabled}
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${disabledClass}`}
/>
</div>
@@ -42,7 +45,8 @@ export function PieceMetaForm({ piece, onChange, movementNames }: PieceMetaFormP
value={piece.max_movements ?? 10}
onChange={(e) => onChange('max_movements', parseInt(e.target.value, 10) || 0)}
min={1}
className="w-32 px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
disabled={disabled}
className={`w-32 px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${disabledClass}`}
/>
<HelpText>1 movement </HelpText>
</div>
@@ -53,7 +57,8 @@ export function PieceMetaForm({ piece, onChange, movementNames }: PieceMetaFormP
<select
value={piece.initial_movement ?? ''}
onChange={(e) => onChange('initial_movement', e.target.value)}
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white"
disabled={disabled}
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white ${disabledClass}`}
>
{movementNames.length === 0 && <option value="">--</option>}
{movementNames.map((name) => (
@@ -77,7 +82,8 @@ export function PieceMetaForm({ piece, onChange, movementNames }: PieceMetaFormP
onChange('triggers', { ...piece.triggers, keywords });
}}
placeholder="keyword1, keyword2, ..."
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
disabled={disabled}
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${disabledClass}`}
/>
<HelpText> piece </HelpText>
</div>
+28 -20
View File
@@ -6,10 +6,12 @@ export interface RulesTableProps {
rules: Array<{ condition: string; next: string }>;
movementNames: string[];
onChange: (rules: Array<{ condition: string; next: string }>) => void;
disabled?: boolean;
}
export function RulesTable({ rules, movementNames, onChange }: RulesTableProps) {
export function RulesTable({ rules, movementNames, onChange, disabled = false }: RulesTableProps) {
const nextOptions = [...movementNames, ...SPECIAL_TARGETS];
const disabledClass = disabled ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : '';
const updateRule = (index: number, field: 'condition' | 'next', value: string) => {
const updated = rules.map((r, i) => (i === index ? { ...r, [field]: value } : r));
@@ -33,7 +35,7 @@ export function RulesTable({ rules, movementNames, onChange }: RulesTableProps)
<tr className="text-xs text-slate-500">
<th className="text-left font-medium pb-1 pr-2">condition</th>
<th className="text-left font-medium pb-1 pr-2 w-44">next</th>
<th className="w-8" />
{!disabled && <th className="w-8" />}
</tr>
</thead>
<tbody>
@@ -44,7 +46,8 @@ export function RulesTable({ rules, movementNames, onChange }: RulesTableProps)
type="text"
value={rule.condition}
onChange={(e) => updateRule(i, 'condition', e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
disabled={disabled}
className={`w-full px-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${disabledClass}`}
placeholder="条件..."
/>
</td>
@@ -52,34 +55,39 @@ export function RulesTable({ rules, movementNames, onChange }: RulesTableProps)
<select
value={rule.next}
onChange={(e) => updateRule(i, 'next', e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white"
disabled={disabled}
className={`w-full px-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white ${disabledClass}`}
>
{nextOptions.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</td>
<td className="pb-1">
<button
type="button"
onClick={() => removeRule(i)}
className="text-slate-400 hover:text-red-500 text-sm px-1"
>
&times;
</button>
</td>
{!disabled && (
<td className="pb-1">
<button
type="button"
onClick={() => removeRule(i)}
className="text-slate-400 hover:text-red-500 text-sm px-1"
>
&times;
</button>
</td>
)}
</tr>
))}
</tbody>
</table>
)}
<button
type="button"
onClick={addRule}
className="text-xs text-blue-600 hover:text-blue-700"
>
+ Add Rule
</button>
{!disabled && (
<button
type="button"
onClick={addRule}
className="text-xs text-blue-600 hover:text-blue-700"
>
+ Add Rule
</button>
)}
<HelpText>LLM transition </HelpText>
</div>
);
+31 -23
View File
@@ -6,6 +6,7 @@ import { HelpText } from './HelpText';
export interface ToolTagInputProps {
value: string[];
onChange: (tools: string[]) => void;
disabled?: boolean;
}
/**
@@ -24,7 +25,7 @@ export interface ToolTagInputProps {
* - Selecting an unavailable catalog tool is still allowed (the user might be
* preparing for a server that's about to come back online).
*/
export function ToolTagInput({ value, onChange }: ToolTagInputProps) {
export function ToolTagInput({ value, onChange, disabled = false }: ToolTagInputProps) {
const { data: catalog } = useToolList();
const [input, setInput] = useState('');
const [showDropdown, setShowDropdown] = useState(false);
@@ -127,24 +128,27 @@ export function ToolTagInput({ value, onChange }: ToolTagInputProps) {
name={tool}
entry={entry}
onRemove={() => removeTool(tool)}
readOnly={disabled}
/>
);
})}
<input
ref={inputRef}
type="text"
value={input}
onChange={(e) => {
setInput(e.target.value);
setShowDropdown(true);
}}
onFocus={() => setShowDropdown(true)}
onKeyDown={handleKeyDown}
placeholder={value.length === 0 ? 'ツール名を入力...' : ''}
className="flex-1 min-w-[120px] text-sm outline-none bg-transparent"
/>
{!disabled && (
<input
ref={inputRef}
type="text"
value={input}
onChange={(e) => {
setInput(e.target.value);
setShowDropdown(true);
}}
onFocus={() => setShowDropdown(true)}
onKeyDown={handleKeyDown}
placeholder={value.length === 0 ? 'ツール名を入力...' : ''}
className="flex-1 min-w-[120px] text-sm outline-none bg-transparent"
/>
)}
</div>
{showDropdown && groupedSuggestions.length > 0 && (
{!disabled && showDropdown && groupedSuggestions.length > 0 && (
<div className="absolute z-10 mt-1 w-full max-h-72 overflow-y-auto bg-white border border-slate-200 rounded-lg shadow-lg">
{groupedSuggestions.map((g) => (
<div key={g.key}>
@@ -202,10 +206,12 @@ function SelectedToolChip({
name,
entry,
onRemove,
readOnly = false,
}: {
name: string;
entry: ToolCatalogEntry | undefined;
onRemove: () => void;
readOnly?: boolean;
}) {
const isUnknown = !entry;
const isUnavailable = entry ? !entry.available : false;
@@ -231,14 +237,16 @@ function SelectedToolChip({
{entry && <ScopeBadge scope={entry.scope} dim />}
{isUnknown && <Badge color="amber">unknown</Badge>}
{!isUnknown && isUnavailable && <Badge color="amber">{entry?.reason ?? 'offline'}</Badge>}
<button
type="button"
onClick={onRemove}
className="text-current opacity-60 hover:opacity-100"
aria-label={`remove ${name}`}
>
&times;
</button>
{!readOnly && (
<button
type="button"
onClick={onRemove}
className="text-current opacity-60 hover:opacity-100"
aria-label={`remove ${name}`}
>
&times;
</button>
)}
</span>
);
}