@stigmer/react 0.2.3 → 0.3.0

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.
Files changed (118) hide show
  1. package/composer/ComposerToolbar.d.ts +5 -1
  2. package/composer/ComposerToolbar.d.ts.map +1 -1
  3. package/composer/ComposerToolbar.js +6 -3
  4. package/composer/ComposerToolbar.js.map +1 -1
  5. package/composer/SessionComposer.d.ts +17 -1
  6. package/composer/SessionComposer.d.ts.map +1 -1
  7. package/composer/SessionComposer.js +10 -3
  8. package/composer/SessionComposer.js.map +1 -1
  9. package/execution/MessageEntry.d.ts +3 -1
  10. package/execution/MessageEntry.d.ts.map +1 -1
  11. package/execution/MessageEntry.js +30 -1
  12. package/execution/MessageEntry.js.map +1 -1
  13. package/index.d.ts +3 -3
  14. package/index.d.ts.map +1 -1
  15. package/index.js +2 -2
  16. package/index.js.map +1 -1
  17. package/models/HarnessSelector.d.ts +41 -0
  18. package/models/HarnessSelector.d.ts.map +1 -0
  19. package/models/HarnessSelector.js +74 -0
  20. package/models/HarnessSelector.js.map +1 -0
  21. package/models/ModelSelector.d.ts +26 -16
  22. package/models/ModelSelector.d.ts.map +1 -1
  23. package/models/ModelSelector.js +128 -48
  24. package/models/ModelSelector.js.map +1 -1
  25. package/models/__tests__/HarnessSelector.test.d.ts +2 -0
  26. package/models/__tests__/HarnessSelector.test.d.ts.map +1 -0
  27. package/models/__tests__/HarnessSelector.test.js +160 -0
  28. package/models/__tests__/HarnessSelector.test.js.map +1 -0
  29. package/models/__tests__/harness.test.d.ts +2 -0
  30. package/models/__tests__/harness.test.d.ts.map +1 -0
  31. package/models/__tests__/harness.test.js +50 -0
  32. package/models/__tests__/harness.test.js.map +1 -0
  33. package/models/__tests__/useModelRegistry.test.d.ts +2 -0
  34. package/models/__tests__/useModelRegistry.test.d.ts.map +1 -0
  35. package/models/__tests__/useModelRegistry.test.js +148 -0
  36. package/models/__tests__/useModelRegistry.test.js.map +1 -0
  37. package/models/harness.d.ts +21 -0
  38. package/models/harness.d.ts.map +1 -0
  39. package/models/harness.js +34 -0
  40. package/models/harness.js.map +1 -0
  41. package/models/index.d.ts +7 -2
  42. package/models/index.d.ts.map +1 -1
  43. package/models/index.js +3 -1
  44. package/models/index.js.map +1 -1
  45. package/models/registry.d.ts +53 -13
  46. package/models/registry.d.ts.map +1 -1
  47. package/models/registry.js +51 -40
  48. package/models/registry.js.map +1 -1
  49. package/models/useModelRegistry.d.ts +39 -19
  50. package/models/useModelRegistry.d.ts.map +1 -1
  51. package/models/useModelRegistry.js +45 -23
  52. package/models/useModelRegistry.js.map +1 -1
  53. package/package.json +4 -4
  54. package/runner/RunnerListPanel.js +2 -1
  55. package/runner/RunnerListPanel.js.map +1 -1
  56. package/runner/__tests__/phase.test.js +6 -2
  57. package/runner/__tests__/phase.test.js.map +1 -1
  58. package/runner/phase.d.ts +9 -7
  59. package/runner/phase.d.ts.map +1 -1
  60. package/runner/phase.js +18 -12
  61. package/runner/phase.js.map +1 -1
  62. package/session/__tests__/useCreateSession.test.d.ts +2 -0
  63. package/session/__tests__/useCreateSession.test.d.ts.map +1 -0
  64. package/session/__tests__/useCreateSession.test.js +232 -0
  65. package/session/__tests__/useCreateSession.test.js.map +1 -0
  66. package/session/__tests__/useNewSessionFlow.test.d.ts +2 -0
  67. package/session/__tests__/useNewSessionFlow.test.d.ts.map +1 -0
  68. package/session/__tests__/useNewSessionFlow.test.js +199 -0
  69. package/session/__tests__/useNewSessionFlow.test.js.map +1 -0
  70. package/session/__tests__/useSessionConversation.test.js +37 -0
  71. package/session/__tests__/useSessionConversation.test.js.map +1 -1
  72. package/session/index.d.ts +1 -1
  73. package/session/index.d.ts.map +1 -1
  74. package/session/useCreateSession.d.ts +8 -0
  75. package/session/useCreateSession.d.ts.map +1 -1
  76. package/session/useCreateSession.js +2 -0
  77. package/session/useCreateSession.js.map +1 -1
  78. package/session/useNewSessionFlow.d.ts +6 -1
  79. package/session/useNewSessionFlow.d.ts.map +1 -1
  80. package/session/useNewSessionFlow.js +34 -8
  81. package/session/useNewSessionFlow.js.map +1 -1
  82. package/session/usePersistedModel.d.ts +16 -1
  83. package/session/usePersistedModel.d.ts.map +1 -1
  84. package/session/usePersistedModel.js +15 -6
  85. package/session/usePersistedModel.js.map +1 -1
  86. package/session/useSessionConversation.d.ts.map +1 -1
  87. package/session/useSessionConversation.js +6 -1
  88. package/session/useSessionConversation.js.map +1 -1
  89. package/session/useSessionPageFlow.d.ts +11 -0
  90. package/session/useSessionPageFlow.d.ts.map +1 -1
  91. package/session/useSessionPageFlow.js +11 -2
  92. package/session/useSessionPageFlow.js.map +1 -1
  93. package/src/composer/ComposerToolbar.tsx +24 -1
  94. package/src/composer/SessionComposer.tsx +35 -1
  95. package/src/execution/MessageEntry.tsx +134 -1
  96. package/src/index.ts +15 -1
  97. package/src/models/HarnessSelector.tsx +130 -0
  98. package/src/models/ModelSelector.tsx +285 -81
  99. package/src/models/__tests__/HarnessSelector.test.tsx +190 -0
  100. package/src/models/__tests__/harness.test.ts +66 -0
  101. package/src/models/__tests__/useModelRegistry.test.tsx +209 -0
  102. package/src/models/harness.ts +45 -0
  103. package/src/models/index.ts +7 -2
  104. package/src/models/registry.ts +122 -50
  105. package/src/models/useModelRegistry.ts +74 -24
  106. package/src/runner/RunnerListPanel.tsx +13 -5
  107. package/src/runner/__tests__/phase.test.ts +6 -2
  108. package/src/runner/phase.ts +18 -12
  109. package/src/session/__tests__/useCreateSession.test.tsx +296 -0
  110. package/src/session/__tests__/useNewSessionFlow.test.tsx +258 -0
  111. package/src/session/__tests__/useSessionConversation.test.tsx +53 -0
  112. package/src/session/index.ts +1 -1
  113. package/src/session/useCreateSession.ts +9 -0
  114. package/src/session/useNewSessionFlow.ts +46 -9
  115. package/src/session/usePersistedModel.ts +30 -6
  116. package/src/session/useSessionConversation.ts +6 -1
  117. package/src/session/useSessionPageFlow.ts +26 -2
  118. package/styles.css +1 -1
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useState } from "react";
3
4
  import Markdown from "react-markdown";
