@stigmer/react 3.1.3 → 3.1.4

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 (151) hide show
  1. package/composer/SessionComposer.d.ts +31 -0
  2. package/composer/SessionComposer.d.ts.map +1 -1
  3. package/composer/SessionComposer.js +12 -4
  4. package/composer/SessionComposer.js.map +1 -1
  5. package/execution/ArtifactApplyButton.d.ts +25 -0
  6. package/execution/ArtifactApplyButton.d.ts.map +1 -0
  7. package/execution/ArtifactApplyButton.js +21 -0
  8. package/execution/ArtifactApplyButton.js.map +1 -0
  9. package/execution/ArtifactContentBody.d.ts +36 -0
  10. package/execution/ArtifactContentBody.d.ts.map +1 -0
  11. package/execution/ArtifactContentBody.js +62 -0
  12. package/execution/ArtifactContentBody.js.map +1 -0
  13. package/execution/ArtifactDocument.d.ts +48 -0
  14. package/execution/ArtifactDocument.d.ts.map +1 -0
  15. package/execution/ArtifactDocument.js +89 -0
  16. package/execution/ArtifactDocument.js.map +1 -0
  17. package/execution/ArtifactPreviewModal.d.ts +4 -4
  18. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  19. package/execution/ArtifactPreviewModal.js +15 -142
  20. package/execution/ArtifactPreviewModal.js.map +1 -1
  21. package/execution/ArtifactRow.d.ts +49 -0
  22. package/execution/ArtifactRow.d.ts.map +1 -0
  23. package/execution/ArtifactRow.js +56 -0
  24. package/execution/ArtifactRow.js.map +1 -0
  25. package/execution/ArtifactsWidget.d.ts +6 -9
  26. package/execution/ArtifactsWidget.d.ts.map +1 -1
  27. package/execution/ArtifactsWidget.js +15 -19
  28. package/execution/ArtifactsWidget.js.map +1 -1
  29. package/execution/index.d.ts +8 -2
  30. package/execution/index.d.ts.map +1 -1
  31. package/execution/index.js +4 -1
  32. package/execution/index.js.map +1 -1
  33. package/execution/useArtifactContent.d.ts +6 -1
  34. package/execution/useArtifactContent.d.ts.map +1 -1
  35. package/execution/useArtifactContent.js +7 -2
  36. package/execution/useArtifactContent.js.map +1 -1
  37. package/execution/useArtifactInspection.d.ts +85 -0
  38. package/execution/useArtifactInspection.d.ts.map +1 -0
  39. package/execution/useArtifactInspection.js +143 -0
  40. package/execution/useArtifactInspection.js.map +1 -0
  41. package/execution/useCreateAgentExecution.d.ts +11 -0
  42. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  43. package/execution/useCreateAgentExecution.js +1 -0
  44. package/execution/useCreateAgentExecution.js.map +1 -1
  45. package/index.d.ts +3 -3
  46. package/index.d.ts.map +1 -1
  47. package/index.js +2 -2
  48. package/index.js.map +1 -1
  49. package/internal/MermaidDiagram.d.ts +47 -0
  50. package/internal/MermaidDiagram.d.ts.map +1 -0
  51. package/internal/MermaidDiagram.js +118 -0
  52. package/internal/MermaidDiagram.js.map +1 -0
  53. package/internal/markdown-components.d.ts.map +1 -1
  54. package/internal/markdown-components.js +34 -0
  55. package/internal/markdown-components.js.map +1 -1
  56. package/internal/mermaid-loader.d.ts +26 -0
  57. package/internal/mermaid-loader.d.ts.map +1 -0
  58. package/internal/mermaid-loader.js +34 -0
  59. package/internal/mermaid-loader.js.map +1 -0
  60. package/internal/mermaid-theme-css.d.ts +41 -0
  61. package/internal/mermaid-theme-css.d.ts.map +1 -0
  62. package/internal/mermaid-theme-css.js +179 -0
  63. package/internal/mermaid-theme-css.js.map +1 -0
  64. package/internal/slug.d.ts +9 -5
  65. package/internal/slug.d.ts.map +1 -1
  66. package/internal/slug.js +10 -6
  67. package/internal/slug.js.map +1 -1
  68. package/package.json +5 -4
  69. package/session/SessionViewer.d.ts.map +1 -1
  70. package/session/SessionViewer.js +91 -18
  71. package/session/SessionViewer.js.map +1 -1
  72. package/session/artifact-document.d.ts +13 -0
  73. package/session/artifact-document.d.ts.map +1 -0
  74. package/session/artifact-document.js +22 -0
  75. package/session/artifact-document.js.map +1 -0
  76. package/session/facets/ArtifactsTab.d.ts +30 -4
  77. package/session/facets/ArtifactsTab.d.ts.map +1 -1
  78. package/session/facets/ArtifactsTab.js +32 -10
  79. package/session/facets/ArtifactsTab.js.map +1 -1
  80. package/session/index.d.ts +2 -1
  81. package/session/index.d.ts.map +1 -1
  82. package/session/index.js +4 -1
  83. package/session/index.js.map +1 -1
  84. package/session/useSessionArtifacts.d.ts +17 -0
  85. package/session/useSessionArtifacts.d.ts.map +1 -1
  86. package/session/useSessionArtifacts.js +16 -5
  87. package/session/useSessionArtifacts.js.map +1 -1
  88. package/session/useSessionConversation.d.ts +17 -1
  89. package/session/useSessionConversation.d.ts.map +1 -1
  90. package/session/useSessionConversation.js +32 -4
  91. package/session/useSessionConversation.js.map +1 -1
  92. package/session/useSessionPageFlow.d.ts.map +1 -1
  93. package/session/useSessionPageFlow.js +1 -0
  94. package/session/useSessionPageFlow.js.map +1 -1
  95. package/session/useSessionPanel.d.ts +20 -0
  96. package/session/useSessionPanel.d.ts.map +1 -1
  97. package/session/useSessionPanel.js +13 -0
  98. package/session/useSessionPanel.js.map +1 -1
  99. package/session/useSessionRailViews.d.ts +13 -1
  100. package/session/useSessionRailViews.d.ts.map +1 -1
  101. package/session/useSessionRailViews.js +5 -3
  102. package/session/useSessionRailViews.js.map +1 -1
  103. package/src/composer/SessionComposer.tsx +69 -1
  104. package/src/composer/__tests__/SessionComposer-editing.test.tsx +128 -0
  105. package/src/execution/ArtifactApplyButton.tsx +79 -0
  106. package/src/execution/ArtifactContentBody.tsx +255 -0
  107. package/src/execution/ArtifactDocument.tsx +344 -0
  108. package/src/execution/ArtifactPreviewModal.tsx +39 -405
  109. package/src/execution/ArtifactRow.tsx +165 -0
  110. package/src/execution/ArtifactsWidget.tsx +24 -33
  111. package/src/execution/__tests__/ArtifactContentBody.test.tsx +131 -0
  112. package/src/execution/__tests__/ArtifactDocument.test.tsx +100 -0
  113. package/src/execution/__tests__/ArtifactRow.test.tsx +139 -0
  114. package/src/execution/__tests__/ArtifactsWidget.test.tsx +78 -0
  115. package/src/execution/__tests__/message-entry.test.tsx +50 -2
  116. package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
  117. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
  118. package/src/execution/index.ts +14 -2
  119. package/src/execution/useArtifactContent.ts +7 -0
  120. package/src/execution/useArtifactInspection.ts +252 -0
  121. package/src/execution/useCreateAgentExecution.ts +12 -0
  122. package/src/index.ts +11 -2
  123. package/src/internal/MermaidDiagram.tsx +164 -0
  124. package/src/internal/__tests__/a11y/mermaid-theme.a11y.test.tsx +122 -0
  125. package/src/internal/__tests__/markdown-components.test.tsx +93 -1
  126. package/src/internal/__tests__/mermaid-diagram.test.tsx +282 -0
  127. package/src/internal/markdown-components.tsx +36 -1
  128. package/src/internal/mermaid-loader.ts +44 -0
  129. package/src/internal/mermaid-theme-css.ts +178 -0
  130. package/src/internal/slug.ts +10 -6
  131. package/src/session/SessionViewer.tsx +177 -33
  132. package/src/session/__tests__/SessionViewer-artifactTabs.test.tsx +210 -0
  133. package/src/session/__tests__/SessionViewer-editResubmit.test.tsx +233 -0
  134. package/src/session/__tests__/useSessionArtifacts.test.ts +57 -0
  135. package/src/session/__tests__/useSessionConversation.test.tsx +172 -1
  136. package/src/session/__tests__/useSessionPanel.test.ts +70 -1
  137. package/src/session/artifact-document.ts +23 -0
  138. package/src/session/facets/ArtifactsTab.tsx +66 -18
  139. package/src/session/facets/__tests__/ArtifactsTab.test.tsx +111 -47
  140. package/src/session/index.ts +5 -1
  141. package/src/session/useSessionArtifacts.ts +16 -5
  142. package/src/session/useSessionConversation.ts +48 -6
  143. package/src/session/useSessionPageFlow.ts +1 -0
  144. package/src/session/useSessionPanel.ts +41 -0
  145. package/src/session/useSessionRailViews.tsx +21 -1
  146. package/styles.css +1 -1
  147. package/execution/ArtifactCard.d.ts +0 -81
  148. package/execution/ArtifactCard.d.ts.map +0 -1
  149. package/execution/ArtifactCard.js +0 -130
  150. package/execution/ArtifactCard.js.map +0 -1
  151. package/src/execution/ArtifactCard.tsx +0 -320
