@ryanfw/prompt-orchestration-pipeline 0.11.0 → 0.13.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 (83) hide show
  1. package/package.json +11 -1
  2. package/src/cli/analyze-task.js +51 -0
  3. package/src/cli/index.js +8 -0
  4. package/src/components/AddPipelineSidebar.jsx +144 -0
  5. package/src/components/AnalysisProgressTray.jsx +87 -0
  6. package/src/components/DAGGrid.jsx +157 -47
  7. package/src/components/JobTable.jsx +4 -3
  8. package/src/components/Layout.jsx +142 -139
  9. package/src/components/MarkdownRenderer.jsx +149 -0
  10. package/src/components/PipelineDAGGrid.jsx +404 -0
  11. package/src/components/PipelineTypeTaskSidebar.jsx +96 -0
  12. package/src/components/SchemaPreviewPanel.jsx +97 -0
  13. package/src/components/StageTimeline.jsx +36 -0
  14. package/src/components/TaskAnalysisDisplay.jsx +227 -0
  15. package/src/components/TaskCreationSidebar.jsx +447 -0
  16. package/src/components/TaskDetailSidebar.jsx +119 -117
  17. package/src/components/TaskFilePane.jsx +94 -39
  18. package/src/components/ui/RestartJobModal.jsx +26 -6
  19. package/src/components/ui/StopJobModal.jsx +183 -0
  20. package/src/components/ui/button.jsx +59 -27
  21. package/src/components/ui/sidebar.jsx +118 -0
  22. package/src/config/models.js +99 -67
  23. package/src/core/config.js +11 -4
  24. package/src/core/lifecycle-policy.js +62 -0
  25. package/src/core/pipeline-runner.js +312 -217
  26. package/src/core/status-writer.js +84 -0
  27. package/src/llm/index.js +129 -9
  28. package/src/pages/Code.jsx +8 -1
  29. package/src/pages/PipelineDetail.jsx +84 -2
  30. package/src/pages/PipelineList.jsx +214 -0
  31. package/src/pages/PipelineTypeDetail.jsx +234 -0
  32. package/src/pages/PromptPipelineDashboard.jsx +10 -11
  33. package/src/providers/deepseek.js +76 -16
  34. package/src/providers/openai.js +61 -34
  35. package/src/task-analysis/enrichers/analysis-writer.js +62 -0
  36. package/src/task-analysis/enrichers/schema-deducer.js +145 -0
  37. package/src/task-analysis/enrichers/schema-writer.js +74 -0
  38. package/src/task-analysis/extractors/artifacts.js +137 -0
  39. package/src/task-analysis/extractors/llm-calls.js +176 -0
  40. package/src/task-analysis/extractors/stages.js +51 -0
  41. package/src/task-analysis/index.js +103 -0
  42. package/src/task-analysis/parser.js +28 -0
  43. package/src/task-analysis/utils/ast.js +43 -0
  44. package/src/ui/client/adapters/job-adapter.js +60 -0
  45. package/src/ui/client/api.js +233 -8
  46. package/src/ui/client/hooks/useAnalysisProgress.js +145 -0
  47. package/src/ui/client/hooks/useJobList.js +14 -1
  48. package/src/ui/client/index.css +64 -0
  49. package/src/ui/client/main.jsx +4 -0
  50. package/src/ui/client/sse-fetch.js +120 -0
  51. package/src/ui/dist/app.js +262 -0
  52. package/src/ui/dist/assets/index-cjHV9mYW.js +82578 -0
  53. package/src/ui/dist/assets/index-cjHV9mYW.js.map +1 -0
  54. package/src/ui/dist/assets/style-CoM9SoQF.css +180 -0
  55. package/src/ui/dist/favicon.svg +12 -0
  56. package/src/ui/dist/index.html +2 -2
  57. package/src/ui/endpoints/create-pipeline-endpoint.js +194 -0
  58. package/src/ui/endpoints/file-endpoints.js +330 -0
  59. package/src/ui/endpoints/job-control-endpoints.js +1001 -0
  60. package/src/ui/endpoints/job-endpoints.js +62 -0
  61. package/src/ui/endpoints/pipeline-analysis-endpoint.js +246 -0
  62. package/src/ui/endpoints/pipeline-type-detail-endpoint.js +181 -0
  63. package/src/ui/endpoints/pipelines-endpoint.js +133 -0
  64. package/src/ui/endpoints/schema-file-endpoint.js +105 -0
  65. package/src/ui/endpoints/sse-endpoints.js +223 -0
  66. package/src/ui/endpoints/state-endpoint.js +85 -0
  67. package/src/ui/endpoints/task-analysis-endpoint.js +104 -0
  68. package/src/ui/endpoints/task-creation-endpoint.js +114 -0
  69. package/src/ui/endpoints/task-save-endpoint.js +101 -0
  70. package/src/ui/endpoints/upload-endpoints.js +406 -0
  71. package/src/ui/express-app.js +227 -0
  72. package/src/ui/lib/analysis-lock.js +67 -0
  73. package/src/ui/lib/sse.js +30 -0
  74. package/src/ui/server.js +42 -1880
  75. package/src/ui/sse-broadcast.js +93 -0
  76. package/src/ui/utils/http-utils.js +139 -0
  77. package/src/ui/utils/mime-types.js +196 -0
  78. package/src/ui/utils/slug.js +31 -0
  79. package/src/ui/vite.config.js +22 -0
  80. package/src/ui/watcher.js +28 -2
  81. package/src/utils/jobs.js +39 -0
  82. package/src/ui/dist/assets/index-DeDzq-Kk.js +0 -23863
  83. package/src/ui/dist/assets/style-aBtD_Yrs.css +0 -62