4
5
  import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
5
6
  import { MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
@@ -20,9 +21,11 @@ export interface MessageEntryProps {
20
21
  * - `MESSAGE_HUMAN` — plain text with muted background
21
22
  * - `MESSAGE_AI` — markdown-rendered via `react-markdown` + `remark-gfm`,
22
23
  * with a blinking cursor while streaming
24
+ * - `MESSAGE_THINKING` — collapsible thinking block with subdued styling,
25
+ * collapsed by default showing a brief summary
23
26
  * - `MESSAGE_SYSTEM` — small muted text
24
27
  * - `MESSAGE_TOOL` / `UNSPECIFIED` — renders nothing (tool results are
25
- * consumed by {@link ToolCallGroup} in SP4)
28
+ * consumed by {@link ToolCallGroup})
26
29
  *
27
30
  * Purely presentational — no data fetching, no state.
28
31
  * All visual properties flow through `--stgm-*` tokens.
@@ -44,6 +47,14 @@ export function MessageEntry({ message, className }: MessageEntryProps) {
44
47
  className={className}
45
48
  />
46
49
  );
50
+ case MessageType.MESSAGE_THINKING:
51
+ return (
52
+ <ThinkingMessage
53
+ content={message.content}
54
+ isStreaming={message.isStreaming}
55
+ className={className}
56
+ />
57
+ );
47
58
  case MessageType.MESSAGE_SYSTEM:
48
59
  return <SystemMessage content={message.content} className={className} />;
49
60
  default:
@@ -103,6 +114,69 @@ function AiMessage({
103
114
  );
104
115
  }
105
116
 
117
+ const THINKING_PREVIEW_LENGTH = 80;
118
+
119
+ function ThinkingMessage({
120
+ content,
121
+ isStreaming,
122
+ className,
123
+ }: {
124
+ content: string;
125
+ isStreaming: boolean;
126
+ className?: string;
127
+ }) {
128
+ const [expanded, setExpanded] = useState(false);
129
+ const hasContent = content.trim().length > 0;
130
+
131
+ if (!hasContent && !isStreaming) return null;
132
+
133
+ const preview = content.length > THINKING_PREVIEW_LENGTH
134
+ ? content.slice(0, THINKING_PREVIEW_LENGTH).trimEnd() + "..."
135
+ : content;
136
+
137
+ return (
138
+ <div
139
+ role="article"
140
+ aria-label="Model thinking"
141
+ className={cn("px-4 py-1.5", className)}
142
+ >
143
+ <button
144
+ type="button"
145
+ aria-expanded={expanded}
146
+ onClick={() => setExpanded((v) => !v)}
147
+ className={cn(
148
+ "flex items-center gap-1.5 text-xs text-muted-foreground transition-colors",
149
+ "hover:text-foreground cursor-pointer",
150
+ )}
151
+ >
152
+ <ThinkingIcon isStreaming={isStreaming} />
153
+ <span className="min-w-0 truncate">
154
+ {isStreaming && !hasContent
155
+ ? "Thinking..."
156
+ : expanded
157
+ ? "Thinking"
158
+ : preview}
159
+ </span>
160
+ {hasContent && <ChevronIcon expanded={expanded} />}
161
+ </button>
162
+
163
+ {expanded && hasContent && (
164
+ <div className="mt-1.5 border-l-2 border-muted-foreground/20 pl-3">
165
+ <p className="text-xs text-muted-foreground whitespace-pre-wrap leading-relaxed">
166
+ {content}
167
+ {isStreaming && (
168
+ <span
169
+ className="inline-block w-[2px] h-[0.8em] bg-muted-foreground align-text-bottom animate-pulse ml-0.5"
170
+ aria-hidden="true"
171
+ />
172
+ )}
173
+ </p>
174
+ </div>
175
+ )}
176
+ </div>
177
+ );
178
+ }
179
+
106
180
  function SystemMessage({
107
181
  content,
108
182
  className,
@@ -121,3 +195,62 @@ function SystemMessage({
121
195
  );
122
196
  }
123
197
 
198
+ function ThinkingIcon({ isStreaming }: { isStreaming: boolean }) {
199
+ if (isStreaming) {
200
+ return (
201
+ <svg
202
+ width="12"
203
+ height="12"
204
+ viewBox="0 0 12 12"
205
+ fill="none"
206
+ stroke="currentColor"
207
+ strokeWidth="1.5"
208
+ className="shrink-0 animate-spin"
209
+ aria-hidden="true"
210
+ >
211
+ <path d="M6 1.5A4.5 4.5 0 1 1 1.5 6" strokeLinecap="round" />
212
+ </svg>
213
+ );
214
+ }
215
+ return (
216
+ <svg
217
+ width="12"
218
+ height="12"
219
+ viewBox="0 0 12 12"
220
+ fill="none"
221
+ stroke="currentColor"
222
+ strokeWidth="1.5"
223
+ strokeLinecap="round"
224
+ strokeLinejoin="round"
225
+ className="shrink-0"
226
+ aria-hidden="true"
227
+ >
228
+ <circle cx="6" cy="5" r="3.5" />
229
+ <path d="M4.5 9.5C4.5 8.5 5 8 6 8s1.5.5 1.5 1.5" />
230
+ <circle cx="5" cy="4.5" r="0.5" fill="currentColor" />
231
+ <circle cx="7" cy="4.5" r="0.5" fill="currentColor" />
232
+ </svg>
233
+ );
234
+ }
235
+
236
+ function ChevronIcon({ expanded }: { expanded: boolean }) {
237
+ return (
238
+ <svg
239
+ width="10"
240
+ height="10"
241
+ viewBox="0 0 10 10"
242
+ fill="none"
243
+ stroke="currentColor"
244
+ strokeWidth="1.5"
245
+ strokeLinecap="round"
246
+ strokeLinejoin="round"
247
+ className={cn(
248
+ "shrink-0 transition-transform duration-150",
249
+ expanded && "rotate-90",
250
+ )}
251
+ aria-hidden="true"
252
+ >
253
+ <path d="M3.5 2L6.5 5L3.5 8" />
254
+ </svg>
255
+ );
256
+ }
package/src/index.ts CHANGED
@@ -18,19 +18,32 @@ export {
18
18
  export { type DeploymentMode, isResourceAvailable, ApiResourceKind } from "@stigmer/sdk";
19
19
  export { CloudFeatureNotice, type CloudFeatureNoticeProps } from "./internal/CloudFeatureNotice";
20
20
 
21
- // Models — data hook, styled component, and registry data
21
+ // Models — data hook, styled components, and registry data
22
22
  export {
23
23
  MODEL_REGISTRY,
24
24
  DEFAULT_MODEL_ID,
25
+ DEFAULT_CURSOR_MODEL_ID,
26
+ DISABLED_PROVIDERS,
27
+ modelKey,
28
+ parseModelKey,
25
29
  useModelRegistry,
26
30
  ModelSelector,
31
+ HarnessSelector,
32
+ DEFAULT_HARNESS,
33
+ HARNESS_LABELS,
34
+ toProtoHarness,
35
+ fromProtoHarness,
27
36
  } from "./models";
28
37
  export type {
29
38
  ModelInfo,
39
+ ParsedModelKey,
30
40
  Provider,
31
41
  CostTier,
32
42
  UseModelRegistryReturn,
43
+ UseModelRegistryOptions,
33
44
  ModelSelectorProps,
45
+ HarnessSelectorProps,
46
+ HarnessOption,
34
47
  } from "./models";
35
48
 
36
49
  // Workspace — behavior hooks and styled components
@@ -92,6 +105,7 @@ export type {
92
105
  UseNewSessionFlowReturn,
93
106
  UseSessionPageFlowOptions,
94
107
  UseSessionPageFlowReturn,
108
+ UsePersistedModelOptions,
95
109
  UsePersistedModelReturn,
96
110
  UseEditSessionPrepReturn,
97
111
  DraftResourceType,
@@ -0,0 +1,130 @@
1
+ "use client";
2
+
3
+ import { useCallback, useRef, type KeyboardEvent } from "react";
4
+ import { HARNESS_LABELS, type HarnessOption } from "./harness";
5
+
6
+ const OPTIONS: readonly HarnessOption[] = ["native", "cursor"];
7
+
8
+ /** Props for {@link HarnessSelector}. */
9
+ export interface HarnessSelectorProps {
10
+ /** Currently selected harness. */
11
+ readonly value: HarnessOption;
12
+ /** Called when the user picks a different harness. */
13
+ readonly onValueChange: (harness: HarnessOption) => void;
14
+ /** Additional CSS class names for the root container. */
15
+ readonly className?: string;
16
+ /** When true, disables the selector. */
17
+ readonly disabled?: boolean;
18
+ }
19
+
20
+ /**
21
+ * Compact segmented control for choosing the session execution engine.
22
+ *
23
+ * Renders two mutually exclusive options — "Stigmer" (native) and
24
+ * "Cursor" (premium) — as adjacent pill segments. The Cursor segment
25
+ * carries a subtle premium tier indicator.
26
+ *
27
+ * Built as a `radiogroup` with full arrow-key navigation and ARIA
28
+ * semantics. All visual properties flow through `--stgm-*` tokens.
29
+ *
30
+ * Platform builders who need different rendering use
31
+ * {@link HarnessOption} and {@link HARNESS_LABELS} directly.
32
+ *
33
+ * @deprecated Use {@link ModelSelector} in unified mode (without the
34
+ * `harness` prop) instead. The unified model picker embeds an engine
35
+ * tag on each model row, eliminating the need for a separate harness
36
+ * control. This component is kept for backward compatibility.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * function LauncherToolbar() {
41
+ * const [harness, setHarness] = useState<HarnessOption>("native");
42
+ *
43
+ * return <HarnessSelector value={harness} onValueChange={setHarness} />;
44
+ * }
45
+ * ```
46
+ */
47
+ export function HarnessSelector({
48
+ value,
49
+ onValueChange,
50
+ className,
51
+ disabled,
52
+ }: HarnessSelectorProps) {
53
+ const groupRef = useRef<HTMLDivElement>(null);
54
+
55
+ const handleKeyDown = useCallback(
56
+ (e: KeyboardEvent<HTMLDivElement>) => {
57
+ if (disabled) return;
58
+
59
+ const idx = OPTIONS.indexOf(value);
60
+ let next: number | undefined;
61
+
62
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") {
63
+ next = (idx + 1) % OPTIONS.length;
64
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
65
+ next = (idx - 1 + OPTIONS.length) % OPTIONS.length;
66
+ }
67
+
68
+ if (next !== undefined) {
69
+ e.preventDefault();
70
+ onValueChange(OPTIONS[next]);
71
+ const buttons = groupRef.current?.querySelectorAll<HTMLButtonElement>("[role=radio]");
72
+ buttons?.[next]?.focus();
73
+ }
74
+ },
75
+ [value, onValueChange, disabled],
76
+ );
77
+
78
+ return (
79
+ <div
80
+ ref={groupRef}
81
+ role="radiogroup"
82
+ aria-label="Execution engine"
83
+ onKeyDown={handleKeyDown}
84
+ className={[
85
+ "inline-flex items-center rounded-md border border-border bg-background p-0.5",
86
+ disabled ? "pointer-events-none opacity-50" : undefined,
87
+ className,
88
+ ]
89
+ .filter(Boolean)
90
+ .join(" ")}
91
+ >
92
+ {OPTIONS.map((option) => {
93
+ const isActive = value === option;
94
+
95
+ return (
96
+ <button
97
+ key={option}
98
+ type="button"
99
+ role="radio"
100
+ aria-checked={isActive}
101
+ aria-label={HARNESS_LABELS[option]}
102
+ tabIndex={isActive ? 0 : -1}
103
+ disabled={disabled}
104
+ onClick={() => {
105
+ if (!isActive) onValueChange(option);
106
+ }}
107
+ className={[
108
+ "inline-flex items-center gap-1 rounded-[5px] px-2 py-1 text-xs transition-colors",
109
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
110
+ "disabled:pointer-events-none",
111
+ isActive
112
+ ? "bg-accent font-medium text-foreground shadow-sm"
113
+ : "text-muted-foreground hover:text-foreground",
114
+ ].join(" ")}
115
+ >
116
+ {HARNESS_LABELS[option]}
117
+ {option === "cursor" && (
118
+ <span
119
+ aria-label="premium"
120
+ className="text-[0.6rem] text-muted-foreground"
121
+ >
122
+ $$$
123
+ </span>
124
+ )}
125
+ </button>
126
+ );
127
+ })}
128
+ </div>
129
+ );
130
+ }