@cueloop/client 0.1.0-alpha.70 → 0.1.0-alpha.72

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.
@@ -38,8 +38,9 @@ import {
38
38
  import { annotationPaletteFor, type AnnotationPalette } from "./annotation-palette";
39
39
  import { printableSequence, type MarkRange, type VisualLine } from "./mark-runs";
40
40
  import {
41
+ activeSlashToken,
42
+ isStandaloneSlashQuery,
41
43
  mergeSlashItems,
42
- resolveInlineSuggestion,
43
44
  slashFilter,
44
45
  slashItemsFrom,
45
46
  } from "./slash-palette";
@@ -233,6 +234,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
233
234
  };
234
235
  const [folded, setFolded] = useState<Set<string>>(new Set());
235
236
  const [composeText, setComposeText] = useState("");
237
+ const [caretOffset, setCaretOffset] = useState(0);
236
238
  const [slashIndex, setSlashIndex] = useState(0);
237
239
  const composerReady = useRef(false);
238
240
  const composeRef = useRef<ComposeState | null>(null);
@@ -304,19 +306,26 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
304
306
  composeRef.current = state;
305
307
  setCompose(state);
306
308
  setComposeText(state.seed);
309
+ setCaretOffset(state.seed.length);
307
310
  setSlashIndex(0);
308
311
  };
309
312
  const closeCompose = (): void => {
310
313
  composeRef.current = null;
311
314
  setCompose(null);
312
315
  setComposeText("");
316
+ setCaretOffset(0);
313
317
  };
314
318
  const skills = useContext(SlashSkillsContext);
315
319
  const paletteItems = mergeSlashItems(slashItemsFrom(quickActions), skills);
316
- // open only while typing the token: a leading "/" with no space yet
317
- const slashActive = compose !== null && /^\/\S*$/.test(composeText);
318
- const slashItems = slashActive ? slashFilter(paletteItems, composeText.slice(1).trim()) : [];
319
- const inlineSlash = resolveInlineSuggestion(slashActive, composeText, paletteItems);
320
+ // the "/word" under the caret, anywhere in the draft, so each new "/" reopens the palette
321
+ const slashToken = compose !== null ? activeSlashToken(composeText, caretOffset) : null;
322
+ const slashActive = slashToken !== null;
323
+ const slashItems = slashToken !== null ? slashFilter(paletteItems, slashToken.slice(1)) : [];
324
+
325
+ // a fresh token starts its selection at the top, so an earlier Down never leaks into it
326
+ useEffect(() => {
327
+ if (slashActive) setSlashIndex(0);
328
+ }, [slashActive]);
320
329
 
321
330
  const saveComment = (body: string): void => {
322
331
  // the body saves verbatim - typed newlines are the author's choice;
@@ -357,11 +366,13 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
357
366
  });
358
367
  };
359
368
 
360
- // clicking away from an open composer commits the draft (blur-save);
361
- // a half-typed slash query is never a comment, so it discards instead
369
+ // clicking away commits the draft (blur-save); only a standalone "/query" is a palette
370
+ // artifact, so prose that merely ends in a "/name" still saves
362
371
  const blurSaveCompose = (): void => {
363
372
  if (!composeRef.current) return;
364
- if (slashActive || composeText.trim().length === 0) return closeCompose();
373
+ if (isStandaloneSlashQuery(composeText) || composeText.trim().length === 0) {
374
+ return closeCompose();
375
+ }
365
376
  saveComment(composeText);
366
377
  };
367
378
 
@@ -431,7 +442,14 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
431
442
  return true;
432
443
  }
433
444
  if (key.name === "return" || key.name === "tab") {
434
- openCompose({ ...activeCompose, seed: `/${slashItems[selected]!.name} ` });
445
+ const token = activeSlashToken(composeText, caretOffset) ?? "";
446
+ const cut = caretOffset - token.length;
447
+ const insertion = `/${slashItems[selected]!.name} `;
448
+ const seed = composeText.slice(0, cut) + insertion + composeText.slice(caretOffset);
449
+
450
+ openCompose({ ...activeCompose, seed });
451
+ // land the caret just after the inserted reference, not at the end of a chained draft
452
+ setCaretOffset(cut + insertion.length);
435
453
 
436
454
  return true;
437
455
  }
@@ -439,17 +457,6 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
439
457
  return false;
440
458
  };
441
459
 
442
- /** Tab completes the trailing "/word" to the matched skill's full name, then a space to chain. */
443
- const handleInlineSlashKey = (key: KeyEvent, activeCompose: ComposeState): boolean => {
444
- if (inlineSlash === null || key.name !== "tab") return false;
445
- const cut = composeText.length - inlineSlash.token.length;
446
- const completed = `${composeText.slice(0, cut)}/${inlineSlash.suggestion.name} `;
447
-
448
- openCompose({ ...activeCompose, seed: completed });
449
-
450
- return true;
451
- };
452
-
453
460
  /** Pre-mount window: buffer printables, honor a fast cmd+enter or newline. */
454
461
  const handlePremountKey = (key: KeyEvent, activeCompose: ComposeState): void => {
455
462
  if (key.name === "return") {
@@ -458,6 +465,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
458
465
 
459
466
  composeRef.current = grown;
460
467
  setComposeText(grown.seed);
468
+ setCaretOffset(grown.seed.length);
461
469
 
462
470
  return setCompose(grown);
463
471
  }
@@ -468,6 +476,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
468
476
 
469
477
  composeRef.current = grown;
470
478
  setComposeText(grown.seed);
479
+ setCaretOffset(grown.seed.length);
471
480
  setCompose(grown);
472
481
  }
473
482
  };
@@ -491,7 +500,6 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
491
500
  return closeCompose();
492
501
  }
493
502
  if (handleSlashKey(key, activeCompose)) return;
494
- if (handleInlineSlashKey(key, activeCompose)) return;
495
503
  if (!composerReady.current) handlePremountKey(key, activeCompose);
496
504
  };
497
505
 
@@ -670,18 +678,19 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
670
678
  tokens={tokens}
671
679
  onSave={saveComment}
672
680
  onReady={() => (composerReady.current = true)}
673
- onInput={setComposeText}
681
+ onInput={(text, caret) => {
682
+ setComposeText(text);
683
+ setCaretOffset(caret);
684
+ }}
674
685
  />
675
686
  ) : null;
676
- // one node below the composer: the palette list for a leading "/", otherwise the
677
- // inline completion hint. Renders null when neither applies, so it always pushes.
687
+ // the palette list below the composer while the caret is on a "/word"; null otherwise, so it pushes
678
688
  const paletteNode = (
679
689
  <ComposerPalette
680
690
  key="composer-palette"
681
691
  slashActive={slashActive}
682
692
  slashItems={slashItems}
683
693
  slashIndex={slashIndex}
684
- inline={inlineSlash}
685
694
  tokens={tokens}
686
695
  />
687
696
  );
@@ -64,6 +64,8 @@ export function useChangesWorkbench(options?: ChangesWorkbenchOptions): ChangesW
64
64
  setChangesOpen(isDiff);
65
65
  setProjectMode(isDiff ? "changes" : "tree");
66
66
  rememberedChanges.current = isDiff;
67
+ // a non-diff thread has no Changes editor; leaving zoom on would strand the hidden Thread pane
68
+ if (!isDiff) setZoomed(false);
67
69
  setGrid(makeGroup([changesTab()]));
68
70
  setFocusedGroup(null);
69
71
  };