@@ -1,10 +1,12 @@
1
- import type { ComponentProps, JSX } from "react";
1
+ import { isValidElement, type ComponentProps, type JSX, type ReactNode } from "react";
2
2
  import type { Components } from "react-markdown";
3
3
  import remarkGfm from "remark-gfm";
4
4
  import { cn } from "@stigmer/theme";
5
5
  import { highlightToReact } from "./code-highlight.js";
6
+ import { MermaidDiagram } from "./MermaidDiagram.js";
6
7
 
7
8
  const LANGUAGE_CLASS_PREFIX = "language-";
9
+ const MERMAID_LANGUAGE_CLASS = "language-mermaid";
8
10
 
9
11
  type MdProps<T extends keyof JSX.IntrinsicElements> = ComponentProps<T>;
10
12
 
@@ -119,6 +121,30 @@ export function extractLeadingH1(markdown: string): {
119
121
  return { title: match[1], body: trimmed.slice(match[0].length) };
120
122
  }
121
123
 
124
+ /**
125
+ * Returns the mermaid source when a `pre` element's children are a single
126
+ * fenced code block explicitly tagged `mermaid`, or `null` otherwise.
127
+ *
128
+ * Both renderers (Streamdown for chat, react-markdown for artifacts/skills)
129
+ * hand `pre` exactly one `<code>` child whose `className` carries the fence's
130
+ * info string as `language-*` and whose children are the raw source text.
131
+ * Anything that deviates from that shape — a different language, a missing
132
+ * tag, non-string children — is not a mermaid fence and keeps the ordinary
133
+ * code-block rendering. Only the explicit `mermaid` tag qualifies:
134
+ * inspecting fence bodies to guess diagram intent is the kind of fuzzy
135
+ * heuristic this codebase avoids.
136
+ */
137
+ function extractMermaidSource(children: ReactNode): string | null {
138
+ if (!isValidElement(children)) return null;
139
+ const { className, children: code } = children.props as {
140
+ className?: unknown;
141
+ children?: unknown;
142
+ };
143
+ if (typeof className !== "string") return null;
144
+ if (!className.split(/\s+/).includes(MERMAID_LANGUAGE_CLASS)) return null;
145
+ return typeof code === "string" ? code : null;
146
+ }
147
+
122
148
  /**
123
149
  * Styled react-markdown component overrides for SDK markdown surfaces.
124
150
  *
@@ -208,6 +234,15 @@ export const MARKDOWN_COMPONENTS: Components = {
208
234
  },
209
235
 
210
236
  pre({ children, ...props }: MdProps<"pre">) {
237
+ // A ```mermaid fence renders as a diagram, not a code block. The check
238
+ // lives here (not in the `code` override) because the diagram must
239
+ // replace the <pre> wrapper too — a block-level diagram container inside
240
+ // <pre> is invalid HTML and would inherit code-block chrome.
241
+ const mermaidSource = extractMermaidSource(children);
242
+ if (mermaidSource !== null) {
243
+ return <MermaidDiagram chart={mermaidSource} />;
244
+ }
245
+
211
246
  return (
212
247
  <pre
213
248
  className="mb-3 last:mb-0 overflow-x-auto rounded-md bg-muted p-3"
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Lazy loader for the `mermaid` library, isolated in its own module — the
3
+ * same seam pattern as `workflow/layout/elk-layout-engine.ts` — so
4
+ * {@link file://./MermaidDiagram.tsx} stays a pure presentation component and
5
+ * tests can mock the load outcome (resolve, reject) without touching module
6
+ * registries.
7
+ *
8
+ * `mermaid` is an optional peer dependency (DD-013): it is large, so it must
9
+ * never sit on the synchronous bundle path, and a host that has not installed
10
+ * it must degrade gracefully (the caller falls back to a code block).
11
+ */
12
+
13
+ /**
14
+ * The mermaid API surface the SDK consumes, typed from the package itself so
15
+ * it stays in lockstep with the installed version.
16
+ */
17
+ export type MermaidModule = typeof import("mermaid").default;
18
+
19
+ /**
20
+ * Holds the in-flight import promise so concurrent diagrams share one load.
21
+ * A failed import clears the cache: rejections are often transient (a
22
+ * chunk-load hiccup mid-deploy), and caching one would break every diagram
23
+ * for the rest of the session.
24
+ */
25
+ let mermaidImport: Promise<MermaidModule> | null = null;
26
+
27
+ /**
28
+ * Dynamically imports `mermaid`, sharing one in-flight import across callers.
29
+ *
30
+ * @returns The mermaid default export.
31
+ * @throws If `mermaid` cannot be imported (not installed, or the async chunk
32
+ * failed to load). Callers treat this as "diagrams unavailable" and render
33
+ * the fenced source as a plain code block.
34
+ */
35
+ export function loadMermaid(): Promise<MermaidModule> {
36
+ mermaidImport ??= import("mermaid").then(
37
+ (module) => module.default,
38
+ (error: unknown) => {
39
+ mermaidImport = null;
40
+ throw error;
41
+ },
42
+ );
43
+ return mermaidImport;
44
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Token-driven `themeCSS` for Mermaid diagram interiors.
3
+ *
4
+ * Mermaid's built-in `default`/`dark` themes color a diagram's SVG interior
5
+ * with a fixed palette that ignores the host's `--stgm-*` preset (Corporate,
6
+ * Fintech, ...). The obvious fix — feeding resolved tokens into Mermaid's
7
+ * `themeVariables` — is blocked: our tokens are authored in `oklch()`, and
8
+ * Mermaid's color engine (`khroma`) parses only hex/rgb/hsl/keyword, so it
9
+ * throws or NaNs on `oklch(...)`. (Mermaid also gates every `themeVariables`
10
+ * value through the allowlist `^[\d "#%(),.;A-Za-z]+$`, which excludes `-` and
11
+ * would blank any `var(--...)`.)
12
+ *
13
+ * Instead we hand Mermaid this CSS via `initialize({ themeCSS })`. Mermaid
14
+ * injects it into the diagram's own `<style>` block, scoped under the render
15
+ * id (`#stgm-mermaid-N`), *after* the theme palette it computes. Because every
16
+ * selector in that block is id-prefixed, our rules and the palette's rules sit
17
+ * at equal specificity, so ours win by source order — and `!important` makes
18
+ * that bulletproof against the few palette rules that are themselves important
19
+ * or inlined. Crucially, `themeCSS` never passes through khroma, so the
20
+ * `oklch()`-backed `var(--stgm-*)` references flow straight to the browser,
21
+ * which resolves them against the `.stgm` scope the SVG lives in. That is why
22
+ * interiors track the active preset *and* color mode live, with no JS color
23
+ * reading and no re-render on theme change.
24
+ *
25
+ * `initialize({ theme })` still selects the built-in `default`/`dark` theme as
26
+ * the base, so any element or diagram type we do not target here degrades to
27
+ * Mermaid's stock palette — never a blank or mis-colored diagram.
28
+ *
29
+ * Palette intent (subtle surfaces + brand accents — legibility over brand
30
+ * saturation): node/actor bodies sit on a muted surface with quiet
31
+ * token-colored borders and edges; labels use the standard foreground; the
32
+ * brand `--stgm-primary` is reserved for structural headers (subgraph/cluster
33
+ * titles, sequence loop/section titles, the sequence-number badge).
34
+ *
35
+ * Coverage is the two families agents emit overwhelmingly — flowchart and
36
+ * sequence — plus the shared marker/edge-label chrome. Both label render paths
37
+ * are handled: SVG `text`/`tspan` (via `fill`) and HTML labels inside
38
+ * `foreignObject` (via `color`).
39
+ */
40
+ export const MERMAID_THEME_CSS = `
41
+ /* Shared: edge arrowheads / markers */
42
+ .marker {
43
+ fill: var(--stgm-border) !important;
44
+ stroke: var(--stgm-border) !important;
45
+ }
46
+ .marker.cross {
47
+ stroke: var(--stgm-border) !important;
48
+ }
49
+
50
+ /* Flowchart: node bodies */
51
+ .node rect,
52
+ .node circle,
53
+ .node ellipse,
54
+ .node polygon,
55
+ .node path {
56
+ fill: var(--stgm-muted) !important;
57
+ stroke: var(--stgm-border) !important;
58
+ }
59
+ .node .neo-node {
60
+ stroke: var(--stgm-border) !important;
61
+ }
62
+ .node .katex path {
63
+ fill: var(--stgm-foreground) !important;
64
+ stroke: var(--stgm-foreground) !important;
65
+ }
66
+
67
+ /* Flowchart: node labels (SVG text and HTML spans) */
68
+ .label {
69
+ color: var(--stgm-foreground) !important;
70
+ }
71
+ .label text,
72
+ .label span {
73
+ fill: var(--stgm-foreground) !important;
74
+ color: var(--stgm-foreground) !important;
75
+ }
76
+
77
+ /* Flowchart: edges */
78
+ .edgePath .path,
79
+ .flowchart-link {
80
+ stroke: var(--stgm-border) !important;
81
+ }
82
+ .arrowheadPath,
83
+ .root .anchor path {
84
+ fill: var(--stgm-border) !important;
85
+ stroke: var(--stgm-border) !important;
86
+ }
87
+
88
+ /* Flowchart: edge labels sit on the diagram surface for legibility */
89
+ .edgeLabel,
90
+ .edgeLabel p,
91
+ .edgeLabel rect,
92
+ .labelBkg {
93
+ background-color: var(--stgm-card) !important;
94
+ fill: var(--stgm-card) !important;
95
+ }
96
+
97
+ /* Flowchart: subgraph clusters — muted surface, brand-accent title */
98
+ .cluster rect {
99
+ fill: var(--stgm-muted-subtle) !important;
100
+ stroke: var(--stgm-border) !important;
101
+ }
102
+ .cluster-label text,
103
+ .cluster text {
104
+ fill: var(--stgm-primary) !important;
105
+ }
106
+ .cluster-label span,
107
+ .cluster span {
108
+ color: var(--stgm-primary) !important;
109
+ }
110
+
111
+ /* Sequence: actors and lifelines */
112
+ .actor {
113
+ fill: var(--stgm-muted) !important;
114
+ stroke: var(--stgm-border) !important;
115
+ }
116
+ text.actor > tspan {
117
+ fill: var(--stgm-foreground) !important;
118
+ }
119
+ .actor-line {
120
+ stroke: var(--stgm-border) !important;
121
+ }
122
+
123
+ /* Sequence: messages */
124
+ .messageLine0,
125
+ .messageLine1 {
126
+ stroke: var(--stgm-border) !important;
127
+ }
128
+ [id$="-arrowhead"] path,
129
+ [id$="-crosshead"] path {
130
+ fill: var(--stgm-border) !important;
131
+ stroke: var(--stgm-border) !important;
132
+ }
133
+ .messageText {
134
+ fill: var(--stgm-foreground) !important;
135
+ }
136
+
137
+ /* Sequence: number badge — brand accent with its paired foreground */
138
+ [id$="-sequencenumber"] {
139
+ fill: var(--stgm-primary) !important;
140
+ }
141
+ .sequenceNumber {
142
+ fill: var(--stgm-primary-foreground) !important;
143
+ }
144
+
145
+ /* Sequence: alt/opt/loop boxes and labels */
146
+ .labelBox {
147
+ fill: var(--stgm-muted-subtle) !important;
148
+ stroke: var(--stgm-border) !important;
149
+ }
150
+ .labelText,
151
+ .labelText > tspan {
152
+ fill: var(--stgm-foreground) !important;
153
+ }
154
+ .loopText,
155
+ .loopText > tspan,
156
+ .sectionTitle,
157
+ .sectionTitle > tspan {
158
+ fill: var(--stgm-primary) !important;
159
+ }
160
+ .loopLine {
161
+ stroke: var(--stgm-border) !important;
162
+ }
163
+
164
+ /* Sequence: notes and activations */
165
+ .note {
166
+ fill: var(--stgm-muted-subtle) !important;
167
+ stroke: var(--stgm-border) !important;
168
+ }
169
+ .noteText,
170
+ .noteText > tspan {
171
+ fill: var(--stgm-foreground) !important;
172
+ }
173
+ .activation0,
174
+ .activation1 {
175
+ fill: var(--stgm-muted) !important;
176
+ stroke: var(--stgm-border) !important;
177
+ }
178
+ `;
@@ -7,23 +7,27 @@
7
7
  *
