@@ -13,6 +13,31 @@ import type DiscourseGraphPlugin from "~/index";
1313import { QueryEngine } from "~/services/QueryEngine" ;
1414import { isProvisionalSchema } from "~/utils/typeUtils" ;
1515import { getNodeTypeIdForFile } from "~/utils/relationsStore" ;
16+ import { formatNodeName } from "~/utils/createNode" ;
17+
18+ // APFS and ext4 both enforce a 255 UTF-8 byte limit per filename component.
19+ const MAX_FILENAME_BYTES = 255 ;
20+ const MD_EXTENSION_BYTES = 3 ; // ".md"
21+
22+ const getByteLength = ( str : string ) : number =>
23+ new TextEncoder ( ) . encode ( str ) . byteLength ;
24+
25+ // Remove characters from the end until the string fits within maxBytes.
26+ const trimToByteLimit = ( str : string , maxBytes : number ) : string => {
27+ if ( getByteLength ( str ) <= maxBytes ) return str ;
28+ let trimmed = str ;
29+ while ( trimmed . length > 0 && getByteLength ( trimmed ) > maxBytes ) {
30+ trimmed = trimmed . slice ( 0 , - 1 ) ;
31+ }
32+ return trimmed ;
33+ } ;
34+
35+ const computeMaxTitleBytes = ( nodeType : DiscourseNode | null ) : number => {
36+ const formatOverhead = nodeType
37+ ? getByteLength ( formatNodeName ( "" , nodeType ) ?? "" )
38+ : 0 ;
39+ return Math . max ( 1 , MAX_FILENAME_BYTES - MD_EXTENSION_BYTES - formatOverhead ) ;
40+ } ;
1641
1742type ModifyNodeFormProps = {
1843 nodeTypes : DiscourseNode [ ] ;
@@ -59,12 +84,17 @@ export const ModifyNodeForm = ({
5984 string | undefined
6085 > ( undefined ) ;
6186 const queryEngine = useRef ( new QueryEngine ( plugin . app ) ) ;
62- const titleInputRef = useRef < HTMLInputElement > ( null ) ;
87+ const titleInputRef = useRef < HTMLTextAreaElement > ( null ) ;
6388 const popoverRef = useRef < HTMLDivElement > ( null ) ;
6489 const menuRef = useRef < HTMLUListElement > ( null ) ;
6590 const debounceTimeoutRef = useRef < number | null > ( null ) ;
6691 const selectedFileRef = useRef < TFile | null > ( null ) ;
6792
93+ const maxTitleBytes = useMemo (
94+ ( ) => computeMaxTitleBytes ( selectedNodeType ) ,
95+ [ selectedNodeType ] ,
96+ ) ;
97+
6898 // Search for nodes when query changes (only in create mode)
6999 useEffect ( ( ) => {
70100 if ( isEditMode ) {
@@ -131,7 +161,7 @@ export const ModifyNodeForm = ({
131161 popover . style . left = `${ inputRect . left } px` ;
132162 popover . style . width = `${ inputRect . width } px` ;
133163 }
134- } , [ isOpen ] ) ;
164+ } , [ isOpen , query ] ) ;
135165
136166 useEffect ( ( ) => {
137167 if ( menuRef . current && isOpen && activeIndex >= 0 ) {
@@ -152,6 +182,13 @@ export const ModifyNodeForm = ({
152182 titleInputRef . current ?. focus ( ) ;
153183 } , [ ] ) ;
154184
185+ useEffect ( ( ) => {
186+ const el = titleInputRef . current ;
187+ if ( ! el ) return ;
188+ el . style . height = "auto" ;
189+ el . style . height = `${ el . scrollHeight } px` ;
190+ } , [ query ] ) ;
191+
155192 // Determine available relationships based on current file and selected node type
156193 const availableRelationships = useMemo ( ( ) => {
157194 if ( ! currentFile || ! selectedNodeType || isEditMode ) {
@@ -256,7 +293,7 @@ export const ModifyNodeForm = ({
256293 } , 50 ) ;
257294 } , [ ] ) ;
258295
259- const handleKeyDown = ( e : React . KeyboardEvent < HTMLInputElement > ) => {
296+ const handleKeyDown = ( e : React . KeyboardEvent < HTMLTextAreaElement > ) => {
260297 if ( selectedExistingNode ) {
261298 // If locked, only handle Escape
262299 if ( e . key === "Escape" ) {
@@ -292,16 +329,26 @@ export const ModifyNodeForm = ({
292329 const newSelectedType =
293330 nodeTypes . find ( ( nt ) => nt . id === selectedId ) || null ;
294331 setSelectedNodeType ( newSelectedType ) ;
332+
295333 if ( selectedExistingNode ) {
334+ selectedFileRef . current = null ;
296335 setSelectedExistingNode ( null ) ;
297336 setQuery ( "" ) ;
298337 setTitle ( "" ) ;
338+ } else {
339+ const newMaxBytes = computeMaxTitleBytes ( newSelectedType ) ;
340+ if ( getByteLength ( query ) > newMaxBytes ) {
341+ const trimmed = trimToByteLimit ( query , newMaxBytes ) ;
342+ setQuery ( trimmed ) ;
343+ setTitle ( trimmed ) ;
344+ }
299345 }
346+
300347 setSelectedRelationshipKey ( undefined ) ;
301348 } ;
302349
303- const handleQueryChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
304- const newQuery = e . target . value ;
350+ const handleQueryChange = ( e : React . ChangeEvent < HTMLTextAreaElement > ) => {
351+ const newQuery = trimToByteLimit ( e . target . value , maxTitleBytes ) ;
305352 setQuery ( newQuery ) ;
306353 setTitle ( newQuery ) ;
307354 if ( selectedExistingNode ) {
@@ -377,12 +424,12 @@ export const ModifyNodeForm = ({
377424 { selectedExistingNode ? (
378425 // Locked state: show selected node with clear button
379426 < div className = "relative flex w-full items-start" >
380- < input
381- type = "text"
427+ < textarea
382428 value = { selectedExistingNode . basename }
383429 readOnly
384430 disabled = { isSubmitting }
385- className = "resize-vertical font-inherit border-background-modifier-border bg-background-secondary text-text-normal max-h-[6em] min-h-[2.5em] w-full cursor-default overflow-y-auto rounded-md border p-2 pr-8"
431+ rows = { 1 }
432+ className = "font-inherit border-background-modifier-border bg-background-secondary text-text-normal min-h-[2.5em] w-full cursor-default resize-none overflow-y-auto rounded-md border p-2 pr-8"
386433 />
387434 < button
388435 onClick = { handleClearSelection }
@@ -397,9 +444,8 @@ export const ModifyNodeForm = ({
397444 ) : (
398445 // Search input with popover (only in create mode)
399446 < div className = "relative w-full" >
400- < input
447+ < textarea
401448 ref = { titleInputRef }
402- type = "text"
403449 placeholder = {
404450 isEditMode
405451 ? "Enter new content"
@@ -419,9 +465,15 @@ export const ModifyNodeForm = ({
419465 setTimeout ( ( ) => setIsFocused ( false ) , 200 ) ;
420466 } }
421467 disabled = { isSubmitting }
422- className = "resize-vertical font-inherit border-background-modifier-border bg-background-primary text-text-normal max-h-[6em] min-h-[2.5em] w-full overflow-y-auto rounded-md border p-2"
468+ rows = { 1 }
469+ className = "font-inherit border-background-modifier-border bg-background-primary text-text-normal min-h-[2.5em] w-full resize-none overflow-hidden rounded-md border p-2"
423470 autoComplete = "off"
424471 />
472+ { getByteLength ( query ) >= maxTitleBytes && (
473+ < p className = "text-error mt-1 text-xs" >
474+ Character limit reached
475+ </ p >
476+ ) }
425477 { isOpen && ! isEditMode && (
426478 < div
427479 ref = { popoverRef }
0 commit comments