@@ -1,7 +1,8 @@
1
- import React, { useEffect, useRef, useState } from "react";
2
- import { Callout } from "@radix-ui/themes";
1
+ import React, { useState, useEffect } from "react";
2
+ import { Callout, Table, Text, Box } from "@radix-ui/themes";
3
3
  import { TaskFilePane } from "./TaskFilePane.jsx";
4
4
  import { TaskState } from "../config/statuses.js";
5
+ import { Sidebar, SidebarSection } from "./ui/sidebar.jsx";
5
6
 
6
7
  /**
7
8
  * TaskDetailSidebar component for displaying task details in a slide-over panel
@@ -12,9 +13,11 @@ import { TaskState } from "../config/statuses.js";
12
13
  * @param {string} props.jobId - Job ID for file operations
13
14
  * @param {string} props.taskId - Task ID for file operations
14
15
  * @param {string|null} props.taskBody - Task body for error callout when status is FAILED
16
+ * @param {Object} props.taskError - Error object with message and stack
15
17
  * @param {Function} props.filesByTypeForItem - Selector returning { artifacts, logs, tmp }
16
18
  * @param {Object} props.task - Original task item, passed for filesByTypeForItem
17
19
  * @param {Function} props.onClose - Close handler
20
+ * @param {number} props.taskIndex - Task index for ID compatibility
18
21
  */
