@xenide-io/the-old-ui-theme 0.9.8 → 0.9.10

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/suite.d.mts CHANGED
@@ -486,7 +486,7 @@ interface SuiteAiChatMessage {
486
486
  * Suite-wide Ask AI chat — Notion-style continuous thread.
487
487
  * Lives in the sidebar (or any parent that mounts it). Not Kraken Deep Research.
488
488
  */
489
- declare function SuiteAiPanel({ presets, emptyState, title, fetchChat, sendMessage, clearChat, brandIcon: BrandIcon, spinner: Spinner, open: openProp, onOpenChange, suiteAppBases, onSwitchApp, onSameAppNavigate, resolveAction, isWaitingForReply, }: {
489
+ declare function SuiteAiPanel({ presets, emptyState, title, fetchChat, sendMessage, editMessage, cancelMessage, clearChat, brandIcon: BrandIcon, spinner: Spinner, open: openProp, onOpenChange, suiteAppBases, onSwitchApp, onSameAppNavigate, resolveAction, isWaitingForReply, }: {
490
490
  presets?: SuiteAiPreset[];
491
491
  emptyState?: ReactNode;
492
492
  title?: string;
@@ -505,6 +505,19 @@ declare function SuiteAiPanel({ presets, emptyState, title, fetchChat, sendMessa
505
505
  messages: SuiteAiChatMessage[];
506
506
  reply: string;
507
507
  }>;
508
+ /** Rewrite the thread from a user message. Falls back to sendMessage. */
509
+ editMessage?: (params: {
510
+ index: number;
511
+ prompt: string;
512
+ signal?: AbortSignal;
513
+ }) => Promise<{
514
+ messages: SuiteAiChatMessage[];
515
+ reply: string;
516
+ }>;
517
+ /** Stop the in-flight reply on the server. */
518
+ cancelMessage?: () => Promise<{
519
+ messages?: SuiteAiChatMessage[];
520
+ }>;
508
521
  clearChat: () => Promise<void>;
509
522
  brandIcon?: ComponentType<{
510
523
  className?: string;
package/dist/suite.d.ts CHANGED
@@ -486,7 +486,7 @@ interface SuiteAiChatMessage {
486
486
  * Suite-wide Ask AI chat — Notion-style continuous thread.
487
487
  * Lives in the sidebar (or any parent that mounts it). Not Kraken Deep Research.
488
488
  */
489
- declare function SuiteAiPanel({ presets, emptyState, title, fetchChat, sendMessage, clearChat, brandIcon: BrandIcon, spinner: Spinner, open: openProp, onOpenChange, suiteAppBases, onSwitchApp, onSameAppNavigate, resolveAction, isWaitingForReply, }: {
489
+ declare function SuiteAiPanel({ presets, emptyState, title, fetchChat, sendMessage, editMessage, cancelMessage, clearChat, brandIcon: BrandIcon, spinner: Spinner, open: openProp, onOpenChange, suiteAppBases, onSwitchApp, onSameAppNavigate, resolveAction, isWaitingForReply, }: {
490
490
  presets?: SuiteAiPreset[];
491
491
  emptyState?: ReactNode;
492
492
  title?: string;
@@ -505,6 +505,19 @@ declare function SuiteAiPanel({ presets, emptyState, title, fetchChat, sendMessa
505
505
  messages: SuiteAiChatMessage[];
506
506
  reply: string;
507
507
  }>;
508
+ /** Rewrite the thread from a user message. Falls back to sendMessage. */
509
+ editMessage?: (params: {
510
+ index: number;
511
+ prompt: string;
512
+ signal?: AbortSignal;
513
+ }) => Promise<{
514
+ messages: SuiteAiChatMessage[];
515
+ reply: string;
516
+ }>;
517
+ /** Stop the in-flight reply on the server. */
518
+ cancelMessage?: () => Promise<{
519
+ messages?: SuiteAiChatMessage[];
520
+ }>;
508
521
  clearChat: () => Promise<void>;
509
522
  brandIcon?: ComponentType<{
510
523
  className?: string;
package/dist/suite.js CHANGED
@@ -3493,6 +3493,8 @@ function SuiteAiPanel({
3493
3493
  title = "Shelly AI",
3494
3494
  fetchChat,
3495
3495
  sendMessage,
3496
+ editMessage,
3497
+ cancelMessage,
3496
3498
  clearChat,
3497
3499
  brandIcon: BrandIcon,
3498
3500
  spinner: Spinner,
@@ -3526,6 +3528,9 @@ function SuiteAiPanel({
3526
3528
  const textareaRef = (0, import_react12.useRef)(null);
3527
3529
  const abortRef = (0, import_react12.useRef)(null);
3528
3530
  const pendingPromptRef = (0, import_react12.useRef)(null);
3531
+ const [editingIndex, setEditingIndex] = (0, import_react12.useState)(null);
3532
+ const [editDraft, setEditDraft] = (0, import_react12.useState)("");
3533
+ const [copiedIndex, setCopiedIndex] = (0, import_react12.useState)(null);
3529
3534
  const waiting = loading || isWaitingForReply;
3530
3535
  (0, import_react12.useEffect)(() => {
3531
3536
  function onOpen(event) {
@@ -3652,6 +3657,11 @@ function SuiteAiPanel({
3652
3657
  (_a = abortRef.current) == null ? void 0 : _a.abort();
3653
3658
  abortRef.current = null;
3654
3659
  setLoading(false);
3660
+ if (!cancelMessage) return;
3661
+ void cancelMessage().then((result) => {
3662
+ if (result == null ? void 0 : result.messages) setMessages(result.messages);
3663
+ }).catch(() => {
3664
+ });
3655
3665
  }
3656
3666
  function retry() {
3657
3667
  var _a;
@@ -3660,6 +3670,55 @@ function SuiteAiPanel({
3660
3670
  if (lastUser) void ask(lastUser);
3661
3671
  else void hydrate();
3662
3672
  }
3673
+ async function saveEdit(index) {
3674
+ var _a;
3675
+ const trimmed = editDraft.trim();
3676
+ if (!trimmed || waiting) return;
3677
+ setEditingIndex(null);
3678
+ setEditDraft("");
3679
+ setError("");
3680
+ setAtBottom(true);
3681
+ setMessages((prev) => prev.slice(0, index));
3682
+ const controller = new AbortController();
3683
+ abortRef.current = controller;
3684
+ setLoading(true);
3685
+ try {
3686
+ const result = editMessage ? await editMessage({
3687
+ index,
3688
+ prompt: trimmed,
3689
+ signal: controller.signal
3690
+ }) : await sendMessage({ prompt: trimmed, signal: controller.signal });
3691
+ if (controller.signal.aborted) return;
3692
+ const nextMessages = (_a = result.messages) != null ? _a : [];
3693
+ setMessages(nextMessages);
3694
+ notifySuiteMutated(
3695
+ nextMessages.flatMap((message) => {
3696
+ var _a2;
3697
+ return (_a2 = message.actions) != null ? _a2 : [];
3698
+ })
3699
+ );
3700
+ } catch (err) {
3701
+ if (controller.signal.aborted) return;
3702
+ setError(
3703
+ err instanceof Error ? err.message : "The AI request failed. Try again."
3704
+ );
3705
+ void hydrate();
3706
+ } finally {
3707
+ if (abortRef.current === controller) abortRef.current = null;
3708
+ setLoading(false);
3709
+ }
3710
+ }
3711
+ async function handleCopy(index, content) {
3712
+ try {
3713
+ await navigator.clipboard.writeText(content);
3714
+ setCopiedIndex(index);
3715
+ window.setTimeout(
3716
+ () => setCopiedIndex((current) => current === index ? null : current),
3717
+ 1500
3718
+ );
3719
+ } catch (e) {
3720
+ }
3721
+ }
3663
3722
  async function handleClear() {
3664
3723
  if (loading) return;
3665
3724
  try {
@@ -3792,12 +3851,99 @@ function SuiteAiPanel({
3792
3851
  var _a;
3793
3852
  const isUser = message.role === "user";
3794
3853
  if (isUser) {
3854
+ if (editingIndex === index) {
3855
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3856
+ "div",
3857
+ {
3858
+ className: "suite-msg-in suite-msg-in--user flex justify-end",
3859
+ "data-test": "ask-ai-user",
3860
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "w-full max-w-[85%] space-y-2", children: [
3861
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3862
+ "textarea",
3863
+ {
3864
+ autoFocus: true,
3865
+ value: editDraft,
3866
+ onChange: (event) => setEditDraft(event.target.value),
3867
+ onKeyDown: (event) => {
3868
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
3869
+ event.preventDefault();
3870
+ void saveEdit(index);
3871
+ }
3872
+ if (event.key === "Escape") {
3873
+ setEditingIndex(null);
3874
+ setEditDraft("");
3875
+ }
3876
+ },
3877
+ rows: 2,
3878
+ "data-test": "ask-ai-edit-input",
3879
+ className: "w-full resize-none rounded-2xl rounded-br-md border border-ph-border bg-ph-surface px-3 py-2 text-sm leading-relaxed text-ph-ink outline-none focus-visible:border-ph-brand/45 focus-visible:ring-2 focus-visible:ring-ph-focus/35"
3880
+ }
3881
+ ),
3882
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex justify-end gap-2", children: [
3883
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3884
+ "button",
3885
+ {
3886
+ type: "button",
3887
+ onClick: () => {
3888
+ setEditingIndex(null);
3889
+ setEditDraft("");
3890
+ },
3891
+ className: "inline-flex min-h-8 items-center rounded-lg border border-ph-border bg-ph-surface px-2.5 text-xs font-medium text-ph-ink transition-colors hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus",
3892
+ children: "Cancel"
3893
+ }
3894
+ ),
3895
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3896
+ "button",
3897
+ {
3898
+ type: "button",
3899
+ "data-test": "ask-ai-edit-save",
3900
+ disabled: !editDraft.trim() || waiting,
3901
+ onClick: () => void saveEdit(index),
3902
+ className: "inline-flex min-h-8 items-center rounded-lg bg-ph-brand px-2.5 text-xs font-medium text-[color:var(--ph-on-accent)] transition-opacity hover:bg-ph-brand-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus disabled:opacity-40",
3903
+ children: "Save"
3904
+ }
3905
+ )
3906
+ ] })
3907
+ ] })
3908
+ },
3909
+ `user-${index}`
3910
+ );
3911
+ }
3795
3912
  return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3796
3913
  "div",
3797
3914
  {
3798
3915
  className: "suite-msg-in suite-msg-in--user flex justify-end",
3799
3916
  "data-test": "ask-ai-user",
3800
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-ph-brand px-3 py-2 text-sm leading-relaxed text-[var(--ph-on-accent)] [overflow-wrap:anywhere]", children: message.content })
3917
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "group flex items-end gap-1.5", children: [
3918
+ !waiting ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "mb-0.5 flex items-center gap-0.5 opacity-100 transition-opacity [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-focus-within:opacity-100 [@media(hover:hover)]:group-hover:opacity-100", children: [
3919
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3920
+ "button",
3921
+ {
3922
+ type: "button",
3923
+ onClick: () => void handleCopy(index, message.content),
3924
+ "aria-label": copiedIndex === index ? "Copied" : "Copy message",
3925
+ "data-test": "ask-ai-copy",
3926
+ className: "flex h-7 w-7 items-center justify-center rounded-lg text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus",
3927
+ children: copiedIndex === index ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_iconoir_react8.Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_iconoir_react8.Copy, { className: "h-3.5 w-3.5" })
3928
+ }
3929
+ ),
3930
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3931
+ "button",
3932
+ {
3933
+ type: "button",
3934
+ onClick: () => {
3935
+ setEditDraft(message.content);
3936
+ setEditingIndex(index);
3937
+ },
3938
+ "aria-label": "Edit message",
3939
+ "data-test": "ask-ai-edit",
3940
+ className: "flex h-7 w-7 items-center justify-center rounded-lg text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus",
3941
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_iconoir_react8.EditPencil, { className: "h-3.5 w-3.5" })
3942
+ }
3943
+ )
3944
+ ] }) : null,
3945
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-ph-brand px-3 py-2 text-sm leading-relaxed text-[var(--ph-on-accent)] [overflow-wrap:anywhere]", children: message.content })
3946
+ ] })
3801
3947
  },
3802
3948
  `user-${index}`
3803
3949
  );
@@ -3961,7 +4107,7 @@ function SuiteAiPanel({
3961
4107
  className: "max-h-36 min-h-10 flex-1 resize-none bg-transparent px-1.5 py-2 text-sm leading-relaxed text-ph-ink outline-none placeholder:text-ph-mutedtext"
3962
4108
  }
3963
4109
  ),
3964
- loading ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Tooltip, { content: "Stop generating", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
4110
+ waiting ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Tooltip, { content: "Stop generating", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3965
4111
  "button",
3966
4112
  {
3967
4113
  type: "button",
@@ -3971,16 +4117,7 @@ function SuiteAiPanel({
3971
4117
  "aria-label": "Stop generating",
3972
4118
  children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_iconoir_react8.Square, { className: "h-3.5 w-3.5 fill-current" })
3973
4119
  }
3974
- ) }) : isWaitingForReply ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3975
- "span",
3976
- {
3977
- className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-ph-muted text-ph-subtle",
3978
- "data-test": "ask-ai-waiting",
3979
- role: "status",
3980
- "aria-label": "Shelly is responding",
3981
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Spinner, { className: "h-4 w-4 animate-spin" })
3982
- }
3983
- ) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Tooltip, { content: "Send message", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
4120
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Tooltip, { content: "Send message", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3984
4121
  "button",
3985
4122
  {
3986
4123
  type: "button",
package/dist/suite.mjs CHANGED
@@ -2390,6 +2390,9 @@ import {
2390
2390
  import {
2391
2391
  ArrowDown,
2392
2392
  ArrowUp,
2393
+ Check,
2394
+ Copy,
2395
+ EditPencil,
2393
2396
  Erase,
2394
2397
  Sparks,
2395
2398
  Square,
@@ -2470,6 +2473,8 @@ function SuiteAiPanel({
2470
2473
  title = "Shelly AI",
2471
2474
  fetchChat,
2472
2475
  sendMessage,
2476
+ editMessage,
2477
+ cancelMessage,
2473
2478
  clearChat,
2474
2479
  brandIcon: BrandIcon,
2475
2480
  spinner: Spinner,
@@ -2503,6 +2508,9 @@ function SuiteAiPanel({
2503
2508
  const textareaRef = useRef3(null);
2504
2509
  const abortRef = useRef3(null);
2505
2510
  const pendingPromptRef = useRef3(null);
2511
+ const [editingIndex, setEditingIndex] = useState8(null);
2512
+ const [editDraft, setEditDraft] = useState8("");
2513
+ const [copiedIndex, setCopiedIndex] = useState8(null);
2506
2514
  const waiting = loading || isWaitingForReply;
2507
2515
  useEffect7(() => {
2508
2516
  function onOpen(event) {
@@ -2629,6 +2637,11 @@ function SuiteAiPanel({
2629
2637
  (_a = abortRef.current) == null ? void 0 : _a.abort();
2630
2638
  abortRef.current = null;
2631
2639
  setLoading(false);
2640
+ if (!cancelMessage) return;
2641
+ void cancelMessage().then((result) => {
2642
+ if (result == null ? void 0 : result.messages) setMessages(result.messages);
2643
+ }).catch(() => {
2644
+ });
2632
2645
  }
2633
2646
  function retry() {
2634
2647
  var _a;
@@ -2637,6 +2650,55 @@ function SuiteAiPanel({
2637
2650
  if (lastUser) void ask(lastUser);
2638
2651
  else void hydrate();
2639
2652
  }
2653
+ async function saveEdit(index) {
2654
+ var _a;
2655
+ const trimmed = editDraft.trim();
2656
+ if (!trimmed || waiting) return;
2657
+ setEditingIndex(null);
2658
+ setEditDraft("");
2659
+ setError("");
2660
+ setAtBottom(true);
2661
+ setMessages((prev) => prev.slice(0, index));
2662
+ const controller = new AbortController();
2663
+ abortRef.current = controller;
2664
+ setLoading(true);
2665
+ try {
2666
+ const result = editMessage ? await editMessage({
2667
+ index,
2668
+ prompt: trimmed,
2669
+ signal: controller.signal
2670
+ }) : await sendMessage({ prompt: trimmed, signal: controller.signal });
2671
+ if (controller.signal.aborted) return;
2672
+ const nextMessages = (_a = result.messages) != null ? _a : [];
2673
+ setMessages(nextMessages);
2674
+ notifySuiteMutated(
2675
+ nextMessages.flatMap((message) => {
2676
+ var _a2;
2677
+ return (_a2 = message.actions) != null ? _a2 : [];
2678
+ })
2679
+ );
2680
+ } catch (err) {
2681
+ if (controller.signal.aborted) return;
2682
+ setError(
2683
+ err instanceof Error ? err.message : "The AI request failed. Try again."
2684
+ );
2685
+ void hydrate();
2686
+ } finally {
2687
+ if (abortRef.current === controller) abortRef.current = null;
2688
+ setLoading(false);
2689
+ }
2690
+ }
2691
+ async function handleCopy(index, content) {
2692
+ try {
2693
+ await navigator.clipboard.writeText(content);
2694
+ setCopiedIndex(index);
2695
+ window.setTimeout(
2696
+ () => setCopiedIndex((current) => current === index ? null : current),
2697
+ 1500
2698
+ );
2699
+ } catch (e) {
2700
+ }
2701
+ }
2640
2702
  async function handleClear() {
2641
2703
  if (loading) return;
2642
2704
  try {
@@ -2769,12 +2831,99 @@ function SuiteAiPanel({
2769
2831
  var _a;
2770
2832
  const isUser = message.role === "user";
2771
2833
  if (isUser) {
2834
+ if (editingIndex === index) {
2835
+ return /* @__PURE__ */ jsx17(
2836
+ "div",
2837
+ {
2838
+ className: "suite-msg-in suite-msg-in--user flex justify-end",
2839
+ "data-test": "ask-ai-user",
2840
+ children: /* @__PURE__ */ jsxs14("div", { className: "w-full max-w-[85%] space-y-2", children: [
2841
+ /* @__PURE__ */ jsx17(
2842
+ "textarea",
2843
+ {
2844
+ autoFocus: true,
2845
+ value: editDraft,
2846
+ onChange: (event) => setEditDraft(event.target.value),
2847
+ onKeyDown: (event) => {
2848
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
2849
+ event.preventDefault();
2850
+ void saveEdit(index);
2851
+ }
2852
+ if (event.key === "Escape") {
2853
+ setEditingIndex(null);
2854
+ setEditDraft("");
2855
+ }
2856
+ },
2857
+ rows: 2,
2858
+ "data-test": "ask-ai-edit-input",
2859
+ className: "w-full resize-none rounded-2xl rounded-br-md border border-ph-border bg-ph-surface px-3 py-2 text-sm leading-relaxed text-ph-ink outline-none focus-visible:border-ph-brand/45 focus-visible:ring-2 focus-visible:ring-ph-focus/35"
2860
+ }
2861
+ ),
2862
+ /* @__PURE__ */ jsxs14("div", { className: "flex justify-end gap-2", children: [
2863
+ /* @__PURE__ */ jsx17(
2864
+ "button",
2865
+ {
2866
+ type: "button",
2867
+ onClick: () => {
2868
+ setEditingIndex(null);
2869
+ setEditDraft("");
2870
+ },
2871
+ className: "inline-flex min-h-8 items-center rounded-lg border border-ph-border bg-ph-surface px-2.5 text-xs font-medium text-ph-ink transition-colors hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus",
2872
+ children: "Cancel"
2873
+ }
2874
+ ),
2875
+ /* @__PURE__ */ jsx17(
2876
+ "button",
2877
+ {
2878
+ type: "button",
2879
+ "data-test": "ask-ai-edit-save",
2880
+ disabled: !editDraft.trim() || waiting,
2881
+ onClick: () => void saveEdit(index),
2882
+ className: "inline-flex min-h-8 items-center rounded-lg bg-ph-brand px-2.5 text-xs font-medium text-[color:var(--ph-on-accent)] transition-opacity hover:bg-ph-brand-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus disabled:opacity-40",
2883
+ children: "Save"
2884
+ }
2885
+ )
2886
+ ] })
2887
+ ] })
2888
+ },
2889
+ `user-${index}`
2890
+ );
2891
+ }
2772
2892
  return /* @__PURE__ */ jsx17(
2773
2893
  "div",
2774
2894
  {
2775
2895
  className: "suite-msg-in suite-msg-in--user flex justify-end",
2776
2896
  "data-test": "ask-ai-user",
2777
- children: /* @__PURE__ */ jsx17("div", { className: "w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-ph-brand px-3 py-2 text-sm leading-relaxed text-[var(--ph-on-accent)] [overflow-wrap:anywhere]", children: message.content })
2897
+ children: /* @__PURE__ */ jsxs14("div", { className: "group flex items-end gap-1.5", children: [
2898
+ !waiting ? /* @__PURE__ */ jsxs14("div", { className: "mb-0.5 flex items-center gap-0.5 opacity-100 transition-opacity [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-focus-within:opacity-100 [@media(hover:hover)]:group-hover:opacity-100", children: [
2899
+ /* @__PURE__ */ jsx17(
2900
+ "button",
2901
+ {
2902
+ type: "button",
2903
+ onClick: () => void handleCopy(index, message.content),
2904
+ "aria-label": copiedIndex === index ? "Copied" : "Copy message",
2905
+ "data-test": "ask-ai-copy",
2906
+ className: "flex h-7 w-7 items-center justify-center rounded-lg text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus",
2907
+ children: copiedIndex === index ? /* @__PURE__ */ jsx17(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx17(Copy, { className: "h-3.5 w-3.5" })
2908
+ }
2909
+ ),
2910
+ /* @__PURE__ */ jsx17(
2911
+ "button",
2912
+ {
2913
+ type: "button",
2914
+ onClick: () => {
2915
+ setEditDraft(message.content);
2916
+ setEditingIndex(index);
2917
+ },
2918
+ "aria-label": "Edit message",
2919
+ "data-test": "ask-ai-edit",
2920
+ className: "flex h-7 w-7 items-center justify-center rounded-lg text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus",
2921
+ children: /* @__PURE__ */ jsx17(EditPencil, { className: "h-3.5 w-3.5" })
2922
+ }
2923
+ )
2924
+ ] }) : null,
2925
+ /* @__PURE__ */ jsx17("div", { className: "w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-ph-brand px-3 py-2 text-sm leading-relaxed text-[var(--ph-on-accent)] [overflow-wrap:anywhere]", children: message.content })
2926
+ ] })
2778
2927
  },
2779
2928
  `user-${index}`
2780
2929
  );
@@ -2938,7 +3087,7 @@ function SuiteAiPanel({
2938
3087
  className: "max-h-36 min-h-10 flex-1 resize-none bg-transparent px-1.5 py-2 text-sm leading-relaxed text-ph-ink outline-none placeholder:text-ph-mutedtext"
2939
3088
  }
2940
3089
  ),
2941
- loading ? /* @__PURE__ */ jsx17(Tooltip, { content: "Stop generating", children: /* @__PURE__ */ jsx17(
3090
+ waiting ? /* @__PURE__ */ jsx17(Tooltip, { content: "Stop generating", children: /* @__PURE__ */ jsx17(
2942
3091
  "button",
2943
3092
  {
2944
3093
  type: "button",
@@ -2948,16 +3097,7 @@ function SuiteAiPanel({
2948
3097
  "aria-label": "Stop generating",
2949
3098
  children: /* @__PURE__ */ jsx17(Square, { className: "h-3.5 w-3.5 fill-current" })
2950
3099
  }
2951
- ) }) : isWaitingForReply ? /* @__PURE__ */ jsx17(
2952
- "span",
2953
- {
2954
- className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-ph-muted text-ph-subtle",
2955
- "data-test": "ask-ai-waiting",
2956
- role: "status",
2957
- "aria-label": "Shelly is responding",
2958
- children: /* @__PURE__ */ jsx17(Spinner, { className: "h-4 w-4 animate-spin" })
2959
- }
2960
- ) : /* @__PURE__ */ jsx17(Tooltip, { content: "Send message", children: /* @__PURE__ */ jsx17(
3100
+ ) }) : /* @__PURE__ */ jsx17(Tooltip, { content: "Send message", children: /* @__PURE__ */ jsx17(
2961
3101
  "button",
2962
3102
  {
2963
3103
  type: "button",
@@ -2977,7 +3117,7 @@ function SuiteAiPanel({
2977
3117
 
2978
3118
  // src/suite/components/workspace-switcher.tsx
2979
3119
  import { useState as useState9 } from "react";
2980
- import { Building, Check, NavArrowDown } from "iconoir-react";
3120
+ import { Building, Check as Check2, NavArrowDown } from "iconoir-react";
2981
3121
  import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
2982
3122
  function SuiteWorkspaceSwitcher({
2983
3123
  workspaces = [],
@@ -3062,7 +3202,7 @@ function SuiteWorkspaceSwitcher({
3062
3202
  workspace.description ? /* @__PURE__ */ jsx18("span", { className: "block truncate text-xs text-ph-mutedtext", children: workspace.description }) : null
3063
3203
  ] }),
3064
3204
  workspace.id === currentId ? /* @__PURE__ */ jsx18(
3065
- Check,
3205
+ Check2,
3066
3206
  {
3067
3207
  className: "h-4 w-4 shrink-0 text-ph-brand",
3068
3208
  "aria-hidden": "true"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenide-io/the-old-ui-theme",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "description": "Props-driven React components and theme tokens inspired by classic interface design — optimized for Next.js apps and internal tools.",
5
5
  "author": "Xenide",
6
6
  "license": "MIT",
@@ -16,6 +16,9 @@ import {
16
16
  import {
17
17
  ArrowDown,
18
18
  ArrowUp,
19
+ Check,
20
+ Copy,
21
+ EditPencil,
19
22
  Erase,
20
23
  Sparks,
21
24
  Square,
@@ -157,6 +160,8 @@ export function SuiteAiPanel({
157
160
  title = "Shelly AI",
158
161
  fetchChat,
159
162
  sendMessage,
163
+ editMessage,
164
+ cancelMessage,
160
165
  clearChat,
161
166
  brandIcon: BrandIcon,
162
167
  spinner: Spinner,
@@ -180,6 +185,14 @@ export function SuiteAiPanel({
180
185
  prompt: string;
181
186
  signal?: AbortSignal;
182
187
  }) => Promise<{ messages: SuiteAiChatMessage[]; reply: string }>;
188
+ /** Rewrite the thread from a user message. Falls back to sendMessage. */
189
+ editMessage?: (params: {
190
+ index: number;
191
+ prompt: string;
192
+ signal?: AbortSignal;
193
+ }) => Promise<{ messages: SuiteAiChatMessage[]; reply: string }>;
194
+ /** Stop the in-flight reply on the server. */
195
+ cancelMessage?: () => Promise<{ messages?: SuiteAiChatMessage[] }>;
183
196
  clearChat: () => Promise<void>;
184
197
  brandIcon?: ComponentType<{ className?: string }>;
185
198
  spinner: ComponentType<{ className?: string }>;
@@ -220,6 +233,9 @@ export function SuiteAiPanel({
220
233
  const textareaRef = useRef<HTMLTextAreaElement>(null);
221
234
  const abortRef = useRef<AbortController | null>(null);
222
235
  const pendingPromptRef = useRef<string | null>(null);
236
+ const [editingIndex, setEditingIndex] = useState<number | null>(null);
237
+ const [editDraft, setEditDraft] = useState("");
238
+ const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
223
239
  const waiting = loading || isWaitingForReply;
224
240
 
225
241
  useEffect(() => {
@@ -356,6 +372,14 @@ export function SuiteAiPanel({
356
372
  abortRef.current?.abort();
357
373
  abortRef.current = null;
358
374
  setLoading(false);
375
+ if (!cancelMessage) return;
376
+ void cancelMessage()
377
+ .then((result) => {
378
+ if (result?.messages) setMessages(result.messages);
379
+ })
380
+ .catch(() => {
381
+ // Best-effort server cancel; the local abort already stopped the UI.
382
+ });
359
383
  }
360
384
 
361
385
  function retry() {
@@ -367,6 +391,58 @@ export function SuiteAiPanel({
367
391
  else void hydrate();
368
392
  }
369
393
 
394
+ async function saveEdit(index: number) {
395
+ const trimmed = editDraft.trim();
396
+ if (!trimmed || waiting) return;
397
+ setEditingIndex(null);
398
+ setEditDraft("");
399
+ setError("");
400
+ setAtBottom(true);
401
+ setMessages((prev) => prev.slice(0, index));
402
+ const controller = new AbortController();
403
+ abortRef.current = controller;
404
+ setLoading(true);
405
+ try {
406
+ const result = editMessage
407
+ ? await editMessage({
408
+ index,
409
+ prompt: trimmed,
410
+ signal: controller.signal,
411
+ })
412
+ : await sendMessage({ prompt: trimmed, signal: controller.signal });
413
+ if (controller.signal.aborted) return;
414
+ const nextMessages = result.messages ?? [];
415
+ setMessages(nextMessages);
416
+ notifySuiteMutated(
417
+ nextMessages.flatMap((message) => message.actions ?? []),
418
+ );
419
+ } catch (err) {
420
+ if (controller.signal.aborted) return;
421
+ setError(
422
+ err instanceof Error
423
+ ? err.message
424
+ : "The AI request failed. Try again.",
425
+ );
426
+ void hydrate();
427
+ } finally {
428
+ if (abortRef.current === controller) abortRef.current = null;
429
+ setLoading(false);
430
+ }
431
+ }
432
+
433
+ async function handleCopy(index: number, content: string) {
434
+ try {
435
+ await navigator.clipboard.writeText(content);
436
+ setCopiedIndex(index);
437
+ window.setTimeout(
438
+ () => setCopiedIndex((current) => (current === index ? null : current)),
439
+ 1500,
440
+ );
441
+ } catch {
442
+ // clipboard unavailable
443
+ }
444
+ }
445
+
370
446
  async function handleClear() {
371
447
  if (loading) return;
372
448
  try {
@@ -516,14 +592,102 @@ export function SuiteAiPanel({
516
592
  {messages.map((message, index) => {
517
593
  const isUser = message.role === "user";
518
594
  if (isUser) {
595
+ if (editingIndex === index) {
596
+ return (
597
+ <div
598
+ key={`user-${index}`}
599
+ className="suite-msg-in suite-msg-in--user flex justify-end"
600
+ data-test="ask-ai-user"
601
+ >
602
+ <div className="w-full max-w-[85%] space-y-2">
603
+ <textarea
604
+ autoFocus
605
+ value={editDraft}
606
+ onChange={(event) => setEditDraft(event.target.value)}
607
+ onKeyDown={(event) => {
608
+ if (
609
+ event.key === "Enter" &&
610
+ !event.shiftKey &&
611
+ !event.nativeEvent.isComposing
612
+ ) {
613
+ event.preventDefault();
614
+ void saveEdit(index);
615
+ }
616
+ if (event.key === "Escape") {
617
+ setEditingIndex(null);
618
+ setEditDraft("");
619
+ }
620
+ }}
621
+ rows={2}
622
+ data-test="ask-ai-edit-input"
623
+ className="w-full resize-none rounded-2xl rounded-br-md border border-ph-border bg-ph-surface px-3 py-2 text-sm leading-relaxed text-ph-ink outline-none focus-visible:border-ph-brand/45 focus-visible:ring-2 focus-visible:ring-ph-focus/35"
624
+ />
625
+ <div className="flex justify-end gap-2">
626
+ <button
627
+ type="button"
628
+ onClick={() => {
629
+ setEditingIndex(null);
630
+ setEditDraft("");
631
+ }}
632
+ className="inline-flex min-h-8 items-center rounded-lg border border-ph-border bg-ph-surface px-2.5 text-xs font-medium text-ph-ink transition-colors hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus"
633
+ >
634
+ Cancel
635
+ </button>
636
+ <button
637
+ type="button"
638
+ data-test="ask-ai-edit-save"
639
+ disabled={!editDraft.trim() || waiting}
640
+ onClick={() => void saveEdit(index)}
641
+ className="inline-flex min-h-8 items-center rounded-lg bg-ph-brand px-2.5 text-xs font-medium text-[color:var(--ph-on-accent)] transition-opacity hover:bg-ph-brand-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus disabled:opacity-40"
642
+ >
643
+ Save
644
+ </button>
645
+ </div>
646
+ </div>
647
+ </div>
648
+ );
649
+ }
519
650
  return (
520
651
  <div
521
652
  key={`user-${index}`}
522
653
  className="suite-msg-in suite-msg-in--user flex justify-end"
523
654
  data-test="ask-ai-user"
524
655
  >
525
- <div className="w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-ph-brand px-3 py-2 text-sm leading-relaxed text-[var(--ph-on-accent)] [overflow-wrap:anywhere]">
526
- {message.content}
656
+ <div className="group flex items-end gap-1.5">
657
+ {!waiting ? (
658
+ <div className="mb-0.5 flex items-center gap-0.5 opacity-100 transition-opacity [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-focus-within:opacity-100 [@media(hover:hover)]:group-hover:opacity-100">
659
+ <button
660
+ type="button"
661
+ onClick={() => void handleCopy(index, message.content)}
662
+ aria-label={
663
+ copiedIndex === index ? "Copied" : "Copy message"
664
+ }
665
+ data-test="ask-ai-copy"
666
+ className="flex h-7 w-7 items-center justify-center rounded-lg text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus"
667
+ >
668
+ {copiedIndex === index ? (
669
+ <Check className="h-3.5 w-3.5" />
670
+ ) : (
671
+ <Copy className="h-3.5 w-3.5" />
672
+ )}
673
+ </button>
674
+ <button
675
+ type="button"
676
+ onClick={() => {
677
+ setEditDraft(message.content);
678
+ setEditingIndex(index);
679
+ }}
680
+ aria-label="Edit message"
681
+ data-test="ask-ai-edit"
682
+ className="flex h-7 w-7 items-center justify-center rounded-lg text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus"
683
+ >
684
+ <EditPencil className="h-3.5 w-3.5" />
685
+ </button>
686
+ </div>
687
+ ) : null}
688
+ <div className="w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-ph-brand px-3 py-2 text-sm leading-relaxed text-[var(--ph-on-accent)] [overflow-wrap:anywhere]">
689
+ {message.content}
690
+ </div>
527
691
  </div>
528
692
  </div>
529
693
  );
@@ -701,7 +865,7 @@ export function SuiteAiPanel({
701
865
  data-test="ask-ai-input"
702
866
  className="max-h-36 min-h-10 flex-1 resize-none bg-transparent px-1.5 py-2 text-sm leading-relaxed text-ph-ink outline-none placeholder:text-ph-mutedtext"
703
867
  />
704
- {loading ? (
868
+ {waiting ? (
705
869
  <Tooltip content="Stop generating">
706
870
  <button
707
871
  type="button"
@@ -713,15 +877,6 @@ export function SuiteAiPanel({
713
877
  <Square className="h-3.5 w-3.5 fill-current" />
714
878
  </button>
715
879
  </Tooltip>
716
- ) : isWaitingForReply ? (
717
- <span
718
- className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-ph-muted text-ph-subtle"
719
- data-test="ask-ai-waiting"
720
- role="status"
721
- aria-label="Shelly is responding"
722
- >
723
- <Spinner className="h-4 w-4 animate-spin" />
724
- </span>
725
880
  ) : (
726
881
  <Tooltip content="Send message">
727
882
  <button