8
8
  * Rules (applied in order):
9
9
  * 1. Convert to lowercase
10
- * 2. Replace spaces with hyphens
10
+ * 2. Replace spaces and dots (namespace separators) with hyphens
11
11
  * 3. Strip everything except `a-z`, `0-9`, and `-`
12
12
  * 4. Collapse consecutive hyphens into one
13
13
  * 5. Trim leading / trailing hyphens
14
14
  *
15
+ * Dots are treated as namespace separators (e.g. `platform.sara` → `platform-sara`),
16
+ * matching the backend generators, so namespace-scoped names stay readable.
17
+ *
15
18
  * @example
16
19
  * ```ts
17
- * generateSlug("My Cool Agent") // "my-cool-agent"
18
- * generateSlug("Acme Corp!") // "acme-corp"
19
- * generateSlug(" --hello-- ") // "hello"
20
- * generateSlug("") // ""
20
+ * generateSlug("My Cool Agent") // "my-cool-agent"
21
+ * generateSlug("Acme Corp!") // "acme-corp"
22
+ * generateSlug(" --hello-- ") // "hello"
23
+ * generateSlug("platform.planton-arch") // "platform-planton-arch"
24
+ * generateSlug("") // ""
21
25
  * ```
22
26
  */
23
27
  export function generateSlug(name: string): string {
24
28
  return name
25
29
  .toLowerCase()
26
- .replace(/ /g, "-")
30
+ .replace(/[ .]/g, "-")
27
31
  .replace(/[^a-z0-9-]/g, "")
28
32
  .replace(/-{2,}/g, "-")
29
33
  .replace(/^-+|-+$/g, "");
@@ -17,7 +17,7 @@ import {
17
17
  WorkspaceSurface,
18
18
  type SurfaceVirtualDocument,
19
19
  } from "../workspace/WorkspaceSurface.js";
20
- import type { InteractionModeOption, SessionComposerHandle } from "../composer/index.js";
20
+ import type { InteractionModeOption, SessionComposerHandle, SessionComposerSubmitContext } from "../composer/index.js";
21
21
  import type { ApplyResourceResult } from "../library/useApplyResource.js";
22
22
  import { ResizableSplit } from "../internal/ResizableSplit.js";
23
23
  import { SelectionStore } from "../internal/store/selection-store.js";
@@ -47,11 +47,17 @@ import { usePlanDraft, planDraftKey, type PlanDraftController } from "./usePlanD
47
47
  import { PlanEditor } from "./PlanEditor.js";
48
48
  import { PlanStreamingDocument } from "./PlanStreamingDocument.js";
49
49
  import { PLAN_DOCUMENT_ENTRY_ID, PLAN_DOCUMENT_PATH } from "./plan-document.js";
50
+ import { ARTIFACT_DOCUMENT_ENTRY_ID } from "./artifact-document.js";
51
+ import { ArtifactDocument } from "../execution/ArtifactDocument.js";
50
52
  import { useSessionRailViews } from "./useSessionRailViews.js";
51
53
  import { useSessionPanel, type SessionPanelController } from "./useSessionPanel.js";
52
54
  import { SessionPanelChip } from "./SessionPanelChip.js";
53
55
  import { useSessionWriteBacks } from "./useSessionWriteBacks.js";
54
- import { useSessionArtifacts } from "./useSessionArtifacts.js";
56
+ import {
57
+ useSessionArtifacts,
58
+ artifactKey,
59
+ type SessionArtifactEntry,
60
+ } from "./useSessionArtifacts.js";
55
61
  import type { SetupTabProps } from "./facets/SetupTab.js";
56
62
  import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
57
63
  import type { RuntimeEnvProvider } from "./runtime-env.js";
@@ -455,6 +461,28 @@ export function SessionViewer({
455
461
  [sessionPlan, panel.openPlanDocument],
456
462
  );
457
463
 
464
+ // "Open artifact" (an Artifacts-facet row) → an editor-pane document tab.
465
+ // Writing the editors store re-renders only the panel subtree (which
466
+ // subscribes), never this streaming column. Plan artifacts are routed to the
467
+ // plan tab upstream (in ArtifactsTab), so they never reach here.
468
+ const handleOpenArtifact = useCallback(
469
+ (entry: SessionArtifactEntry) => {
470
+ panel.openArtifact(entry.artifact);
471
+ },
472
+ [panel.openArtifact],
473
+ );
474
+
475
+ // Double-click an artifact row → pin its tab (the leading single click already
476
+ // opened the preview via `handleOpenArtifact`). Mirrors the file tree's
477
+ // preview/pin split; plan artifacts are routed to the plan tab upstream (in
478
+ // ArtifactsTab) and never reach here.
479
+ const handleActivateArtifact = useCallback(
480
+ (entry: SessionArtifactEntry) => {
481
+ panel.pinArtifact(entry.artifact);
482
+ },
483
+ [panel.pinArtifact],
484
+ );
485
+
458
486
  // Open a transcript tool-call file path in the panel's read-only editor.
459
487
  // Resolves the (possibly absolute / subdir-prefixed) path to a
460
488
  // repo/root-relative selection the editor can fetch; on a definite hit it
@@ -561,6 +589,8 @@ export function SessionViewer({
561
589
  planDraft={planDraft}
562
590
  openPlanExecutionId={openPlanExecutionId}
563
591
  onOpenPlan={handleOpenPlan}
592
+ onOpenArtifact={handleOpenArtifact}
593
+ onActivateArtifact={handleActivateArtifact}
564
594
  enableLocal={enableLocal}
565
595
  onBrowseLocalFolder={onBrowseLocalFolder}
566
596
  workspaceFileLister={workspaceFileLister}
@@ -652,18 +682,55 @@ const ConversationColumn = memo(function ConversationColumn({
652
682
  void conv.stop();
653
683
  }, [conv.stop]);
654
684
 
655
- // Edit-and-resubmit: stop the in-flight turn and pre-fill the composer with
656
- // the original text. The append-only execution log can't be rewritten, so
657
- // the user reviews the prefilled message and resubmits it as a NEW execution
658
- // through the normal Send pipeline. The cancelled turn stays in history with
659
- // its phase badge an honest record rather than a silent edit.
685
+ // Edit-and-resubmit: stop the in-flight turn, pre-fill the composer with
686
+ // the original text, and remember which execution is being edited. The
687
+ // append-only execution log is never rewritten submitting while editing
688
+ // creates a NEW execution that carries `supersedesExecutionId`, and the
689
+ // conversation read model hides the superseded turn so the edited message
690
+ // replaces the original in place (Cursor-style, stigmer/stigmer#181).
691
+ //
692
+ // The editing state is explicit (banner + cancel in the composer) so the
693
+ // supersede link only attaches when the user actually resubmits the edit.
694
+ // Cancelling drops the link — a subsequent unrelated message appends
695
+ // normally without hiding any history.
696
+ const [editingExecutionId, setEditingExecutionId] = useState<string | null>(
697
+ null,
698
+ );
699
+ const activeExecutionId = conv.activeStreamExecution?.metadata?.id ?? null;
700
+
660
701
  const handleEditMessage = useCallback(
661
702
  (text: string) => {
662
703
  void conv.stop();
704
+ setEditingExecutionId(activeExecutionId);
663
705
  composerRef.current?.setMessage(text);
664
706
  composerRef.current?.focus();
665
707
  },
666
- [conv.stop, composerRef],
708
+ [conv.stop, activeExecutionId, composerRef],
709
+ );
710
+
711
+ const handleCancelEdit = useCallback(() => {
712
+ setEditingExecutionId(null);
713
+ composerRef.current?.setMessage("");
714
+ }, [composerRef]);
715
+
716
+ // Submit wrapper that attaches the supersede link while editing, then
717
+ // exits editing mode. Ordinary (non-editing) submits pass through as-is.
718
+ const handleComposerSubmit = useCallback(
719
+ (
720
+ message: string,
721
+ modelName?: string,
722
+ context?: SessionComposerSubmitContext,
723
+ ) => {
724
+ void flow.handleSubmit(
725
+ message,
726
+ modelName,
727
+ editingExecutionId
728
+ ? { ...context, supersedesExecutionId: editingExecutionId }
729
+ : context,
730
+ );
731
+ setEditingExecutionId(null);
732
+ },
733
+ [flow.handleSubmit, editingExecutionId],
667
734
  );
668
735
 
669
736
  // The composer-docked FileReviewDock sits OUTSIDE MessageThread, so the
@@ -743,11 +810,13 @@ const ConversationColumn = memo(function ConversationColumn({
743
810
  </FilePathContext.Provider>
744
811
  <SessionComposer
745
812
  ref={composerRef}
746
- onSubmit={flow.handleSubmit}
813
+ onSubmit={handleComposerSubmit}
747
814
  isSubmitting={conv.isSending}
748
815
  disabled={!conv.canSendFollowUp}
749
816
  onStop={conv.isStoppable ? handleStop : undefined}
750
817
  isStopping={conv.isStopping}
818
+ isEditing={editingExecutionId != null}
819
+ onCancelEdit={handleCancelEdit}
751
820
  org={org}
752
821
  harness={flow.harness}
753
822
  defaultModelId={modelId}
@@ -815,6 +884,10 @@ interface SessionPanelRegionProps {
815
884
  readonly openPlanExecutionId: string | null;
816
885
  /** Opens a plan in the plan tab (routed to the Artifacts facet's plan.md). */
817
886
  readonly onOpenPlan: (executionId: string) => void;
887
+ /** Opens an artifact as an editor-pane document (routed to the Artifacts facet). */
888
+ readonly onOpenArtifact: (entry: SessionArtifactEntry) => void;
889
+ /** Pins an artifact's document tab — the double-click half of the open/pin split. */
890
+ readonly onActivateArtifact: (entry: SessionArtifactEntry) => void;
818
891
  readonly enableLocal: boolean;
819
892
  readonly onBrowseLocalFolder?: () => Promise<string | null>;
820
893
  readonly workspaceFileLister?: WorkspaceFileLister;
@@ -836,6 +909,8 @@ function SessionPanelRegion({
836
909
  planDraft,
837
910
  openPlanExecutionId,
838
911
  onOpenPlan,
912
+ onOpenArtifact,
913
+ onActivateArtifact,
839
914
  enableLocal,
840
915
  onBrowseLocalFolder,
841
916
  workspaceFileLister,
@@ -847,6 +922,9 @@ function SessionPanelRegion({
847
922
  const { editors, activeKey, activeFile, reveal } = useWorkspaceEditors(
848
923
  panel.editorsStore,
849
924
  );
925
+ // Session artifacts, deduped by identity — the source both the Artifacts
926
+ // rail-view badge and the open artifact document tabs resolve against.
927
+ const { artifacts: sessionArtifacts } = useSessionArtifacts(flow.allExecutions);
850
928
 
851
929
  // Honor a pending jump-to-line reveal only while it targets the active editor
852
930
  // (an EditorReveal is structurally a RevealTarget). Switching tabs changes
@@ -902,30 +980,28 @@ function SessionPanelRegion({
902
980
  // tab and its turn then ended without publishing, with no earlier plan to
903
981
  // fall back to.
904
982
  const openPlanIsLatest = openPlan?.executionId === sessionPlan?.executionId;
905
- const virtualDocuments = useMemo<readonly SurfaceVirtualDocument[]>(() => {
906
- return [
907
- {
908
- entryId: PLAN_DOCUMENT_ENTRY_ID,
909
- path: PLAN_DOCUMENT_PATH,
910
- content: streamingPlan ? (
911
- <PlanStreamingDocument
912
- key={`streaming-${streamingPlan.executionId}`}
913
- displayText={streamingPlan.displayText}
914
- />
915
- ) : openPlan ? (
916
- <PlanEditor
917
- key={planDraftKey(openPlan)}
918
- plan={openPlan}
919
- draft={openPlanIsLatest ? planDraft : undefined}
920
- onBuildFromPlan={openPlanIsLatest ? onImplementPlan : undefined}
921
- buildDisabled={implementPlanDisabled}
922
- readOnly={!openPlanIsLatest}
923
- />
924
- ) : (
925
- <PlanUnavailableNotice />
926
- ),
927
- },
928
- ];
983
+ const planVirtualDocument = useMemo<SurfaceVirtualDocument>(() => {
984
+ return {
985
+ entryId: PLAN_DOCUMENT_ENTRY_ID,
986
+ path: PLAN_DOCUMENT_PATH,
987
+ content: streamingPlan ? (
988
+ <PlanStreamingDocument
989
+ key={`streaming-${streamingPlan.executionId}`}
990
+ displayText={streamingPlan.displayText}
991
+ />
992
+ ) : openPlan ? (
993
+ <PlanEditor
994
+ key={planDraftKey(openPlan)}
995
+ plan={openPlan}
996
+ draft={openPlanIsLatest ? planDraft : undefined}
997
+ onBuildFromPlan={openPlanIsLatest ? onImplementPlan : undefined}
998
+ buildDisabled={implementPlanDisabled}
999
+ readOnly={!openPlanIsLatest}
1000
+ />
1001
+ ) : (
1002
+ <PlanUnavailableNotice />
1003
+ ),
1004
+ };
929
1005
  }, [
930
1006
  streamingPlan,
931
1007
  openPlan,
@@ -935,6 +1011,50 @@ function SessionPanelRegion({
935
1011
  implementPlanDisabled,
936
1012
  ]);
937
1013
 
1014
+ // The open artifact document tabs (the artifact virtual-document FAMILY).
1015
+ // Built in a SEPARATE memo from the plan doc so artifact-tab churn never
1016
+ // recreates the plan doc (and vice versa). Bounded to what is actually open:
1017
+ // one document per editor whose entry id is the artifact family's, resolved
1018
+ // back to its `SessionArtifactEntry` by the same `artifactKey` used to open
1019
+ // it (single source of truth for artifact identity). Only the ACTIVE doc's
1020
+ // `content` is mounted by the surface, so building these elements is cheap;
1021
+ // keying on `contentHash` remounts a tab whose artifact was overwritten by a
1022
+ // later execution. A tab whose artifact no longer exists degrades to an
1023
+ // honest notice rather than vanishing.
1024
+ const artifactByKey = useMemo(
1025
+ () =>
1026
+ new Map(sessionArtifacts.map((entry) => [artifactKey(entry.artifact), entry])),
1027
+ [sessionArtifacts],
1028
+ );
1029
+ const artifactVirtualDocuments = useMemo<readonly SurfaceVirtualDocument[]>(() => {
1030
+ return editors
1031
+ .filter((editor) => editor.entryId === ARTIFACT_DOCUMENT_ENTRY_ID)
1032
+ .map((editor) => {
1033
+ const entry = artifactByKey.get(editor.path);
1034
+ return {
1035
+ entryId: ARTIFACT_DOCUMENT_ENTRY_ID,
1036
+ path: editor.path,
1037
+ content: entry ? (
1038
+ <ArtifactDocument
1039
+ key={entry.artifact.contentHash || editor.path}
1040
+ artifact={entry.artifact}
1041
+ executionId={entry.executionId}
1042
+ org={org}
1043
+ isTerminal={entry.isTerminal}
1044
+ onApplied={onApplied}
1045
+ />
1046
+ ) : (
1047
+ <ArtifactUnavailableNotice />
1048
+ ),
1049
+ };
1050
+ });
1051
+ }, [editors, artifactByKey, org, onApplied]);
1052
+
1053
+ const virtualDocuments = useMemo<readonly SurfaceVirtualDocument[]>(
1054
+ () => [planVirtualDocument, ...artifactVirtualDocuments],
1055
+ [planVirtualDocument, artifactVirtualDocuments],
1056
+ );
1057
+
938
1058
  const handleRemoveAgent = useCallback(() => {
939
1059
  flow.setAgentRef(null);
940
1060
  flow.setResolution(null);
@@ -1001,6 +1121,8 @@ function SessionPanelRegion({
1001
1121
  onApplied,
1002
1122
  onImplementPlan,
1003
1123
  onOpenPlan,
1124
+ onOpenArtifact,
1125
+ onActivateArtifact,
1004
1126
  });
1005
1127
 
1006
1128
  // Explorer-footer folder attach (desktop only — needs the native picker).
@@ -1125,6 +1247,28 @@ function PlanUnavailableNotice() {
1125
1247
  );
1126
1248
  }
1127
1249
 
1250
+ /**
1251
+ * Shown in an artifact document tab whose artifact is no longer among the
1252
+ * session's outputs (an edge only reachable if the executions backing the tab
1253
+ * changed underneath it). The tab stays closable; the content is honest rather
1254
+ * than blank.
1255
+ */
1256
+ function ArtifactUnavailableNotice() {
1257
+ return (
1258
+ <div
1259
+ role="status"
1260
+ className="mx-auto flex w-full max-w-3xl flex-col items-center gap-1 px-4 py-8 text-center"
1261
+ >
1262
+ <p className="text-xs font-medium text-foreground">
1263
+ This artifact is no longer available.
1264
+ </p>
1265
+ <p className="text-xs text-muted-foreground">
1266
+ It may have been replaced by a newer version of this session.
1267
+ </p>
1268
+ </div>
1269
+ );
1270
+ }
1271
+
1128
1272
  /**
1129
1273
  * Non-blocking notice for a build that fell back to message-only implement
1130
1274
  * (the approved plan.md could not be uploaded/attached). The build itself