19
22
  export function TaskDetailSidebar({
20
23
  open,
@@ -27,36 +30,28 @@ export function TaskDetailSidebar({
27
30
  filesByTypeForItem = () => ({ artifacts: [], logs: [], tmp: [] }),
28
31
  task,
29
32
  onClose,
30
- taskIndex, // Add taskIndex for ID compatibility
33
+ taskIndex: _taskIndex, // eslint-disable-line no-unused-vars
31
34
  }) {
32
35
  // Internal state
33
36
  const [filePaneType, setFilePaneType] = useState("artifacts");
34
37
  const [filePaneOpen, setFilePaneOpen] = useState(false);
35
38
  const [filePaneFilename, setFilePaneFilename] = useState(null);
36
39
  const [showStack, setShowStack] = useState(false);
37
- const closeButtonRef = useRef(null);
38
40
 
39
41
  // Get CSS classes for card header based on status (mirrored from DAGGrid)
40
42
  const getHeaderClasses = (status) => {
41
43
  switch (status) {
42
44
  case TaskState.DONE:
43
- return "bg-green-50 border-green-200 text-green-700";
45
+ return "bg-success/10 border-success/30 text-success-foreground";
44
46
  case TaskState.RUNNING:
45
- return "bg-amber-50 border-amber-200 text-amber-700";
47
+ return "bg-warning/10 border-warning/30 text-warning-foreground";
46
48
  case TaskState.FAILED:
47
- return "bg-pink-50 border-pink-200 text-pink-700";
49
+ return "bg-error/10 border-error/30 text-error-foreground";
48
50
  default:
49
- return "bg-gray-100 border-gray-200 text-gray-700";
51
+ return "bg-muted/50 border-input text-foreground";
50
52
  }
51
53
  };
52
54
 
53
- // Focus close button when sidebar opens
54
- useEffect(() => {
55
- if (open && closeButtonRef.current) {
56
- closeButtonRef.current.focus();
57
- }
58
- }, [open]);
59
-
60
55
  // Reset internal state when open changes
61
56
  useEffect(() => {
62
57
  if (open) {
@@ -93,100 +88,80 @@ export function TaskDetailSidebar({
93
88
  const filesForTab = filesForStep[filePaneType] ?? [];
94
89
 
95
90
  return (
96
- <aside
97
- role="dialog"
98
- aria-modal="true"
99
- aria-labelledby={`slide-over-title-${taskIndex}`}
100
- aria-hidden={false}
101
- className={`fixed inset-y-0 right-0 z-[2000] w-full max-w-4xl bg-white border-l border-gray-200 transform transition-transform duration-300 ease-out translate-x-0`}
102
- >
103
- {/* Header */}
104
- <div
105
- className={`px-6 py-4 border-b flex items-center justify-between ${getHeaderClasses(status)}`}
91
+ <>
92
+ <Sidebar
93
+ open={open}
94
+ onOpenChange={(isOpen) => !isOpen && onClose()}
95
+ title={title}
96
+ headerClassName={getHeaderClasses(status)}
106
97
  >
107
- <div
108
- id={`slide-over-title-${taskIndex}`}
109
- className="text-lg font-semibold truncate"
110
- >
111
- {title}
112
- </div>
113
- <button
114
- ref={closeButtonRef}
115
- type="button"
116
- aria-label="Close details"
117
- onClick={onClose}
118
- className="rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 px-3 py-1.5 text-base"
119
- >
120
- ×
121
- </button>
122
- </div>
123
-
124
- <div className="p-6 space-y-8 overflow-y-auto h-full">
125
98
  {/* Error Callout - shown when task has error status */}
126
99
  {status === TaskState.FAILED && (taskError?.message || taskBody) && (
127
- <section aria-label="Error">
128
- <Callout.Root role="alert" aria-live="assertive">
129
- <Callout.Text className="whitespace-pre-wrap break-words">
130
- {taskError?.message || taskBody}
131
- </Callout.Text>
132
- </Callout.Root>
133
-
134
- {/* Stack trace toggle */}
135
- {taskError?.stack && (
136
- <div className="mt-3">
137
- <button
138
- onClick={() => setShowStack(!showStack)}
139
- className="text-sm text-blue-600 hover:text-blue-800 underline"
140
- aria-expanded={showStack}
141
- aria-controls="error-stack"
142
- >
143
- {showStack ? "Hide stack" : "Show stack"}
144
- </button>
145
- {showStack && (
146
- <pre
147
- id="error-stack"
148
- className="mt-2 p-2 bg-gray-50 border rounded text-xs font-mono max-h-64 overflow-auto whitespace-pre-wrap"
100
+ <SidebarSection className="bg-destructive/5 border-b">
101
+ <section aria-label="Error">
102
+ <Callout.Root role="alert" aria-live="assertive">
103
+ <Callout.Text className="whitespace-pre-wrap break-words">
104
+ {taskError?.message || taskBody}
105
+ </Callout.Text>
106
+ </Callout.Root>
107
+
108
+ {/* Stack trace toggle */}
109
+ {taskError?.stack && (
110
+ <div className="mt-3">
111
+ <button
112
+ onClick={() => setShowStack(!showStack)}
113
+ className="text-sm text-primary hover:text-primary/80 underline"
114
+ aria-expanded={showStack}
115
+ aria-controls="error-stack"
149
116
  >
150
- {taskError.stack}
151
- </pre>
152
- )}
153
- </div>
154
- )}
155
- </section>
117
+ {showStack ? "Hide stack" : "Show stack"}
118
+ </button>
119
+ {showStack && (
120
+ <pre
121
+ id="error-stack"
122
+ className="mt-2 p-2 bg-muted border rounded text-xs font-mono max-h-64 overflow-auto whitespace-pre-wrap"
123
+ >
124
+ {taskError.stack}
125
+ </pre>
126
+ )}
127
+ </div>
128
+ )}
129
+ </section>
130
+ </SidebarSection>
156
131
  )}
157
132
 
158
133
  {/* File Display Area with Type Tabs */}
159
- <section className="mt-6">
134
+ <SidebarSection>
160
135
  <div className="flex items-center justify-between mb-4">
161
- <h3 className="text-base font-semibold text-gray-900">Files</h3>
136
+ <h3 className="text-base font-semibold text-foreground">Files</h3>
162
137
  <div className="flex items-center space-x-2">
163
- <div className="flex rounded-lg border border-gray-200 bg-gray-50 p-1">
138
+ <div className="flex rounded-lg border border-input bg-muted p-1">
164
139
  <button
165
140
  onClick={() => setFilePaneType("artifacts")}
166
- className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
141
+ className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors border-l-2 ${
167
142
  filePaneType === "artifacts"
168
- ? "bg-white text-gray-900 shadow-sm"
169
- : "text-gray-600 hover:text-gray-900"
143
+ ? "bg-background text-foreground shadow-sm border-indigo-400"
144
+ : "text-muted-foreground hover:text-foreground border-transparent"
170
145
  }`}
171
146
  >
172
147
  Artifacts
173
148
  </button>
174
149
  <button
175
150
  onClick={() => setFilePaneType("logs")}
176
- className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
151
+ className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors border-l-2 ${
177
152
  filePaneType === "logs"
178
- ? "bg-white text-gray-900 shadow-sm"
179
- : "text-gray-600 hover:text-gray-900"
153
+ ? "bg-background text-foreground shadow-sm border-indigo-400"
154
+ : "text-muted-foreground hover:text-foreground border-transparent"
180
155
  }`}
181
156
  >
182
157
  Logs
183
158
  </button>
184
159
  <button
185
160
  onClick={() => setFilePaneType("tmp")}
186
- className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
161
+ className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors border-l-2 ${
187
162
  filePaneType === "tmp"
188
- ? "bg-white text-gray-900 shadow-sm"
189
- : "text-gray-600 hover:text-gray-900"
163
+ ? "bg-background text-foreground shadow-sm border-indigo-400"
164
+ : "text-muted-foreground hover:text-foreground border-transparent"
190
165
  }`}
191
166
  >
192
167
  Temp
@@ -194,46 +169,73 @@ export function TaskDetailSidebar({
194
169
  </div>
195
170
  </div>
196
171
  </div>
197
- </section>
198
172
 
199
- {/* File List */}
200
- <div className="space-y-2">
201
- <div className="text-sm text-gray-600">
202
- {filePaneType.charAt(0).toUpperCase() + filePaneType.slice(1)} files
203
- for {taskId}
204
- </div>
205
- <div className="space-y-1">
173
+ {/* File List Table */}
174
+ <div className="space-y-2">
175
+ <Text size="2" className="text-muted-foreground">
176
+ {filePaneType.charAt(0).toUpperCase() + filePaneType.slice(1)}{" "}
177
+ files for {taskId}
178
+ </Text>
206
179
  {filesForTab.length === 0 ? (
207
- <div className="text-sm text-gray-500 italic py-4 text-center">
208
- No {filePaneType} files available for this task
209
- </div>
180
+ <Box className="py-4 text-center">
181
+ <Text size="2" className="text-muted-foreground italic">
182
+ No {filePaneType} files available for this task
183
+ </Text>
184
+ </Box>
210
185
  ) : (
211
- filesForTab.map((name) => (
212
- <div
213
- key={`${filePaneType}-${name}`}
214
- className="flex items-center justify-between p-2 rounded border border-gray-200 hover:border-gray-300 hover:bg-gray-50 cursor-pointer transition-colors"
215
- onClick={() => handleFileClick(name)}
216
- >
217
- <div className="flex items-center space-x-2">
218
- <span className="text-sm text-gray-700">{name}</span>
219
- </div>
220
- </div>
221
- ))
186
+ <Table.Root radius="none">
187
+ <Table.Header>
188
+ <Table.Row>
189
+ <Table.ColumnHeaderCell>File Name</Table.ColumnHeaderCell>
190
+ </Table.Row>
191
+ </Table.Header>
192
+ <Table.Body>
193
+ {filesForTab.map((name) => (
194
+ <Table.Row
195
+ key={`${filePaneType}-${name}`}
196
+ className="cursor-pointer hover:bg-slate-50/50"
197
+ onClick={() => handleFileClick(name)}
198
+ >
199
+ <Table.Cell>
200
+ <Text size="2">{name}</Text>
201
+ </Table.Cell>
202
+ </Table.Row>
203
+ ))}
204
+ </Table.Body>
205
+ </Table.Root>
222
206
  )}
223
207
  </div>
224
- </div>
225
208
 
226
- {/* TaskFilePane Modal */}
227
- <TaskFilePane
228
- isOpen={filePaneOpen}
229
- jobId={jobId}
230
- taskId={taskId}
231
- type={filePaneType}
232
- filename={filePaneFilename}
233
- onClose={handleFilePaneClose}
234
- />
235
- </div>
236
- </aside>
209
+ {/* Inline File Preview */}
210
+ {filePaneOpen && (
211
+ <div className="mt-4 border-t pt-4">
212
+ <div className="flex items-center justify-between mb-2">
213
+ <Text size="2" weight="medium" className="text-foreground">
214
+ File Preview
215
+ </Text>
216
+ <button
217
+ onClick={handleFilePaneClose}
218
+ className="text-sm text-muted-foreground hover:text-foreground"
219
+ >
220
+ Close Preview
221
+ </button>
222
+ </div>
223
+ <div className="h-96 overflow-auto">
224
+ <TaskFilePane
225
+ isOpen={filePaneOpen}
226
+ jobId={jobId}
227
+ taskId={taskId}
228
+ type={filePaneType}
229
+ filename={filePaneFilename}
230
+ onClose={handleFilePaneClose}
231
+ inline={true}
232
+ />
233
+ </div>
234
+ </div>
235
+ )}
236
+ </SidebarSection>
237
+ </Sidebar>
238
+ </>
237
239
  );
238
240
  }
239
241
 
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect, useRef } from "react";
2
+ import { Button } from "./ui/button.jsx";
2
3
 
3
4
  /**
4
5
  * TaskFilePane component for displaying a single task file with preview
@@ -9,6 +10,7 @@ import React, { useState, useEffect, useRef } from "react";
9
10
  * @param {string} props.type - File type (artifacts|logs|tmp)
10
11
  * @param {string} props.filename - File name to display
11
12
  * @param {Function} props.onClose - Close handler
13
+ * @param {boolean} props.inline - Whether to render inline (vs modal)
12
14
  */
13
15
  export function TaskFilePane({
14
16
  isOpen,
@@ -17,6 +19,7 @@ export function TaskFilePane({
17
19
  type,
18
20
  filename,
19
21
  onClose,
22
+ inline = false,
20
23
  }) {
21
24
  const [copyNotice, setCopyNotice] = useState(null);
22
25
  const [loading, setLoading] = useState(false);
@@ -364,12 +367,14 @@ export function TaskFilePane({
364
367
  <p className="mt-1 text-sm text-red-700">
365
368
  {error.error?.message || "Unknown error"}
366
369
  </p>
367
- <button
370
+ <Button
371
+ variant="ghost"
372
+ size="sm"
368
373
  onClick={handleRetry}
369
- className="mt-2 text-sm text-red-600 hover:text-red-800 underline"
374
+ className="mt-2 text-red-600 hover:text-red-800"
370
375
  >
371
376
  Retry
372
- </button>
377
+ </Button>
373
378
  </div>
374
379
  </div>
375
380
  </div>
@@ -493,12 +498,18 @@ export function TaskFilePane({
493
498
  return null;
494
499
  }
495
500
 
501
+ const containerClassName = inline
502
+ ? "flex flex-col h-full"
503
+ : "bg-white rounded-lg shadow-xl w-full max-w-6xl max-h-[90vh] flex flex-col";
504
+
496
505
  return (
497
- <div className="bg-white rounded-lg shadow-xl w-full max-w-6xl max-h-[90vh] flex flex-col">
506
+ <div className={containerClassName}>
498
507
  {/* Header */}
499
- <div className="flex items-center justify-between p-4 border-b">
508
+ <div className="flex items-center justify-between p-4 border-b bg-white">
500
509
  <div>
501
- <h2 className="text-lg font-semibold">File Preview</h2>
510
+ <h2 className="text-lg font-semibold">
511
+ {inline ? "File Preview" : filename}
512
+ </h2>
502
513
  </div>
503
514
  <button
504
515
  ref={closeButtonRef}
@@ -523,44 +534,88 @@ export function TaskFilePane({
523
534
  </div>
524
535
 
525
536
  {/* Preview */}
526
- <div className="flex-1 flex flex-col bg-gray-50">
527
- {/* Preview Header */}
528
- <div className="bg-white border-b p-4">
529
- <div className="flex items-center justify-between">
530
- <div>
531
- <h3 className="font-medium">{filename}</h3>
532
- <div className="flex items-center text-sm text-gray-500 mt-1">
533
- {size && <span>{formatSize(size)}</span>}
534
- {size && mtime && <span className="mx-1">•</span>}
535
- {mtime && <span>{formatDate(mtime)}</span>}
536
- {mime && (size || mtime) && <span className="mx-1">•</span>}
537
- {mime && <span>{mime}</span>}
537
+ <div className="flex-1 flex flex-col bg-gray-50 overflow-hidden">
538
+ {/* Preview Header - only show filename in non-inline mode */}
539
+ {!inline && (
540
+ <div className="bg-white border-b p-4">
541
+ <div className="flex items-center justify-between">
542
+ <div>
543
+ <h3 className="font-medium">{filename}</h3>
544
+ <div className="flex items-center text-sm text-gray-500 mt-1">
545
+ {size && <span>{formatSize(size)}</span>}
546
+ {size && mtime && <span className="mx-1">•</span>}
547
+ {mtime && <span>{formatDate(mtime)}</span>}
548
+ {mime && (size || mtime) && <span className="mx-1">•</span>}
549
+ {mime && <span>{mime}</span>}
550
+ </div>
551
+ </div>
552
+ <div className="flex items-center space-x-2">
553
+ {copyNotice && (
554
+ <div
555
+ className={`text-sm ${
556
+ copyNotice.type === "success"
557
+ ? "text-green-600"
558
+ : "text-red-600"
559
+ }`}
560
+ >
561
+ {copyNotice.message}
562
+ </div>
563
+ )}
564
+ {content && encoding === "utf8" && (
565
+ <Button
566
+ variant="solid"
567
+ size="sm"
568
+ onClick={handleCopy}
569
+ aria-label="Copy content to clipboard"
570
+ >
571
+ Copy
572
+ </Button>
573
+ )}
538
574
  </div>
539
575
  </div>
540
- <div className="flex items-center space-x-2">
541
- {copyNotice && (
542
- <div
543
- className={`text-sm ${
544
- copyNotice.type === "success"
545
- ? "text-green-600"
546
- : "text-red-600"
547
- }`}
548
- >
549
- {copyNotice.message}
576
+ </div>
577
+ )}
578
+
579
+ {/* Inline preview header with filename and actions */}
580
+ {inline && (
581
+ <div className="bg-white border-b p-3">
582
+ <div className="flex items-center justify-between">
583
+ <div>
584
+ <h3 className="font-medium text-sm">{filename}</h3>
585
+ <div className="flex items-center text-xs text-gray-500 mt-1">
586
+ {size && <span>{formatSize(size)}</span>}
587
+ {size && mtime && <span className="mx-1">•</span>}
588
+ {mtime && <span>{formatDate(mtime)}</span>}
589
+ {mime && (size || mtime) && <span className="mx-1">•</span>}
590
+ {mime && <span>{mime}</span>}
550
591
  </div>
551
- )}
552
- {content && encoding === "utf8" && (
553
- <button
554
- onClick={handleCopy}
555
- className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
556
- aria-label="Copy content to clipboard"
557
- >
558
- Copy
559
- </button>
560
- )}
592
+ </div>
593
+ <div className="flex items-center space-x-2">
594
+ {copyNotice && (
595
+ <div
596
+ className={`text-xs ${
597
+ copyNotice.type === "success"
598
+ ? "text-green-600"
599
+ : "text-red-600"
600
+ }`}
601
+ >
602
+ {copyNotice.message}
603
+ </div>
604
+ )}
605
+ {content && encoding === "utf8" && (
606
+ <Button
607
+ variant="solid"
608
+ size="sm"
609
+ onClick={handleCopy}
610
+ aria-label="Copy content to clipboard"
611
+ >
612
+ Copy
613
+ </Button>
614
+ )}
615
+ </div>
561
616
  </div>
562
617
  </div>
563
- </div>
618
+ )}
564
619
 
565
620
  {/* Preview Content */}
566
621
  <div className="flex-1 overflow-auto">{renderContent()}</div>
@@ -7,7 +7,7 @@ import { Button } from "./button.jsx";
7
7
  * @param {Object} props
8
8
  * @param {boolean} props.open - Whether the modal is open
9
9
  * @param {Function} props.onClose - Function to call when modal is closed
10
- * @param {Function} props.onConfirm - Function to call when restart is confirmed
10
+ * @param {Function} props.onConfirm - Function to call when restart is confirmed (receives options object)
11
11
  * @param {string} props.jobId - The ID of the job to restart
12
12
  * @param {string} props.taskId - The ID of the task that triggered the restart (optional)
13
13
  * @param {boolean} props.isSubmitting - Whether the restart action is in progress
@@ -47,7 +47,10 @@ export function RestartJobModal({
47
47
  const handleKeyDown = (e) => {
48
48
  if (e.key === "Enter" && !isSubmitting && open) {
49
49
  e.preventDefault();
50
- onConfirm();
50
+ // Do not confirm via Enter when a task is set; let the user click explicitly
51
+ if (!taskId) {
52
+ onConfirm({ singleTask: false });
53
+ }
51
54
  }
52
55
  };
53
56
 
@@ -100,9 +103,15 @@ export function RestartJobModal({
100
103
  </Text>
101
104
 
102
105
  {taskId && (
103
- <Text as="p" className="text-sm text-gray-600 mb-3">
104
- <strong>Triggered from task:</strong> {taskId}
105
- </Text>
106
+ <>
107
+ <Text as="p" className="text-sm text-gray-600 mb-3">
108
+ <strong>Triggered from task:</strong> {taskId}
109
+ </Text>
110
+ <Text as="p" className="text-sm text-blue-600 mb-3">
111
+ <strong>Just this task:</strong> Only the selected task will
112
+ be reset and re-run. Other tasks remain unchanged.
113
+ </Text>
114
+ </>
106
115
  )}
107
116
 
108
117
  <Text as="p" className="text-sm text-gray-500 italic">
@@ -121,9 +130,20 @@ export function RestartJobModal({
121
130
  Cancel
122
131
  </Button>
123
132
 
133
+ {taskId && (
134
+ <Button
135
+ variant="outline"
136
+ onClick={() => onConfirm({ singleTask: true })}
137
+ disabled={isSubmitting}
138
+ className="min-w-[120px]"
139
+ >
140
+ {isSubmitting ? "Running..." : "Just this task"}
141
+ </Button>
142
+ )}
143
+
124
144
  <Button
125
145
  variant="destructive"
126
- onClick={onConfirm}
146
+ onClick={() => onConfirm({ singleTask: false })}
127
147
  disabled={isSubmitting}
128
148
  className="min-w-[80px]"
129
149
  >