bosun 0.40.21 → 0.41.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 (41) hide show
  1. package/agent/agent-custom-tools.mjs +23 -5
  2. package/agent/agent-pool.mjs +6 -2
  3. package/agent/primary-agent.mjs +81 -7
  4. package/bench/swebench/bosun-swebench.mjs +5 -0
  5. package/cli.mjs +208 -3
  6. package/config/config-doctor.mjs +51 -2
  7. package/config/config.mjs +103 -3
  8. package/github/github-auth-manager.mjs +70 -19
  9. package/infra/library-manager.mjs +894 -60
  10. package/infra/monitor.mjs +8 -2
  11. package/infra/session-tracker.mjs +13 -3
  12. package/infra/test-runtime.mjs +267 -0
  13. package/package.json +8 -5
  14. package/server/setup-web-server.mjs +4 -1
  15. package/server/ui-server.mjs +1323 -20
  16. package/task/task-claims.mjs +6 -10
  17. package/ui/components/chat-view.js +18 -1
  18. package/ui/components/workspace-switcher.js +321 -9
  19. package/ui/demo-defaults.js +11746 -9470
  20. package/ui/demo.html +9 -1
  21. package/ui/modules/router.js +1 -1
  22. package/ui/modules/voice-client-sdk.js +1 -1
  23. package/ui/modules/voice-client.js +33 -2
  24. package/ui/styles/components.css +514 -1
  25. package/ui/tabs/library.js +410 -55
  26. package/ui/tabs/tasks.js +1052 -506
  27. package/ui/tabs/workflow-canvas-utils.mjs +30 -0
  28. package/ui/tabs/workflows.js +914 -298
  29. package/voice/voice-agents-sdk.mjs +1 -1
  30. package/voice/voice-relay.mjs +24 -16
  31. package/workflow/project-detection.mjs +559 -0
  32. package/workflow/workflow-contract.mjs +433 -232
  33. package/workflow/workflow-engine.mjs +181 -30
  34. package/workflow/workflow-nodes.mjs +304 -6
  35. package/workflow/workflow-templates.mjs +92 -16
  36. package/workflow-templates/agents.mjs +20 -19
  37. package/workflow-templates/code-quality.mjs +20 -14
  38. package/workflow-templates/task-batch.mjs +3 -2
  39. package/workflow-templates/task-execution.mjs +752 -0
  40. package/workflow-templates/task-lifecycle.mjs +34 -8
  41. package/workspace/workspace-manager.mjs +151 -0
package/ui/tabs/tasks.js CHANGED
@@ -2406,6 +2406,43 @@ export function TaskDetailModal({ task, onClose, onStart, presentation = "modal"
2406
2406
  task?.workflowHistory,
2407
2407
  task?.workflows,
2408
2408
  ]);
2409
+
2410
+ // ── Execution Plan state ──────────────────────────────────────────────────
2411
+ const [executionPlan, setExecutionPlan] = useState(null);
2412
+ const [executionPlanLoading, setExecutionPlanLoading] = useState(false);
2413
+ const [expandedNodes, setExpandedNodes] = useState({});
2414
+ const [expandedStages, setExpandedStages] = useState({});
2415
+ const [dryRunLoading, setDryRunLoading] = useState(false);
2416
+ const [dryRunResults, setDryRunResults] = useState(null);
2417
+ const [fullScreen, setFullScreen] = useState(false);
2418
+ const [activeTab, setActiveTab] = useState("details");
2419
+
2420
+ const fetchExecutionPlan = useCallback((mode = "resolve") => {
2421
+ if (!task?.id) return;
2422
+ if (mode === "resolve") { setExecutionPlan(null); setExecutionPlanLoading(true); }
2423
+ else { setDryRunLoading(true); }
2424
+ const wsParam = typeof window !== "undefined" && window.__bosunWorkspaceId ? `&workspace=${encodeURIComponent(window.__bosunWorkspaceId)}` : "";
2425
+ fetch(`/api/tasks/execution-plan?taskId=${encodeURIComponent(task.id)}${wsParam}&mode=${mode}`)
2426
+ .then((r) => r.json())
2427
+ .then((data) => {
2428
+ if (data?.ok) {
2429
+ if (mode === "resolve") setExecutionPlan(data);
2430
+ else { setExecutionPlan(data); setDryRunResults(data.dryRunResults || null); }
2431
+ }
2432
+ })
2433
+ .catch(() => {})
2434
+ .finally(() => { setExecutionPlanLoading(false); setDryRunLoading(false); });
2435
+ }, [task?.id]);
2436
+
2437
+ useEffect(() => { fetchExecutionPlan("resolve"); }, [task?.id]);
2438
+
2439
+ const toggleNodeExpand = useCallback((stageIdx, nodeId) => {
2440
+ setExpandedNodes((prev) => ({ ...prev, [`${stageIdx}-${nodeId}`]: !prev[`${stageIdx}-${nodeId}`] }));
2441
+ }, []);
2442
+ const toggleStageExpand = useCallback((stageIdx) => {
2443
+ setExpandedStages((prev) => ({ ...prev, [stageIdx]: !prev[stageIdx] }));
2444
+ }, []);
2445
+
2409
2446
  const relatedLinks = useMemo(() => buildTaskRelatedLinks(task), [
2410
2447
  task?.id,
2411
2448
  task?.branch,
@@ -3055,9 +3092,11 @@ export function TaskDetailModal({ task, onClose, onStart, presentation = "modal"
3055
3092
  <${Modal}
3056
3093
  title=${task?.title || "Task Detail"}
3057
3094
  onClose=${onClose}
3058
- contentClassName=${"modal-content-wide task-detail-modal-jira" + (presentation === "side-sheet" ? " task-detail-side-sheet" : "")}
3059
- layout=${presentation === "side-sheet" ? "side-sheet" : "sheet"}
3060
- resizable=${presentation === "side-sheet"}
3095
+ contentClassName=${fullScreen
3096
+ ? "task-detail-fullscreen"
3097
+ : "modal-content-wide task-detail-modal-jira" + (presentation === "side-sheet" ? " task-detail-side-sheet" : "")}
3098
+ layout=${fullScreen ? "sheet" : (presentation === "side-sheet" ? "side-sheet" : "sheet")}
3099
+ resizable=${!fullScreen && presentation === "side-sheet"}
3061
3100
  widthStorageKey="tasks.task-detail.width"
3062
3101
  defaultWidth=${900}
3063
3102
  unsavedChanges=${changeCount}
@@ -3068,577 +3107,674 @@ export function TaskDetailModal({ task, onClose, onStart, presentation = "modal"
3068
3107
  }}
3069
3108
  activeOperationLabel=${activeOperationLabel}
3070
3109
  >
3071
- <div class="task-modal-summary jira-task-summary">
3072
- <div class="task-modal-id" style="user-select:all">ID: ${task?.id}</div>
3073
- <div class="task-modal-badges">
3074
- <${Badge} status=${task?.status} text=${task?.status} />
3075
- ${task?.priority &&
3076
- html`<${Badge} status=${task.priority} text=${task.priority} />`}
3077
- ${manualOverride && html`<${Badge} status="warning" text="manual" />`}
3110
+ ${/* ── Breadcrumb ── */ ""}
3111
+ <div class="task-detail-breadcrumb">
3112
+ <span>Tasks</span>
3113
+ <span>/</span>
3114
+ <span style="color:var(--color-text);font-weight:500;user-select:all;">${task?.id?.slice(0, 8) || "New"}</span>
3115
+ ${task?.priority && html`<span class="task-priority-dot" data-priority=${task.priority}></span>`}
3116
+ ${manualOverride && html`<span class="exec-plan-badge" style="background:#fbbf2420;color:#fbbf24;">MANUAL</span>`}
3117
+ </div>
3118
+
3119
+ ${/* ── Title + Actions ── */ ""}
3120
+ <div class="task-detail-title-area" style="display:flex;gap:12px;align-items:flex-start;">
3121
+ <div style="flex:1;min-width:0;">
3122
+ <input class="task-detail-title-input" value=${title} onInput=${(e) => setTitle(e.target.value)} placeholder="Task title" />
3078
3123
  </div>
3079
- ${canDispatch &&
3080
- html`
3081
- <div class="task-modal-actions">
3124
+ <div style="display:flex;gap:6px;align-items:center;padding-top:6px;flex-shrink:0;">
3125
+ <button class="task-status-btn" data-status=${status}>
3126
+ ${(status || "todo").toUpperCase()}
3127
+ </button>
3128
+ ${canDispatch && html`
3082
3129
  <${Button} variant="contained" size="small" onClick=${handleStart}>
3083
- ${iconText(":play: Dispatch Task")}
3130
+ ${iconText(":play: Dispatch")}
3084
3131
  <//>
3085
- </div>
3086
- `}
3132
+ `}
3133
+ <button class="task-action-icon-btn"
3134
+ onClick=${() => setFullScreen(!fullScreen)}
3135
+ title=${fullScreen ? "Exit fullscreen" : "Fullscreen"}>
3136
+ ${fullScreen ? resolveIcon("minimize") || "⊟" : resolveIcon("maximize") || "⊞"}
3137
+ </button>
3138
+ </div>
3087
3139
  </div>
3088
3140
 
3141
+ ${/* ── Tab Bar (Jira style) ── */ ""}
3142
+ <div class="task-tab-bar">
3143
+ <button class="task-tab-btn" data-active=${activeTab === "details"} onClick=${() => setActiveTab("details")}>
3144
+ ${resolveIcon("edit") || "✎"} Details
3145
+ </button>
3146
+ <button class="task-tab-btn" data-active=${activeTab === "execution"} onClick=${() => setActiveTab("execution")}>
3147
+ ${resolveIcon("play")} Execution Plan
3148
+ ${executionPlan?.stageCount > 0 && html`<span class="task-tab-count">${executionPlan.stageCount}</span>`}
3149
+ </button>
3150
+ <button class="task-tab-btn" data-active=${activeTab === "history"} onClick=${() => setActiveTab("history")}>
3151
+ ${resolveIcon("clock") || "⏱"} History
3152
+ ${historyEntries.length > 0 && html`<span class="task-tab-count">${historyEntries.length}</span>`}
3153
+ </button>
3154
+ </div>
3089
3155
 
3090
- <div class="modal-form-span">
3091
- <div class="task-comments-block jira-panel" style="padding:12px;border:1px solid var(--border);border-radius:12px;background:var(--bg-surface)">
3092
- <div class="task-attachments-title">Tracking Overview</div>
3093
- <div class="task-comments-list" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;">
3094
- <div class="task-comment-item">
3095
- <div class="task-comment-meta">Assigned Agents</div>
3096
- <div class="task-comment-body">${taskAgents.length ? taskAgents.join(" · ") : "No agent assignment recorded."}</div>
3097
- </div>
3098
- <div class="task-comment-item">
3099
- <div class="task-comment-meta">Workflow Runs</div>
3100
- <div class="task-comment-body">${workflowRuns.length ? `${workflowRuns.length} linked runs` : "No workflow runs linked yet."}</div>
3101
- </div>
3102
- <div class="task-comment-item">
3103
- <div class="task-comment-meta">Timeline Events</div>
3104
- <div class="task-comment-body">${historyEntries.length ? `${historyEntries.length} recorded entries` : "No timeline history yet."}</div>
3156
+ ${/* ── Content Body ───────────────────────────────────────────── */ ""}
3157
+ <div style="padding:${fullScreen ? '20px 24px' : '0'};overflow-y:auto;max-height:${fullScreen ? 'calc(100dvh - 140px)' : 'auto'};">
3158
+
3159
+ ${/* ── DETAILS TAB — Two-column Jira layout ─────────────────── */ ""}
3160
+ ${activeTab === "details" && html`<div class="task-detail-columns" style="max-height:${fullScreen ? 'calc(100dvh - 160px)' : '65vh'};overflow:hidden;">
3161
+
3162
+ ${/* ── LEFT: Main Content ── */ ""}
3163
+ <div class="task-detail-main">
3164
+
3165
+ ${/* Description */ ""}
3166
+ <div class="task-section">
3167
+ <div class="task-section-title">Description</div>
3168
+ <div class="task-section-body">
3169
+ <div class="textarea-with-mic" style="position:relative">
3170
+ <${TextField} multiline rows=${4} size="small" placeholder="Add a description..." value=${description} onInput=${(e) => setDescription(e.target.value)} style=${{ paddingRight: "36px" }} fullWidth />
3171
+ <${VoiceMicButton}
3172
+ onTranscript=${(t) => setDescription((prev) => (prev ? prev + " " + t : t))}
3173
+ disabled=${saving || rewriting}
3174
+ size="sm"
3175
+ className="textarea-mic-btn"
3176
+ />
3105
3177
  </div>
3106
- <div class="task-comment-item">
3107
- <div class="task-comment-meta">Branch / PR</div>
3108
- <div class="task-comment-body">${relatedLinks.length ? relatedLinks.map((item) => `${item.kind}: ${item.value}`).join(" · ") : "No branch or PR links recorded."}</div>
3178
+ <div style="display:flex;gap:6px;margin-top:8px;">
3179
+ <${Tooltip} title="Use AI to expand and improve this task description">
3180
+ <${Button}
3181
+ variant="text" size="small"
3182
+ style=${{ display: "flex", alignItems: "center", gap: "6px", fontSize: "12px", padding: "5px 10px", opacity: !title.trim() ? 0.45 : 1 }}
3183
+ disabled=${!title.trim() || rewriting || saving}
3184
+ onClick=${async () => {
3185
+ if (!title.trim() || rewriting) return;
3186
+ setRewriting(true);
3187
+ haptic("medium");
3188
+ try {
3189
+ const res = await apiFetch("/api/tasks/rewrite", {
3190
+ method: "POST",
3191
+ body: JSON.stringify({ title: title.trim(), description: description.trim() }),
3192
+ });
3193
+ if (res?.data) {
3194
+ if (res.data.title) setTitle(res.data.title);
3195
+ if (res.data.description) setDescription(res.data.description);
3196
+ showToast("Task description improved", "success");
3197
+ haptic("medium");
3198
+ }
3199
+ } catch { /* toast via apiFetch */ }
3200
+ setRewriting(false);
3201
+ }}
3202
+ >
3203
+ ${rewriting
3204
+ ? html`<span style="display:inline-block;animation:spin 0.8s linear infinite">${resolveIcon(":clock:")}</span> Improving…`
3205
+ : html`${iconText(":star: Improve with AI")}`
3206
+ }
3207
+ <//>
3208
+ <//>
3109
3209
  </div>
3110
3210
  </div>
3111
3211
  </div>
3112
- </div>
3113
3212
 
3114
- ${workflowRuns.length > 0 && html`
3115
- <div class="task-comments-block modal-form-span jira-panel">
3116
- <div class="task-attachments-title">Workflow Activity</div>
3117
- <div class="task-comments-list">
3118
- ${workflowRuns.map((run, index) => html`
3119
- <div class="task-comment-item" key=${`workflow-${index}`}>
3120
- <div class="task-comment-meta">
3121
- ${run.workflowId || "workflow"}
3122
- ${run.runId ? ` · run ${run.runId}` : ""}
3123
- ${run.timestamp ? ` · ${formatRelative(run.timestamp)}` : ""}
3124
- </div>
3125
- <div class="task-comment-body">${run.status || run.result || "No status summary"}</div>
3126
- ${run.result && run.status && run.result !== run.status && html`
3127
- <div class="task-comment-body">${run.result}</div>
3128
- `}
3213
+ ${/* Attachments */ ""}
3214
+ <div class="task-section" onPaste=${handleAttachmentPaste}>
3215
+ <div class="task-section-title">
3216
+ Attachments
3217
+ <span class="task-tab-count">${attachments.length}</span>
3218
+ <span style="margin-left:auto;">
3219
+ <${Button}
3220
+ variant="text" size="small"
3221
+ type="button"
3222
+ onClick=${() => attachmentInputRef.current && attachmentInputRef.current.click()}
3223
+ disabled=${uploadingAttachment}
3224
+ >
3225
+ Upload
3226
+ <//>
3227
+ </span>
3228
+ </div>
3229
+ <div class="task-section-body">
3230
+ <input
3231
+ ref=${attachmentInputRef}
3232
+ type="file"
3233
+ multiple
3234
+ style="display:none"
3235
+ onChange=${handleAttachmentPick}
3236
+ />
3237
+ ${attachments.length === 0 && !uploadingAttachment && html`
3238
+ <div class="meta-text">No attachments uploaded.</div>
3239
+ `}
3240
+ ${uploadingAttachment && html`
3241
+ <div class="meta-text">Uploading attachments...</div>
3242
+ `}
3243
+ ${attachments.length > 0 && html`
3244
+ <div class="task-attachments-list">
3245
+ ${attachments.map((att, index) => {
3246
+ const name = att.name || att.filename || "attachment";
3247
+ const url = att.url || att.filePath || att.path || "";
3248
+ const size = att.size ? formatBytes(att.size) : "";
3249
+ const isImage = isImageAttachment(att);
3250
+ return html`
3251
+ <div class="task-attachment-item" key=${att.id || `${name}-${index}`}>
3252
+ ${isImage && url
3253
+ ? html`<img class="task-attachment-thumb" src=${url} alt=${name} />`
3254
+ : html`<span class="task-attachment-icon">${resolveIcon(":link:")}</span>`}
3255
+ <div class="task-attachment-meta">
3256
+ ${url
3257
+ ? html`<a class="task-attachment-name" href=${url} target="_blank" rel="noopener">${name}</a>`
3258
+ : html`<span class="task-attachment-name">${name}</span>`}
3259
+ <div class="task-attachment-sub">
3260
+ ${(att.kind || "file")}${size ? ` · ${size}` : ""}
3261
+ </div>
3262
+ </div>
3263
+ </div>
3264
+ `;
3265
+ })}
3129
3266
  </div>
3130
- `)}
3267
+ `}
3131
3268
  </div>
3132
3269
  </div>
3133
- `}
3134
3270
 
3135
- ${historyEntries.length > 0 && html`
3136
- <div class="task-comments-block modal-form-span jira-panel">
3137
- <div class="task-attachments-title">History Timeline</div>
3138
- <div class="task-comments-list">
3139
- ${historyEntries.map((entry, index) => html`
3140
- <div class="task-comment-item" key=${`history-${index}`}>
3141
- <div class="task-comment-meta">
3142
- ${entry.timestamp ? formatRelative(entry.timestamp) : "Time unknown"}
3143
- ${entry.source ? ` · ${entry.source}` : ""}
3271
+ ${/* Subtasks */ ""}
3272
+ <div class="task-section">
3273
+ <div class="task-section-title">
3274
+ Subtasks
3275
+ ${subtasks.length > 0 && html`<span class="task-tab-count">${subtasks.length}</span>`}
3276
+ <span style="margin-left:auto;">
3277
+ <${Button}
3278
+ variant="text"
3279
+ size="small"
3280
+ onClick=${loadSubtasks}
3281
+ disabled=${subtasksLoading || creatingSubtask}
3282
+ >
3283
+ ${subtasksLoading ? "Refreshing…" : "Refresh"}
3284
+ <//>
3285
+ </span>
3286
+ </div>
3287
+ <div class="task-section-body">
3288
+ <div class="task-comment-composer" style=${{ marginBottom: "8px" }}>
3289
+ <${TextField}
3290
+ size="small"
3291
+ placeholder="Create subtask summary"
3292
+ value=${subtaskTitle}
3293
+ onInput=${(e) => setSubtaskTitle(e.target.value)}
3294
+ fullWidth
3295
+ />
3296
+ <${Button}
3297
+ variant="contained"
3298
+ size="small"
3299
+ disabled=${creatingSubtask || !sanitizeTaskText(subtaskTitle || "").trim()}
3300
+ onClick=${handleCreateSubtask}
3301
+ >
3302
+ ${creatingSubtask ? "Creating…" : "Add"}
3303
+ <//>
3304
+ </div>
3305
+ <div class="task-comments-list">
3306
+ ${!subtasksLoading && !subtasks.length && html`<div class="meta-text">No subtasks yet.</div>`}
3307
+ ${subtasks.map((subtask) => html`
3308
+ <div class="task-comment-item" key=${subtask.id}>
3309
+ <div class="task-comment-meta">
3310
+ <span style="user-select:all">${subtask.id}</span>
3311
+ ${subtask.status ? ` · ${subtask.status}` : ""}
3312
+ ${subtask.storyPoints ? ` · ${subtask.storyPoints} pts` : ""}
3313
+ </div>
3314
+ <div class="task-comment-body">${subtask.title}</div>
3315
+ ${subtask.assignee && html`<div class="task-comment-meta">Assignee: ${subtask.assignee}</div>`}
3144
3316
  </div>
3145
- <div class="task-comment-body">${entry.label}</div>
3146
- </div>
3147
- `)}
3317
+ `)}
3318
+ </div>
3148
3319
  </div>
3149
3320
  </div>
3150
- `}
3151
3321
 
3152
- ${relatedLinks.length > 0 && html`
3153
- <div class="task-comments-block modal-form-span jira-panel">
3154
- <div class="task-attachments-title">Branch and PR Links</div>
3155
- <div class="task-comments-list">
3156
- ${relatedLinks.map((item, index) => html`
3157
- <div class="task-comment-item" key=${`link-${index}`}>
3158
- <div class="task-comment-meta">${item.kind}</div>
3159
- <div class="task-comment-body">
3160
- ${item.url
3161
- ? html`<a href=${item.url} target="_blank" rel="noopener">${item.value}</a>`
3162
- : item.value}
3163
- </div>
3164
- </div>
3165
- `)}
3322
+ ${/* Comments & Updates */ ""}
3323
+ <div class="task-section">
3324
+ <div class="task-section-title">
3325
+ Comments & Updates
3326
+ ${comments.length > 0 && html`<span class="task-tab-count">${comments.length}</span>`}
3166
3327
  </div>
3167
- </div>
3168
- `} <div class="flex-col gap-md modal-form-grid jira-task-layout">
3169
- <div class="input-with-mic modal-form-span">
3170
- <${TextField} size="small" variant="outlined" placeholder="Title" value=${title} onInput=${(e) => setTitle(e.target.value)} fullWidth />
3171
- <${VoiceMicButtonInline}
3172
- onTranscript=${(t) => setTitle((prev) => (prev ? prev + " " + t : t))}
3173
- disabled=${saving || rewriting}
3174
- />
3175
- </div>
3176
- <div class="textarea-with-mic modal-form-span" style="position:relative">
3177
- <${TextField} multiline rows=${5} size="small" placeholder="Description" value=${description} onInput=${(e) => setDescription(e.target.value)} style=${{ paddingRight: "36px" }} fullWidth />
3178
- <${VoiceMicButton}
3179
- onTranscript=${(t) => setDescription((prev) => (prev ? prev + " " + t : t))}
3180
- disabled=${saving || rewriting}
3181
- size="sm"
3182
- className="textarea-mic-btn"
3183
- />
3184
- </div>
3185
- <div
3186
- class="task-attachments-block modal-form-span jira-panel"
3187
- onPaste=${handleAttachmentPaste}
3188
- >
3189
- <div class="task-attachments-header">
3190
- <div class="task-attachments-title">Attachments</div>
3191
- <div class="task-attachments-actions">
3328
+ <div class="task-section-body">
3329
+ <div class="task-comments-list">
3330
+ ${comments.length > 0
3331
+ ? comments.map((comment, index) => html`
3332
+ <div class="task-comment-item" key=${comment.id || `comment-${index}`}>
3333
+ <div class="task-comment-meta">
3334
+ ${comment.author ? `@${comment.author}` : "comment"}
3335
+ ${comment.createdAt ? ` · ${formatRelative(comment.createdAt)}` : ""}
3336
+ </div>
3337
+ <div class="task-comment-body">${comment.body}</div>
3338
+ </div>
3339
+ `)
3340
+ : html`<div class="meta-text">No comments yet. Add one below.</div>`}
3341
+ </div>
3342
+ <div class="task-comment-composer" style=${{ marginTop: "10px" }}>
3343
+ <${TextField}
3344
+ multiline
3345
+ rows=${2}
3346
+ size="small"
3347
+ placeholder="Add a comment or status update..."
3348
+ value=${commentDraft}
3349
+ onInput=${(e) => setCommentDraft(e.target.value)}
3350
+ fullWidth
3351
+ />
3192
3352
  <${Button}
3193
- variant="text" size="small"
3194
- type="button"
3195
- onClick=${() => attachmentInputRef.current && attachmentInputRef.current.click()}
3196
- disabled=${uploadingAttachment}
3353
+ variant="contained"
3354
+ size="small"
3355
+ disabled=${postingComment || !sanitizeTaskText(commentDraft || "").trim()}
3356
+ onClick=${handlePostComment}
3197
3357
  >
3198
- Upload
3358
+ ${postingComment ? "Posting…" : "Post Comment"}
3199
3359
  <//>
3200
3360
  </div>
3201
3361
  </div>
3202
- <input
3203
- ref=${attachmentInputRef}
3204
- type="file"
3205
- multiple
3206
- style="display:none"
3207
- onChange=${handleAttachmentPick}
3208
- />
3209
- ${attachments.length === 0 && !uploadingAttachment && html`
3210
- <div class="meta-text">No attachments uploaded.</div>
3211
- `}
3212
- ${uploadingAttachment && html`
3213
- <div class="meta-text">Uploading attachments...</div>
3214
- `}
3215
- ${attachments.length > 0 && html`
3216
- <div class="task-attachments-list">
3217
- ${attachments.map((att, index) => {
3218
- const name = att.name || att.filename || "attachment";
3219
- const url = att.url || att.filePath || att.path || "";
3220
- const size = att.size ? formatBytes(att.size) : "";
3221
- const isImage = isImageAttachment(att);
3222
- return html`
3223
- <div class="task-attachment-item" key=${att.id || `${name}-${index}`}>
3224
- ${isImage && url
3225
- ? html`<img class="task-attachment-thumb" src=${url} alt=${name} />`
3226
- : html`<span class="task-attachment-icon">${resolveIcon(":link:")}</span>`}
3227
- <div class="task-attachment-meta">
3228
- ${url
3229
- ? html`<a class="task-attachment-name" href=${url} target="_blank" rel="noopener">${name}</a>`
3230
- : html`<span class="task-attachment-name">${name}</span>`}
3231
- <div class="task-attachment-sub">
3232
- ${(att.kind || "file")}${size ? ` · ${size}` : ""}
3233
- </div>
3362
+ </div>
3363
+
3364
+ ${/* Tracking Overview */ ""}
3365
+ <div class="task-section">
3366
+ <div class="task-section-title">Tracking Overview</div>
3367
+ <div class="task-section-body">
3368
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;">
3369
+ <div class="task-comment-item">
3370
+ <div class="task-comment-meta">Assigned Agents</div>
3371
+ <div class="task-comment-body">${taskAgents.length ? taskAgents.join(" · ") : "No agent assignment recorded."}</div>
3372
+ </div>
3373
+ <div class="task-comment-item">
3374
+ <div class="task-comment-meta">Workflow Runs</div>
3375
+ <div class="task-comment-body">${workflowRuns.length ? `${workflowRuns.length} linked runs` : "No workflow runs linked yet."}</div>
3376
+ </div>
3377
+ <div class="task-comment-item">
3378
+ <div class="task-comment-meta">Timeline Events</div>
3379
+ <div class="task-comment-body">${historyEntries.length ? `${historyEntries.length} recorded entries` : "No timeline history yet."}</div>
3380
+ </div>
3381
+ <div class="task-comment-item">
3382
+ <div class="task-comment-meta">Branch / PR</div>
3383
+ <div class="task-comment-body">${relatedLinks.length ? relatedLinks.map((item) => `${item.kind}: ${item.value}`).join(" · ") : "No branch or PR links recorded."}</div>
3384
+ </div>
3385
+ </div>
3386
+ </div>
3387
+ </div>
3388
+
3389
+ ${/* Workflow Activity */ ""}
3390
+ ${workflowRuns.length > 0 && html`
3391
+ <div class="task-section">
3392
+ <div class="task-section-title">Workflow Activity</div>
3393
+ <div class="task-section-body">
3394
+ <div class="task-comments-list">
3395
+ ${workflowRuns.map((run, index) => html`
3396
+ <div class="task-comment-item" key=${`workflow-${index}`}>
3397
+ <div class="task-comment-meta">
3398
+ ${run.workflowId || "workflow"}
3399
+ ${run.runId ? ` · run ${run.runId}` : ""}
3400
+ ${run.timestamp ? ` · ${formatRelative(run.timestamp)}` : ""}
3234
3401
  </div>
3402
+ <div class="task-comment-body">${run.status || run.result || "No status summary"}</div>
3403
+ ${run.result && run.status && run.result !== run.status && html`
3404
+ <div class="task-comment-body">${run.result}</div>
3405
+ `}
3235
3406
  </div>
3236
- `;
3237
- })}
3407
+ `)}
3408
+ </div>
3238
3409
  </div>
3239
- `}
3410
+ </div>
3411
+ `}
3412
+
3413
+ </div>
3414
+
3415
+ ${/* ── RIGHT: Sidebar ── */ ""}
3416
+ <div class="task-detail-sidebar">
3417
+
3418
+ ${/* Status */ ""}
3419
+ <div class="task-sidebar-field">
3420
+ <div class="task-sidebar-label">Status</div>
3421
+ <div class="task-sidebar-value">
3422
+ <${Select}
3423
+ size="small"
3424
+ value=${status}
3425
+ onChange=${(e) => {
3426
+ const next = e.target.value;
3427
+ setStatus(next);
3428
+ if (next === "draft") setDraft(true);
3429
+ else if (draft) setDraft(false);
3430
+ }}
3431
+ fullWidth
3432
+ >
3433
+ ${["draft", "todo", "inprogress", "inreview", "done", "cancelled"].map(
3434
+ (s) => html`<${MenuItem} value=${s}>${s}</${MenuItem}>`,
3435
+ )}
3436
+ </${Select}>
3437
+ </div>
3240
3438
  </div>
3241
- <div class="task-comments-block modal-form-span jira-panel">
3242
- <div class="task-attachments-title">Comments & Updates</div>
3243
- <div class="task-comments-list">
3244
- ${comments.length > 0
3245
- ? comments.map((comment, index) => html`
3246
- <div class="task-comment-item" key=${comment.id || `comment-${index}`}>
3247
- <div class="task-comment-meta">
3248
- ${comment.author ? `@${comment.author}` : "comment"}
3249
- ${comment.createdAt ? ` · ${formatRelative(comment.createdAt)}` : ""}
3250
- </div>
3251
- <div class="task-comment-body">${comment.body}</div>
3252
- </div>
3253
- `)
3254
- : html`<div class="meta-text">No comments yet. Add one below.</div>`}
3439
+
3440
+ ${/* Priority */ ""}
3441
+ <div class="task-sidebar-field">
3442
+ <div class="task-sidebar-label">Priority</div>
3443
+ <div class="task-sidebar-value">
3444
+ <${Select}
3445
+ size="small"
3446
+ value=${priority}
3447
+ onChange=${(e) => setPriority(e.target.value)}
3448
+ fullWidth
3449
+ >
3450
+ <${MenuItem} value="">No priority</${MenuItem}>
3451
+ ${["low", "medium", "high", "critical"].map(
3452
+ (p) => html`<${MenuItem} value=${p}>${p}</${MenuItem}>`,
3453
+ )}
3454
+ </${Select}>
3255
3455
  </div>
3256
- <div class="task-comment-composer">
3456
+ </div>
3457
+
3458
+ ${/* Assignee */ ""}
3459
+ <div class="task-sidebar-field">
3460
+ <div class="task-sidebar-label">Assignee</div>
3461
+ <div class="task-sidebar-value">
3257
3462
  <${TextField}
3258
- multiline
3259
- rows=${2}
3260
3463
  size="small"
3261
- placeholder="Add a comment or status update..."
3262
- value=${commentDraft}
3263
- onInput=${(e) => setCommentDraft(e.target.value)}
3464
+ variant="outlined"
3465
+ placeholder="Assignee"
3466
+ value=${assignee}
3467
+ onInput=${(e) => setAssignee(e.target.value)}
3264
3468
  fullWidth
3265
3469
  />
3266
- <${Button}
3267
- variant="contained"
3470
+ </div>
3471
+ </div>
3472
+
3473
+ ${/* Assignees */ ""}
3474
+ <div class="task-sidebar-field">
3475
+ <div class="task-sidebar-label">Assignees</div>
3476
+ <div class="task-sidebar-value">
3477
+ <${TextField}
3268
3478
  size="small"
3269
- disabled=${postingComment || !sanitizeTaskText(commentDraft || "").trim()}
3270
- onClick=${handlePostComment}
3271
- >
3272
- ${postingComment ? "Posting…" : "Post Comment"}
3273
- <//>
3479
+ variant="outlined"
3480
+ placeholder="alice, bob"
3481
+ value=${assigneesInput}
3482
+ onInput=${(e) => setAssigneesInput(e.target.value)}
3483
+ fullWidth
3484
+ />
3274
3485
  </div>
3275
3486
  </div>
3276
- <div class="task-comments-block modal-form-span jira-subtasks-panel">
3277
- <div class="task-attachments-header">
3278
- <div class="task-attachments-title">Subtasks</div>
3279
- <div class="task-attachments-actions">
3280
- <${Button}
3281
- variant="text"
3487
+
3488
+ ${/* Sprint */ ""}
3489
+ <div class="task-sidebar-field">
3490
+ <div class="task-sidebar-label">Sprint</div>
3491
+ <div class="task-sidebar-value">
3492
+ <div style="display:flex;gap:6px;">
3493
+ <${Select}
3282
3494
  size="small"
3283
- onClick=${loadSubtasks}
3284
- disabled=${subtasksLoading || creatingSubtask}
3495
+ value=${selectedSprintId}
3496
+ onChange=${(e) => setSelectedSprintId(e.target.value)}
3497
+ style=${{ flex: 1 }}
3285
3498
  >
3286
- ${subtasksLoading ? "Refreshing…" : "Refresh"}
3287
- <//>
3499
+ <${MenuItem} value="">No sprint</${MenuItem}>
3500
+ ${sprintOptions.map((sprint) => html`
3501
+ <${MenuItem} key=${sprint.id} value=${sprint.id}>${sprint.label}</${MenuItem}>
3502
+ `)}
3503
+ </${Select}>
3504
+ <${TextField}
3505
+ size="small"
3506
+ type="number"
3507
+ placeholder="#"
3508
+ value=${sprintOrderInput}
3509
+ onInput=${(e) => setSprintOrderInput(e.target.value)}
3510
+ inputProps=${{ min: 1, step: 1 }}
3511
+ style=${{ width: "60px" }}
3512
+ />
3288
3513
  </div>
3289
3514
  </div>
3290
- <div class="task-comment-composer" style=${{ marginTop: "8px" }}>
3515
+ </div>
3516
+
3517
+ ${/* Story Points */ ""}
3518
+ <div class="task-sidebar-field">
3519
+ <div class="task-sidebar-label">Story Points</div>
3520
+ <div class="task-sidebar-value">
3291
3521
  <${TextField}
3292
3522
  size="small"
3293
- placeholder="Create subtask summary"
3294
- value=${subtaskTitle}
3295
- onInput=${(e) => setSubtaskTitle(e.target.value)}
3523
+ variant="outlined"
3524
+ type="number"
3525
+ placeholder="Points"
3526
+ value=${storyPoints}
3527
+ onInput=${(e) => setStoryPoints(e.target.value)}
3296
3528
  fullWidth
3297
3529
  />
3298
- <${Button}
3299
- variant="contained"
3530
+ </div>
3531
+ </div>
3532
+
3533
+ ${/* Due Date */ ""}
3534
+ <div class="task-sidebar-field">
3535
+ <div class="task-sidebar-label">Due Date</div>
3536
+ <div class="task-sidebar-value">
3537
+ <${TextField}
3300
3538
  size="small"
3301
- disabled=${creatingSubtask || !sanitizeTaskText(subtaskTitle || "").trim()}
3302
- onClick=${handleCreateSubtask}
3303
- >
3304
- ${creatingSubtask ? "Creating…" : "Add"}
3305
- <//>
3539
+ variant="outlined"
3540
+ type="date"
3541
+ value=${dueDate}
3542
+ onInput=${(e) => setDueDate(e.target.value)}
3543
+ InputLabelProps=${{ shrink: true }}
3544
+ fullWidth
3545
+ />
3306
3546
  </div>
3307
- <div class="task-comments-list" style=${{ marginTop: "8px" }}>
3308
- ${!subtasksLoading && !subtasks.length && html`<div class="meta-text">No subtasks yet.</div>`}
3309
- ${subtasks.map((subtask) => html`
3310
- <div class="task-comment-item" key=${subtask.id}>
3311
- <div class="task-comment-meta">
3312
- <span style="user-select:all">${subtask.id}</span>
3313
- ${subtask.status ? ` · ${subtask.status}` : ""}
3314
- ${subtask.storyPoints ? ` · ${subtask.storyPoints} pts` : ""}
3315
- </div>
3316
- <div class="task-comment-body">${subtask.title}</div>
3317
- ${subtask.assignee && html`<div class="task-comment-meta">Assignee: ${subtask.assignee}</div>`}
3547
+ </div>
3548
+
3549
+ ${/* Epic */ ""}
3550
+ <div class="task-sidebar-field">
3551
+ <div class="task-sidebar-label">Epic</div>
3552
+ <div class="task-sidebar-value">
3553
+ <${TextField}
3554
+ size="small"
3555
+ variant="outlined"
3556
+ placeholder="Epic"
3557
+ value=${epicId}
3558
+ onInput=${(e) => setEpicId(e.target.value)}
3559
+ fullWidth
3560
+ />
3561
+ ${epicCatalog.length > 0 && html`
3562
+ <div class="tag-row" style=${{ marginTop: "6px" }}>
3563
+ ${epicCatalog.slice(0, 6).map((entry) => html`<button type="button" class="tag-chip task-structure-chip ${epicId === entry.id ? "task-structure-chip-active" : ""}" style="font-size:10px;" onClick=${() => setEpicId(entry.id)}>${entry.label}</button>`)}
3318
3564
  </div>
3319
- `)}
3565
+ `}
3320
3566
  </div>
3321
3567
  </div>
3322
- <div class="task-comments-block modal-form-span jira-panel">
3323
- <div class="task-attachments-title">Dependencies & Sprint Wiring</div>
3324
- ${currentEpicEntry && html`<div class="meta-text" style=${{ marginBottom: "8px" }}>Epic: ${currentEpicEntry.label} · ${currentEpicEntry.taskCount} tasks</div>`}
3325
- <${TextField}
3326
- multiline
3327
- rows=${2}
3328
- size="small"
3329
- placeholder="Dependency task IDs (comma or newline separated)"
3330
- value=${dependenciesInput}
3331
- onInput=${(e) => setDependenciesInput(e.target.value)}
3332
- fullWidth
3333
- />
3334
- ${currentDependencyIds.length > 0 && html`
3335
- <div class="tag-row" style=${{ marginTop: "8px" }}>
3336
- ${currentDependencyIds.map((depId) => html`<button type="button" class="tag-chip task-structure-chip" onClick=${() => handleDependencyChipRemove(depId)} title="Remove dependency">${depId} ×</button>`)}
3337
- </div>
3338
- `}
3339
- ${dependencySuggestions.length > 0 && html`
3340
- <div style=${{ marginTop: "8px" }}>
3341
- <div class="meta-text" style=${{ marginBottom: "6px" }}>Quick add dependencies</div>
3342
- <div class="tag-row">
3343
- ${dependencySuggestions.map((entry) => html`<button type="button" class="tag-chip task-structure-chip task-structure-chip-muted" onClick=${() => handleDependencyChipAdd(entry.id)}>${entry.id}: ${truncate(entry.title || entry.id, 26)}</button>`)}
3344
- </div>
3345
- </div>
3346
- `}
3347
- <div class="input-row" style=${{ marginTop: "8px" }}>
3348
- <${Select}
3349
- size="small"
3350
- value=${selectedSprintId}
3351
- onChange=${(e) => setSelectedSprintId(e.target.value)}
3352
- >
3353
- <${MenuItem} value="">No sprint</${MenuItem}>
3354
- ${sprintOptions.map((sprint) => html`
3355
- <${MenuItem} key=${sprint.id} value=${sprint.id}>${sprint.label}</${MenuItem}>
3356
- `)}
3357
- </${Select}>
3568
+
3569
+ ${/* Parent Task */ ""}
3570
+ <div class="task-sidebar-field">
3571
+ <div class="task-sidebar-label">Parent Task</div>
3572
+ <div class="task-sidebar-value">
3358
3573
  <${TextField}
3359
3574
  size="small"
3360
- type="number"
3361
- placeholder="Sprint order"
3362
- value=${sprintOrderInput}
3363
- onInput=${(e) => setSprintOrderInput(e.target.value)}
3364
- inputProps=${{ min: 1, step: 1 }}
3575
+ variant="outlined"
3576
+ placeholder="Parent task ID"
3577
+ value=${parentTaskId}
3578
+ onInput=${(e) => setParentTaskId(e.target.value)}
3579
+ fullWidth
3365
3580
  />
3366
3581
  </div>
3367
- <div class="btn-row" style=${{ marginTop: "8px", flexWrap: "wrap" }}>
3368
- <${Button} variant="outlined" size="small" disabled=${savingDependencies} onClick=${handleSaveDependencies}>
3369
- ${savingDependencies ? "Saving…" : "Save Dependencies"}
3370
- <//>
3371
- <${Button} variant="outlined" size="small" disabled=${savingSprint || !selectedSprintId} onClick=${handleSaveSprintAssignment}>
3372
- ${savingSprint ? "Saving…" : "Save Sprint Assignment"}
3373
- <//>
3374
- <${Button} variant="text" size="small" disabled=${savingSprint} onClick=${() => handleSprintOrderNudge(-1)}>↑ Earlier</${Button}>
3375
- <${Button} variant="text" size="small" disabled=${savingSprint} onClick=${() => handleSprintOrderNudge(1)}>↓ Later</${Button}>
3376
- ${dependencyFeedback && html`<span class="meta-text">${dependencyFeedback}</span>`}
3377
- </div>
3378
- ${epicCatalog.length > 0 && html`
3379
- <div style=${{ marginTop: "10px" }}>
3380
- <div class="meta-text" style=${{ marginBottom: "6px" }}>Epic shortcuts</div>
3381
- <div class="tag-row">
3382
- ${epicCatalog.slice(0, 12).map((entry) => html`<button type="button" class="tag-chip task-structure-chip ${epicId === entry.id ? "task-structure-chip-active" : ""}" onClick=${() => setEpicId(entry.id)}>${entry.label}</button>`)}
3383
- </div>
3384
- </div>
3385
- `}
3386
3582
  </div>
3387
- <${Tooltip} title="Use AI to expand and improve this task description"><${Button}
3388
- variant="text" size="small"
3389
- style=${{ display: "flex", alignItems: "center", gap: "6px", alignSelf: "flex-start", fontSize: "12px", padding: "5px 10px", opacity: !title.trim() ? 0.45 : 1 }}
3390
- disabled=${!title.trim() || rewriting || saving}
3391
- onClick=${async () => {
3392
- if (!title.trim() || rewriting) return;
3393
- setRewriting(true);
3394
- haptic("medium");
3395
- try {
3396
- const res = await apiFetch("/api/tasks/rewrite", {
3397
- method: "POST",
3398
- body: JSON.stringify({ title: title.trim(), description: description.trim() }),
3399
- });
3400
- if (res?.data) {
3401
- if (res.data.title) setTitle(res.data.title);
3402
- if (res.data.description) setDescription(res.data.description);
3403
- showToast("Task description improved", "success");
3404
- haptic("medium");
3405
- }
3406
- } catch { /* toast via apiFetch */ }
3407
- setRewriting(false);
3408
- }}
3409
- >
3410
- ${rewriting
3411
- ? html`<span style="display:inline-block;animation:spin 0.8s linear infinite">${resolveIcon(":clock:")}</span> Improving…`
3412
- : html`${iconText(":star: Improve with AI")}`
3413
- }
3414
- <//><//>
3415
- <${TextField} size="small" variant="outlined" className="modal-form-span" placeholder="Base branch (optional, e.g. feature/xyz)" value=${baseBranch} onInput=${(e) => setBaseBranch(e.target.value)} fullWidth /> <div class="modal-form-span jira-meta-grid">
3416
- <${TextField}
3417
- size="small"
3418
- variant="outlined"
3419
- label="Assignee"
3420
- value=${assignee}
3421
- onInput=${(e) => setAssignee(e.target.value)}
3422
- fullWidth
3423
- />
3424
- <${TextField}
3425
- size="small"
3426
- variant="outlined"
3427
- label="Assignees"
3428
- placeholder="alice, bob"
3429
- value=${assigneesInput}
3430
- onInput=${(e) => setAssigneesInput(e.target.value)}
3431
- fullWidth
3432
- />
3433
- <${TextField}
3434
- size="small"
3435
- variant="outlined"
3436
- label="Epic"
3437
- value=${epicId}
3438
- onInput=${(e) => setEpicId(e.target.value)}
3439
- fullWidth
3440
- />
3441
- <${TextField}
3442
- size="small"
3443
- variant="outlined"
3444
- type="number"
3445
- label="Story Points"
3446
- value=${storyPoints}
3447
- onInput=${(e) => setStoryPoints(e.target.value)}
3448
- fullWidth
3449
- />
3450
- <${TextField}
3451
- size="small"
3452
- variant="outlined"
3453
- type="date"
3454
- label="Due Date"
3455
- value=${dueDate}
3456
- onInput=${(e) => setDueDate(e.target.value)}
3457
- InputLabelProps=${{ shrink: true }}
3458
- fullWidth
3459
- />
3460
- <${TextField}
3461
- size="small"
3462
- variant="outlined"
3463
- label="Parent Task"
3464
- value=${parentTaskId}
3465
- onInput=${(e) => setParentTaskId(e.target.value)}
3466
- fullWidth
3467
- />
3583
+
3584
+ ${/* Tags */ ""}
3585
+ <div class="task-sidebar-field">
3586
+ <div class="task-sidebar-label">Tags</div>
3587
+ <div class="task-sidebar-value">
3588
+ <${TextField} size="small" variant="outlined" placeholder="Tags (comma-separated)" value=${tagsInput} onInput=${(e) => setTagsInput(e.target.value)} fullWidth />
3589
+ ${normalizeTagInput(tagsInput).length > 0 && html`
3590
+ <div class="tag-row" style=${{ marginTop: "4px" }}>
3591
+ ${normalizeTagInput(tagsInput).map(
3592
+ (tag) => html`<span class="tag-chip">#${tag}</span>`,
3593
+ )}
3594
+ </div>
3595
+ `}
3596
+ </div>
3468
3597
  </div>
3469
- <div class="input-row modal-form-span">
3470
- <${Select}
3471
- size="small"
3472
- value=${workspaceId}
3473
- onChange=${(e) => setWorkspaceId(e.target.value)}
3474
- >
3475
- <${MenuItem} value="">Active workspace</${MenuItem}>
3476
- ${workspaceOptions.map(
3477
- (ws) => html`<${MenuItem} value=${ws.id}>${ws.name || ws.id}</${MenuItem}>`,
3478
- )}
3479
- </${Select}>
3480
- <${Select}
3481
- size="small"
3482
- value=${repository}
3483
- onChange=${(e) => setRepository(e.target.value)}
3484
- disabled=${!repositoryOptions.length}
3485
- >
3486
- <${MenuItem} value="">
3487
- ${repositoryOptions.length ? "Auto repository" : "No repos in workspace"}
3488
- </${MenuItem}>
3489
- ${repositoryOptions.map(
3490
- (repo) =>
3491
- html`<${MenuItem} value=${repo.slug}>${repo.name}${repo.primary ? " (Primary)" : ""}</${MenuItem}>`,
3492
- )}
3493
- </${Select}>
3598
+
3599
+ ${/* Workspace & Repository */ ""}
3600
+ <div class="task-sidebar-field">
3601
+ <div class="task-sidebar-label">Workspace</div>
3602
+ <div class="task-sidebar-value">
3603
+ <${Select}
3604
+ size="small"
3605
+ value=${workspaceId}
3606
+ onChange=${(e) => setWorkspaceId(e.target.value)}
3607
+ fullWidth
3608
+ >
3609
+ <${MenuItem} value="">Active workspace</${MenuItem}>
3610
+ ${workspaceOptions.map(
3611
+ (ws) => html`<${MenuItem} value=${ws.id}>${ws.name || ws.id}</${MenuItem}>`,
3612
+ )}
3613
+ </${Select}>
3614
+ </div>
3494
3615
  </div>
3495
- <${TextField} size="small" variant="outlined" className="modal-form-span" placeholder="Tags (comma-separated)" value=${tagsInput} onInput=${(e) => setTagsInput(e.target.value)} fullWidth />
3496
- ${normalizeTagInput(tagsInput).length > 0 &&
3497
- html`
3498
- <div class="tag-row modal-form-span">
3499
- ${normalizeTagInput(tagsInput).map(
3500
- (tag) => html`<span class="tag-chip">#${tag}</span>`,
3501
- )}
3616
+
3617
+ <div class="task-sidebar-field">
3618
+ <div class="task-sidebar-label">Repository</div>
3619
+ <div class="task-sidebar-value">
3620
+ <${Select}
3621
+ size="small"
3622
+ value=${repository}
3623
+ onChange=${(e) => setRepository(e.target.value)}
3624
+ disabled=${!repositoryOptions.length}
3625
+ fullWidth
3626
+ >
3627
+ <${MenuItem} value="">
3628
+ ${repositoryOptions.length ? "Auto repository" : "No repos in workspace"}
3629
+ </${MenuItem}>
3630
+ ${repositoryOptions.map(
3631
+ (repo) =>
3632
+ html`<${MenuItem} value=${repo.slug}>${repo.name}${repo.primary ? " (Primary)" : ""}</${MenuItem}>`,
3633
+ )}
3634
+ </${Select}>
3502
3635
  </div>
3503
- `}
3636
+ </div>
3504
3637
 
3505
- <div class="input-row modal-form-span">
3506
- <${Select}
3507
- size="small"
3508
- value=${status}
3509
- onChange=${(e) => {
3510
- const next = e.target.value;
3511
- setStatus(next);
3512
- if (next === "draft") setDraft(true);
3513
- else if (draft) setDraft(false);
3514
- }}
3515
- >
3516
- ${["draft", "todo", "inprogress", "inreview", "done", "cancelled"].map(
3517
- (s) => html`<${MenuItem} value=${s}>${s}</${MenuItem}>`,
3518
- )}
3519
- </${Select}>
3520
- <${Select}
3521
- size="small"
3522
- value=${priority}
3523
- onChange=${(e) => setPriority(e.target.value)}
3524
- >
3525
- <${MenuItem} value="">No priority</${MenuItem}>
3526
- ${["low", "medium", "high", "critical"].map(
3527
- (p) => html`<${MenuItem} value=${p}>${p}</${MenuItem}>`,
3528
- )}
3529
- </${Select}>
3638
+ ${/* Base Branch */ ""}
3639
+ <div class="task-sidebar-field">
3640
+ <div class="task-sidebar-label">Base Branch</div>
3641
+ <div class="task-sidebar-value">
3642
+ <${TextField} size="small" variant="outlined" placeholder="e.g. feature/xyz" value=${baseBranch} onInput=${(e) => setBaseBranch(e.target.value)} fullWidth />
3643
+ </div>
3530
3644
  </div>
3531
- <div class="modal-form-span">
3532
- <${Toggle}
3533
- label="Draft (keep in backlog)"
3534
- checked=${draft}
3535
- onChange=${(next) => {
3536
- setDraft(next);
3537
- if (next) setStatus("draft");
3538
- else if (status === "draft") setStatus("todo");
3539
- }}
3540
- />
3645
+
3646
+ ${/* Draft toggle */ ""}
3647
+ <div class="task-sidebar-field">
3648
+ <div class="task-sidebar-label">Draft</div>
3649
+ <div class="task-sidebar-value">
3650
+ <${Toggle}
3651
+ label="Keep in backlog"
3652
+ checked=${draft}
3653
+ onChange=${(next) => {
3654
+ setDraft(next);
3655
+ if (next) setStatus("draft");
3656
+ else if (status === "draft") setStatus("todo");
3657
+ }}
3658
+ />
3659
+ </div>
3541
3660
  </div>
3542
- <div class="modal-form-span">
3543
- <${Toggle}
3544
- label="Manual takeover (exclude from automation)"
3545
- checked=${manualOverride}
3546
- disabled=${manualBusy || !task?.id}
3547
- onChange=${handleManualToggle}
3548
- />
3661
+
3662
+ ${/* Manual Override toggle */ ""}
3663
+ <div class="task-sidebar-field">
3664
+ <div class="task-sidebar-label">Manual</div>
3665
+ <div class="task-sidebar-value">
3666
+ <${Toggle}
3667
+ label="Exclude from automation"
3668
+ checked=${manualOverride}
3669
+ disabled=${manualBusy || !task?.id}
3670
+ onChange=${handleManualToggle}
3671
+ />
3672
+ ${manualOverride && html`
3673
+ <${TextField} size="small" variant="outlined" placeholder="Reason (optional)" value=${manualReason} disabled=${manualBusy} onInput=${(e) => setManualReason(e.target.value)} fullWidth style=${{ marginTop: "6px" }} />
3674
+ <div class="meta-text" style=${{ marginTop: "4px" }}>Bosun will skip this task until cleared.</div>
3675
+ `}
3676
+ </div>
3549
3677
  </div>
3550
- <${TextField} size="small" variant="outlined" className="modal-form-span" placeholder="Manual reason (optional)" value=${manualReason} disabled=${manualBusy} onInput=${(e) => setManualReason(e.target.value)} fullWidth />
3551
- ${manualOverride &&
3552
- html`
3553
- <div class="modal-form-span">
3554
- <div class="meta-text">
3555
- Bosun will skip this task until manual takeover is cleared.
3678
+
3679
+ ${/* Dependencies */ ""}
3680
+ <div class="task-sidebar-field" style="flex-direction:column;gap:6px;">
3681
+ <div class="task-sidebar-label" style="width:auto;">Dependencies</div>
3682
+ <div class="task-sidebar-value">
3683
+ ${currentEpicEntry && html`<div class="meta-text" style=${{ marginBottom: "6px" }}>Epic: ${currentEpicEntry.label} · ${currentEpicEntry.taskCount} tasks</div>`}
3684
+ <${TextField}
3685
+ multiline
3686
+ rows=${2}
3687
+ size="small"
3688
+ placeholder="Dependency task IDs (comma or newline separated)"
3689
+ value=${dependenciesInput}
3690
+ onInput=${(e) => setDependenciesInput(e.target.value)}
3691
+ fullWidth
3692
+ />
3693
+ ${currentDependencyIds.length > 0 && html`
3694
+ <div class="tag-row" style=${{ marginTop: "6px" }}>
3695
+ ${currentDependencyIds.map((depId) => html`<button type="button" class="tag-chip task-structure-chip" onClick=${() => handleDependencyChipRemove(depId)} title="Remove dependency">${depId} ×</button>`)}
3696
+ </div>
3697
+ `}
3698
+ ${dependencySuggestions.length > 0 && html`
3699
+ <div style=${{ marginTop: "6px" }}>
3700
+ <div class="meta-text" style=${{ marginBottom: "4px" }}>Quick add</div>
3701
+ <div class="tag-row">
3702
+ ${dependencySuggestions.map((entry) => html`<button type="button" class="tag-chip task-structure-chip task-structure-chip-muted" onClick=${() => handleDependencyChipAdd(entry.id)}>${entry.id}: ${truncate(entry.title || entry.id, 20)}</button>`)}
3703
+ </div>
3704
+ </div>
3705
+ `}
3706
+ <div style="display:flex;gap:4px;margin-top:6px;flex-wrap:wrap;">
3707
+ <${Button} variant="outlined" size="small" disabled=${savingDependencies} onClick=${handleSaveDependencies}>
3708
+ ${savingDependencies ? "Saving…" : "Save Deps"}
3709
+ <//>
3710
+ <${Button} variant="outlined" size="small" disabled=${savingSprint || !selectedSprintId} onClick=${handleSaveSprintAssignment}>
3711
+ ${savingSprint ? "Saving…" : "Save Sprint"}
3712
+ <//>
3713
+ <${Button} variant="text" size="small" disabled=${savingSprint} onClick=${() => handleSprintOrderNudge(-1)}>↑<//>
3714
+ <${Button} variant="text" size="small" disabled=${savingSprint} onClick=${() => handleSprintOrderNudge(1)}>↓<//>
3556
3715
  </div>
3557
- ${manualReason &&
3558
- html`<div class="meta-text">Reason: ${manualReason}</div>`}
3716
+ ${dependencyFeedback && html`<span class="meta-text">${dependencyFeedback}</span>`}
3559
3717
  </div>
3560
- `}
3718
+ </div>
3561
3719
 
3562
- ${task?.created_at &&
3563
- html`
3564
- <div class="meta-text modal-form-span">
3565
- Created: ${new Date(task.created_at).toLocaleString()}
3720
+ ${/* Meta info */ ""}
3721
+ ${task?.meta?.triggerTemplate?.id && html`
3722
+ <div class="task-sidebar-field">
3723
+ <div class="task-sidebar-label">Trigger</div>
3724
+ <div class="task-sidebar-value" style="font-size:11px;opacity:0.7;">${task.meta.triggerTemplate.id}</div>
3566
3725
  </div>
3567
3726
  `}
3568
- ${task?.updated_at &&
3569
- html`
3570
- <div class="meta-text modal-form-span">
3571
- Updated: ${formatRelative(task.updated_at)}
3572
- </div>
3573
- `}
3574
- ${task?.meta?.triggerTemplate?.id &&
3575
- html`
3576
- <div class="meta-text modal-form-span">
3577
- Trigger Template: ${task.meta.triggerTemplate.id}
3727
+ ${(task?.meta?.execution?.sdk || task?.meta?.execution?.model) && html`
3728
+ <div class="task-sidebar-field">
3729
+ <div class="task-sidebar-label">Exec Override</div>
3730
+ <div class="task-sidebar-value" style="font-size:11px;opacity:0.7;">
3731
+ ${task?.meta?.execution?.sdk || "auto"}${task?.meta?.execution?.model ? ` · ${task.meta.execution.model}` : ""}
3732
+ </div>
3578
3733
  </div>
3579
3734
  `}
3580
- ${(task?.meta?.execution?.sdk || task?.meta?.execution?.model) &&
3581
- html`
3582
- <div class="meta-text modal-form-span">
3583
- Execution Override:
3584
- ${task?.meta?.execution?.sdk || "auto"}
3585
- ${task?.meta?.execution?.model
3586
- ? html` · ${task.meta.execution.model}`
3587
- : ""}
3588
- </div>
3589
- `}
3590
- ${task?.assignee &&
3591
- html` <div class="meta-text modal-form-span">Assignee: ${task.assignee}</div> `}
3592
- ${task?.branch &&
3593
- html`
3594
- <div class="meta-text modal-form-span" style="user-select:all">
3595
- Branch: ${task.branch}
3735
+ ${task?.branch && html`
3736
+ <div class="task-sidebar-field">
3737
+ <div class="task-sidebar-label">Branch</div>
3738
+ <div class="task-sidebar-value" style="font-size:11px;user-select:all;word-break:break-all;">${task.branch}</div>
3596
3739
  </div>
3597
3740
  `}
3598
3741
 
3599
- <${SaveDiscardBar}
3600
- dirty=${hasUnsaved}
3601
- message=${unsavedChangesMessage(changeCount)}
3602
- saveLabel="Save Changes"
3603
- discardLabel="Discard"
3604
- onSave=${() => {
3605
- void handleSave({ closeAfterSave: false });
3606
- }}
3607
- onDiscard=${handleDiscardChanges}
3608
- saving=${saving}
3609
- disabled=${Boolean(activeOperationLabel && !saving)}
3610
- />
3742
+ ${/* Timestamps */ ""}
3743
+ <div class="task-timestamps">
3744
+ ${task?.created_at && html`<div class="task-timestamp-row">Created ${new Date(task.created_at).toLocaleString()}</div>`}
3745
+ ${task?.updated_at && html`<div class="task-timestamp-row">Updated ${formatRelative(task.updated_at)}</div>`}
3746
+ </div>
3611
3747
 
3612
- <div class="btn-row modal-form-span">
3613
- ${(task?.status === "error" || task?.status === "cancelled") &&
3614
- html`
3615
- <${Button} variant="contained" size="small" onClick=${handleRetry}>
3616
- ↻ Retry
3617
- <//>
3748
+ ${/* Save bar */ ""}
3749
+ <div class="task-save-bar">
3750
+ <${SaveDiscardBar}
3751
+ dirty=${hasUnsaved}
3752
+ message=${unsavedChangesMessage(changeCount)}
3753
+ saveLabel="Save Changes"
3754
+ discardLabel="Discard"
3755
+ onSave=${() => {
3756
+ void handleSave({ closeAfterSave: false });
3757
+ }}
3758
+ onDiscard=${handleDiscardChanges}
3759
+ saving=${saving}
3760
+ disabled=${Boolean(activeOperationLabel && !saving)}
3761
+ />
3762
+ </div>
3763
+
3764
+ <div style="display:flex;gap:4px;flex-wrap:wrap;">
3765
+ ${(task?.status === "error" || task?.status === "cancelled") && html`
3766
+ <${Button} variant="contained" size="small" onClick=${handleRetry}>↻ Retry<//>
3618
3767
  `}
3619
3768
  <${Button}
3620
3769
  variant="outlined" size="small"
3621
- onClick=${() => {
3622
- void handleSave({ closeAfterSave: true });
3623
- }}
3770
+ onClick=${() => { void handleSave({ closeAfterSave: true }); }}
3624
3771
  disabled=${saving}
3625
3772
  >
3626
3773
  ${saving ? "Saving…" : iconText(":save: Save")}
3627
3774
  <//>
3628
- <${Button}
3629
- variant="text" size="small"
3630
- onClick=${() => handleStatusUpdate("inreview")}
3631
- >
3632
- → Review
3633
- <//>
3634
- <${Button}
3635
- variant="text" size="small"
3636
- onClick=${() => handleStatusUpdate("done")}
3637
- >
3638
- ${iconText("✓ Done")}
3639
- <//>
3640
- ${task?.status !== "cancelled" &&
3641
- html`
3775
+ <${Button} variant="text" size="small" onClick=${() => handleStatusUpdate("inreview")}>→ Review<//>
3776
+ <${Button} variant="text" size="small" onClick=${() => handleStatusUpdate("done")}>${iconText("✓ Done")}<//>
3777
+ ${task?.status !== "cancelled" && html`
3642
3778
  <${Button}
3643
3779
  variant="text" size="small"
3644
3780
  style=${{ color: "var(--color-error)" }}
@@ -3647,20 +3783,430 @@ export function TaskDetailModal({ task, onClose, onStart, presentation = "modal"
3647
3783
  ${iconText("✕ Cancel")}
3648
3784
  <//>
3649
3785
  `}
3786
+ ${task?.id && html`
3787
+ <${Button}
3788
+ variant="text" size="small"
3789
+ onClick=${() => {
3790
+ haptic();
3791
+ sendCommandToChat("/logs " + task.id);
3792
+ }}
3793
+ >
3794
+ ${iconText(":file: Logs")}
3795
+ <//>
3796
+ `}
3650
3797
  </div>
3651
3798
 
3652
- ${task?.id &&
3653
- html`
3654
- <${Button}
3655
- variant="text" size="small"
3656
- onClick=${() => {
3657
- haptic();
3658
- sendCommandToChat("/logs " + task.id);
3659
- }}
3660
- >
3661
- ${iconText(":file: View Agent Logs")}
3662
- <//>
3663
- `}
3799
+ </div>
3800
+
3801
+ </div>`}
3802
+
3803
+ ${/* ── EXECUTION TAB ───────────────────────────────────────────── */ ""}
3804
+ ${activeTab === "execution" && html`<div style="display:contents;">
3805
+
3806
+ ${/* ── Execution Plan Visualization (Premium) ─────────────────── */ ""}
3807
+ <div class="exec-plan-stage" style="margin:0 0 14px;">
3808
+ <div class="exec-plan-stage-header" style="background:transparent;border-bottom:none;padding:12px 16px;">
3809
+ <span style="font-weight:700;font-size:0.9em;flex:1;">${resolveIcon("play")} Execution Plan</span>
3810
+ ${executionPlanLoading && html`<span style="font-size:0.8em;opacity:0.6;">Loading…</span>`}
3811
+ ${executionPlan && html`<span style="font-size:0.8em;opacity:0.6;">${executionPlan.stageCount || 0} workflows · ${executionPlan.agentRunTotal || 0} agent runs</span>`}
3812
+ ${executionPlan?.validationIssues?.length > 0 && html`
3813
+ <span class="exec-plan-badge" style="background:#ef444420;color:#f87171;">
3814
+ ${executionPlan.validationIssues.filter((v) => v.level === "error").length} errors
3815
+ </span>
3816
+ `}
3817
+ </div>
3818
+ <div class="exec-plan-stage-body">
3819
+ ${/* ── Action buttons ── */ ""}
3820
+ <div style="display:flex;gap:8px;margin-bottom:14px;align-items:center;">
3821
+ <button style="padding:6px 14px;border-radius:6px;border:1px solid var(--border-color,#444);background:var(--color-bg-secondary,#1a1f2e);color:var(--color-text,#e0e0e0);font-size:0.8em;cursor:pointer;"
3822
+ onClick=${() => fetchExecutionPlan("resolve")} disabled=${executionPlanLoading}>
3823
+ ${resolveIcon("refresh")} Refresh Plan
3824
+ </button>
3825
+ <button style="padding:6px 14px;border-radius:6px;border:1px solid #3b82f660;background:#3b82f620;color:#60a5fa;font-size:0.8em;cursor:pointer;font-weight:600;"
3826
+ onClick=${() => fetchExecutionPlan("dry-run")} disabled=${dryRunLoading || executionPlanLoading}>
3827
+ ${dryRunLoading ? "Simulating…" : `${resolveIcon("play")} Dry Run Simulation`}
3828
+ </button>
3829
+ ${executionPlan?.mode === "dry-run" && html`
3830
+ <span style="font-size:0.8em;color:#10b981;font-weight:600;">${resolveIcon("check") || "✓"} Dry-run complete</span>
3831
+ `}
3832
+ </div>
3833
+
3834
+ ${/* ── Validation Issues ── */ ""}
3835
+ ${executionPlan?.validationIssues?.length > 0 && html`
3836
+ <div style="margin-bottom:10px;border:1px solid #ef444440;border-radius:6px;padding:8px;background:#ef444410;">
3837
+ <div style="font-weight:600;font-size:0.8em;color:#f87171;margin-bottom:4px;">${resolveIcon("warning")} Validation Issues</div>
3838
+ ${executionPlan.validationIssues.map((issue, ii) => html`
3839
+ <div key=${`vi-${ii}`} style="font-size:0.75em;padding:2px 0;display:flex;gap:4px;align-items:start;">
3840
+ <span style="color:${issue.level === 'error' ? '#f87171' : '#fbbf24'};flex-shrink:0;">${issue.level === "error" ? "✗" : "⚠"}</span>
3841
+ <span><strong>${issue.workflowName}:</strong> ${issue.message}</span>
3842
+ </div>
3843
+ `)}
3844
+ </div>
3845
+ `}
3846
+
3847
+ ${!executionPlan && !executionPlanLoading && html`
3848
+ <div style="opacity:0.6;font-size:0.85em;padding:8px;">No execution plan data available.</div>
3849
+ `}
3850
+
3851
+ ${/* ── Workflow Stages (Enhanced) ── */ ""}
3852
+ ${executionPlan?.stages?.map((stage, si) => {
3853
+ const matchColors = {
3854
+ task_assigned: { bg: "#3b82f620", color: "#60a5fa", label: "Task Match" },
3855
+ polling: { bg: "#6b728020", color: "#9ca3af", label: "Lifecycle" },
3856
+ pr_event: { bg: "#8b5cf620", color: "#a78bfa", label: "PR Event" },
3857
+ schedule: { bg: "#06b6d420", color: "#22d3ee", label: "Scheduled" },
3858
+ event: { bg: "#f5932020", color: "#fb923c", label: "Event" },
3859
+ anomaly: { bg: "#ef444420", color: "#f87171", label: "Anomaly" },
3860
+ webhook: { bg: "#84cc1620", color: "#a3e635", label: "Webhook" },
3861
+ manual: { bg: "#6b728020", color: "#d1d5db", label: "Manual" },
3862
+ workflow_call: { bg: "#14b8a620", color: "#2dd4bf", label: "Sub-call" },
3863
+ };
3864
+ const mc = matchColors[stage.matchType] || { bg: "#33333320", color: "#888", label: stage.matchType };
3865
+
3866
+ return html`
3867
+ <div key=${`stage-${si}`} class="exec-plan-stage">
3868
+ <div class="exec-plan-stage-header" onClick=${() => toggleStageExpand(si)}>
3869
+ <span style="font-size:0.8em;opacity:0.5;">${expandedStages[si] !== false ? "▾" : "▸"}</span>
3870
+ ${stage.core ? html`<span class="exec-plan-badge" style="background:#8b5cf620;color:#a78bfa;">CORE</span>` : ""}
3871
+ <strong style="font-size:0.9em;flex:1;">${stage.workflowName}</strong>
3872
+ <span class="exec-plan-badge" style="background:${mc.bg};color:${mc.color};">${mc.label}</span>
3873
+ <span style="font-size:0.75em;opacity:0.5;">${stage.nodeCount} nodes · ${stage.agentRunCount} agents</span>
3874
+ <span style="font-size:0.65em;opacity:0.35;text-transform:uppercase;">${stage.category || ""}</span>
3875
+ </div>
3876
+
3877
+ ${expandedStages[si] !== false && html`
3878
+ <div class="exec-plan-stage-body">
3879
+ ${stage.description ? html`<div style="font-size:0.8em;opacity:0.6;margin-bottom:10px;">${stage.description}</div>` : ""}
3880
+
3881
+ ${/* ── Node Pipeline ── */ ""}
3882
+ <div style="display:flex;flex-direction:column;gap:0;">
3883
+ ${(stage.nodes || []).map((nd, ni) => {
3884
+ const isExpanded = expandedNodes[`${si}-${nd.id}`];
3885
+ const nodeColor = nd.isAgentRun ? "#3b82f6"
3886
+ : nd.isTrigger ? "#eab308"
3887
+ : nd.isCondition ? "#8b5cf6"
3888
+ : nd.isCommand || nd.isValidation ? "#22c55e"
3889
+ : nd.isStatusUpdate ? "#ef4444"
3890
+ : nd.isPromptBuilder ? "#f59e0b"
3891
+ : nd.isCreatePr || nd.isPushBranch ? "#06b6d4"
3892
+ : nd.isSubWorkflow ? "#14b8a6"
3893
+ : nd.isNotify ? "#64748b"
3894
+ : "#6b7280";
3895
+ const hasIssue = nd.expressionValid === false || !nd.typeRegistered || (nd.unresolvedVars?.length > 0);
3896
+ const dryRunNode = dryRunResults?.find((dr) => dr.workflowId === stage.workflowId)?.nodes?.find((dn) => dn.id === nd.id);
3897
+
3898
+ return html`
3899
+ <div key=${`n-${ni}`}>
3900
+ ${ni > 0 && html`<div class="exec-plan-connector"></div>`}
3901
+ <div class="exec-plan-node" style="border-color:${hasIssue ? '#ef4444' : nodeColor + '40'};">
3902
+ <div class="exec-plan-node-header" onClick=${() => toggleNodeExpand(si, nd.id)}>
3903
+ <span style="width:22px;height:22px;border-radius:6px;background:${nodeColor}20;color:${nodeColor};display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0;">${ni + 1}</span>
3904
+ <span style="font-size:0.7em;color:${nodeColor};opacity:0.8;min-width:70px;font-weight:500;">${nd.type.split(".").pop()}</span>
3905
+ <strong style="flex:1;font-size:0.85em;">${nd.label}</strong>
3906
+ ${hasIssue ? html`<span style="color:#ef4444;font-size:0.7em;">✗</span>` : ""}
3907
+
3908
+ ${/* Agent badges */ ""}
3909
+ ${nd.isAgentRun && nd.resolvedAgent ? html`
3910
+ <span class="exec-plan-skill-tag" style="background:${nodeColor}15;color:${nodeColor};border-color:${nodeColor}30;">
3911
+ ${resolveIcon("bot")} ${nd.resolvedAgent} ${nd.confidence ? `(${Math.round(nd.confidence * 100)}%)` : ""}
3912
+ </span>
3913
+ ` : ""}
3914
+ ${nd.isAgentRun && !nd.resolvedAgent && nd.resolveMode === "library" ? html`
3915
+ <span class="exec-plan-badge" style="background:#f59e0b20;color:#fbbf24;">Auto-Resolve</span>
3916
+ ` : ""}
3917
+
3918
+ ${/* Context flow chips */ ""}
3919
+ ${nd.contextPreview?.hasTaskPrompt ? html`<span class="exec-plan-context-chip">TaskPrompt</span>` : ""}
3920
+ ${nd.contextPreview?.hasPreviousOutput ? html`<span class="exec-plan-context-chip" style="background:#8b5cf615;color:#a78bfa;border-color:#8b5cf630;">PrevOutput</span>` : ""}
3921
+
3922
+ ${/* Status indicators */ ""}
3923
+ ${nd.isStatusUpdate ? html`<span class="exec-plan-badge" style="background:#ef444420;color:#fca5a5;">→ ${nd.targetStatus}</span>` : ""}
3924
+ ${nd.isCommand ? html`<span style="font-size:0.65em;font-family:monospace;opacity:0.5;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${nd.commandResolved || nd.commandRaw}</span>` : ""}
3925
+ ${nd.isPromptBuilder ? html`<span class="exec-plan-badge" style="background:#f59e0b20;color:#fbbf24;">Builds TaskPrompt</span>` : ""}
3926
+ ${nd.isCreatePr ? html`<span class="exec-plan-badge" style="background:#06b6d420;color:#22d3ee;">Creates PR</span>` : ""}
3927
+
3928
+ ${dryRunNode ? html`<span style="font-size:0.65em;color:${dryRunNode.status === 'simulated' || dryRunNode.status === 'COMPLETED' ? '#10b981' : '#fbbf24'};">● ${dryRunNode.status}</span>` : ""}
3929
+ <span style="font-size:0.65em;opacity:0.3;">${isExpanded ? "▾" : "▸"}</span>
3930
+ </div>
3931
+
3932
+ ${isExpanded && html`
3933
+ <div class="exec-plan-node-body">
3934
+ ${/* ── Inputs from previous nodes ── */ ""}
3935
+ ${nd.inputsFrom?.length > 0 ? html`
3936
+ <div style="margin-bottom:8px;padding:6px 8px;background:var(--bg-surface,#0d1117);border-radius:6px;border:1px solid var(--border-color,#333);">
3937
+ <div style="font-size:0.85em;opacity:0.6;margin-bottom:4px;font-weight:600;">Inputs From:</div>
3938
+ <div style="display:flex;flex-wrap:wrap;gap:4px;">
3939
+ ${nd.inputsFrom.map((inp) => html`
3940
+ <span class="exec-plan-context-chip" style="background:#3b82f610;color:#60a5fa;border-color:#3b82f625;">
3941
+ ${inp.nodeLabel} ${inp.port ? `[${inp.port}]` : ""} ${inp.condition ? `if: ${inp.condition.slice(0, 30)}` : ""}
3942
+ </span>
3943
+ `)}
3944
+ </div>
3945
+ </div>
3946
+ ` : ""}
3947
+
3948
+ <div style="display:grid;grid-template-columns:auto 1fr;gap:3px 12px;align-items:start;">
3949
+ <span style="opacity:0.5;">Type:</span>
3950
+ <span style="font-family:monospace;">${nd.type}${!nd.typeRegistered ? html` <span style="color:#ef4444;">✗ unregistered</span>` : ""}</span>
3951
+
3952
+ ${nd.isTrigger && nd.taskPattern ? html`
3953
+ <span style="opacity:0.5;">Pattern:</span>
3954
+ <span style="font-family:monospace;">${nd.taskPattern} ${nd.patternMatches === true ? html`<span style="color:#10b981;">✓ matches</span>` : nd.patternMatches === false ? html`<span style="color:#ef4444;">✗ no match</span>` : ""}</span>
3955
+ ` : ""}
3956
+
3957
+ ${nd.isTrigger && nd.prEvents ? html`
3958
+ <span style="opacity:0.5;">PR Events:</span>
3959
+ <span>${nd.prEvents.join(", ")}</span>
3960
+ ` : ""}
3961
+
3962
+ ${nd.isTrigger && nd.eventTypes?.length > 0 ? html`
3963
+ <span style="opacity:0.5;">Event Types:</span>
3964
+ <span>${nd.eventTypes.join(", ")}</span>
3965
+ ` : ""}
3966
+
3967
+ ${nd.isTrigger && nd.intervalMs ? html`
3968
+ <span style="opacity:0.5;">Interval:</span>
3969
+ <span>${Math.round(nd.intervalMs / 60000)}min</span>
3970
+ ` : ""}
3971
+
3972
+ ${nd.isCondition && nd.expression ? html`
3973
+ <span style="opacity:0.5;">Expression:</span>
3974
+ <span style="font-family:monospace;word-break:break-all;">${nd.expression}${nd.expressionValid === false ? html` <span style="color:#ef4444;">✗ ${nd.expressionError}</span>` : html` <span style="color:#10b981;">✓</span>`}</span>
3975
+ ` : ""}
3976
+ ${nd.isCondition && nd.cases ? html`
3977
+ <span style="opacity:0.5;">Cases:</span>
3978
+ <span>${nd.cases.join(", ")}</span>
3979
+ ` : ""}
3980
+
3981
+ ${nd.isAgentRun ? html`
3982
+ <span style="opacity:0.5;">SDK:</span><span>${nd.sdk || "auto"}</span>
3983
+ <span style="opacity:0.5;">Model:</span><span>${nd.model || "auto"}</span>
3984
+ <span style="opacity:0.5;">Timeout:</span><span>${Math.round((nd.timeoutMs || 3600000) / 60000)}min</span>
3985
+ <span style="opacity:0.5;">Retries:</span><span>${nd.maxRetries ?? 2} retries, ${nd.maxContinues ?? 2} continues</span>
3986
+ <span style="opacity:0.5;">Resolve:</span><span style="font-weight:500;color:${nd.resolveMode === 'library' ? '#f59e0b' : '#60a5fa'};">${nd.resolveMode || "manual"}${nd.resolveMode === "library" ? " (auto)" : ""}</span>
3987
+ <span style="opacity:0.5;">CWD:</span><span style="font-family:monospace;">${nd.cwd || "auto"}</span>
3988
+ ` : ""}
3989
+
3990
+ ${nd.isCommand ? html`
3991
+ <span style="opacity:0.5;">Command:</span>
3992
+ <span style="font-family:monospace;word-break:break-all;">${nd.commandResolved || nd.commandRaw}</span>
3993
+ <span style="opacity:0.5;">CWD:</span><span style="font-family:monospace;">${nd.commandCwd}</span>
3994
+ <span style="opacity:0.5;">Timeout:</span><span>${Math.round((nd.commandTimeout || 300000) / 1000)}s</span>
3995
+ <span style="opacity:0.5;">Fail on error:</span><span>${nd.failOnError ? "Yes" : "No"}</span>
3996
+ ` : ""}
3997
+
3998
+ ${nd.isResolveExecutor ? html`
3999
+ <span style="opacity:0.5;">SDK Override:</span><span>${nd.sdkOverride || "auto"}</span>
4000
+ <span style="opacity:0.5;">Model Override:</span><span>${nd.modelOverride || "auto"}</span>
4001
+ ` : ""}
4002
+
4003
+ ${nd.isSubWorkflow ? html`
4004
+ <span style="opacity:0.5;">Sub-workflow:</span><span style="font-family:monospace;">${nd.targetWorkflowId || "—"}</span>
4005
+ <span style="opacity:0.5;">Inherit ctx:</span><span>${nd.inheritContext ? "Yes" : "No"}</span>
4006
+ ` : ""}
4007
+
4008
+ ${nd.isValidation ? html`
4009
+ <span style="opacity:0.5;">${nd.validationType} cmd:</span>
4010
+ <span style="font-family:monospace;">${nd.commandResolved || nd.commandRaw || "auto-detect"}</span>
4011
+ ` : ""}
4012
+
4013
+ ${nd.isPromptBuilder ? html`
4014
+ <span style="opacity:0.5;">Output:</span><span>${"{{TaskPrompt}}"}</span>
4015
+ <span style="opacity:0.5;">Include Skills:</span><span>${nd.includeSkills !== false ? "Yes" : "No"}</span>
4016
+ <span style="opacity:0.5;">Include Agent Instructions:</span><span>${nd.includeAgentInstructions !== false ? "Yes" : "No"}</span>
4017
+ ` : ""}
4018
+
4019
+ ${nd.isCreatePr ? html`
4020
+ <span style="opacity:0.5;">PR Title:</span><span>${nd.prTitle || "auto"}</span>
4021
+ <span style="opacity:0.5;">Base Branch:</span><span>${nd.prBaseBranch || "main"}</span>
4022
+ ` : ""}
4023
+
4024
+ ${nd.joinMode ? html`
4025
+ <span style="opacity:0.5;">Join mode:</span><span>${nd.joinMode}</span>
4026
+ ` : ""}
4027
+
4028
+ ${nd.isNotify && nd.logMessage ? html`
4029
+ <span style="opacity:0.5;">Message:</span><span>${nd.logMessage}</span>
4030
+ ` : ""}
4031
+
4032
+ ${nd.unresolvedVars?.length > 0 ? html`
4033
+ <span style="opacity:0.5;color:#fbbf24;">Unresolved:</span>
4034
+ <span style="color:#fbbf24;">${nd.unresolvedVars.map((v) => `{{${v}}}`).join(", ")}</span>
4035
+ ` : ""}
4036
+ </div>
4037
+
4038
+ ${/* ── Agent: Context Preview ── */ ""}
4039
+ ${nd.isAgentRun && nd.contextPreview ? html`
4040
+ <div style="margin-top:8px;padding:6px 8px;background:#1a1a2e;border-radius:6px;border:1px solid #333;">
4041
+ <div style="font-size:0.85em;opacity:0.6;margin-bottom:4px;font-weight:600;">${resolveIcon("link") || "🔗"} Context Injected:</div>
4042
+ <div style="display:flex;flex-wrap:wrap;gap:4px;">
4043
+ ${nd.contextPreview.hasTaskPrompt ? html`<span class="exec-plan-context-chip">Task Prompt (built by build_task_prompt)</span>` : ""}
4044
+ ${nd.contextPreview.hasPreviousOutput ? html`<span class="exec-plan-context-chip" style="background:#8b5cf615;color:#a78bfa;border-color:#8b5cf630;">Previous Agent Output</span>` : ""}
4045
+ ${nd.contextPreview.hasWorktreePath ? html`<span class="exec-plan-context-chip" style="background:#22c55e15;color:#4ade80;border-color:#22c55e30;">Worktree Path</span>` : ""}
4046
+ ${nd.contextPreview.hasBranchName ? html`<span class="exec-plan-context-chip" style="background:#06b6d415;color:#22d3ee;border-color:#06b6d430;">Branch Name</span>` : ""}
4047
+ ${nd.contextPreview.hasPrUrl ? html`<span class="exec-plan-context-chip" style="background:#8b5cf615;color:#c4b5fd;border-color:#8b5cf630;">PR Link</span>` : ""}
4048
+ ${nd.includeTaskContext ? html`<span class="exec-plan-context-chip" style="background:#f59e0b15;color:#fbbf24;border-color:#f59e0b30;">Full Task Context</span>` : ""}
4049
+ ${(nd.contextPreview.injectedVariables || []).map((v) => html`
4050
+ <span class="exec-plan-context-chip" style="background:#64748b15;color:#94a3b8;border-color:#64748b30;">${"{{" + v + "}}"}</span>
4051
+ `)}
4052
+ </div>
4053
+ </div>
4054
+ ` : ""}
4055
+
4056
+ ${/* ── Agent: resolved skills with descriptions ── */ ""}
4057
+ ${nd.isAgentRun && nd.resolvedSkills?.length > 0 ? html`
4058
+ <div style="margin-top:8px;padding:8px;background:var(--bg-surface,#0d1117);border-radius:6px;border:1px solid var(--border-color,#333);">
4059
+ <div style="font-size:0.85em;opacity:0.6;margin-bottom:6px;font-weight:600;">${resolveIcon("star")} Resolved Skills (${nd.resolvedSkills.length}):</div>
4060
+ ${nd.resolvedSkills.map((sk) => html`
4061
+ <div style="display:flex;gap:8px;padding:4px 0;align-items:start;border-bottom:1px solid var(--border-color,#222);">
4062
+ <span class="exec-plan-skill-tag">${sk.name} ${sk.score ? `${Math.round(sk.score * 100)}%` : ""}</span>
4063
+ ${sk.source ? html`<span style="opacity:0.35;font-size:0.85em;">(${sk.source})</span>` : ""}
4064
+ ${sk.description ? html`<span style="opacity:0.5;font-size:0.85em;flex:1;">${sk.description.slice(0, 120)}${sk.description.length > 120 ? "…" : ""}</span>` : ""}
4065
+ </div>
4066
+ `)}
4067
+ </div>
4068
+ ` : ""}
4069
+
4070
+ ${/* ── Agent: resolved tools ── */ ""}
4071
+ ${nd.isAgentRun && nd.resolvedTools && (nd.resolvedTools.builtin?.length > 0 || nd.resolvedTools.mcp?.length > 0) ? html`
4072
+ <div style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px;align-items:center;">
4073
+ <span style="opacity:0.6;font-size:0.85em;">${resolveIcon("tool")} Tools:</span>
4074
+ ${[...(nd.resolvedTools.builtin || []), ...(nd.resolvedTools.mcp || [])].map((t) => html`
4075
+ <span style="padding:1px 6px;border-radius:4px;font-size:0.75em;background:#22c55e10;color:#4ade80;border:1px solid #22c55e25;">${t}</span>
4076
+ `)}
4077
+ </div>
4078
+ ` : ""}
4079
+
4080
+ ${/* ── Agent: alternatives ── */ ""}
4081
+ ${nd.isAgentRun && nd.alternatives?.length > 0 ? html`
4082
+ <div style="margin-top:6px;opacity:0.5;font-size:0.85em;">
4083
+ <span>Alternatives: ${nd.alternatives.map((a) => `${a.name} (${Math.round((a.confidence || 0) * 100)}%)`).join(", ")}</span>
4084
+ </div>
4085
+ ` : ""}
4086
+
4087
+ ${/* ── Agent: prompt preview ── */ ""}
4088
+ ${nd.isAgentRun && nd.promptResolved ? html`
4089
+ <details style="margin-top:8px;">
4090
+ <summary style="cursor:pointer;opacity:0.6;font-size:0.85em;font-weight:500;">Prompt Preview (${nd.promptResolved.length} chars)</summary>
4091
+ <pre style="margin-top:4px;padding:8px;background:#00000030;border-radius:6px;white-space:pre-wrap;word-break:break-word;max-height:300px;overflow-y:auto;font-size:0.8em;line-height:1.5;">${nd.promptResolved.slice(0, 3000)}${nd.promptResolved.length > 3000 ? "\n…(truncated)" : ""}</pre>
4092
+ </details>
4093
+ ` : ""}
4094
+ </div>
4095
+ `}
4096
+ </div>
4097
+ </div>
4098
+ `;
4099
+ })}
4100
+ </div>
4101
+
4102
+ ${/* ── Edge routing ── */ ""}
4103
+ ${stage.edges?.some((e) => e.condition || e.sourcePort || e.isBackEdge) && html`
4104
+ <details style="margin-top:10px;">
4105
+ <summary style="cursor:pointer;font-size:0.8em;opacity:0.5;font-weight:500;">Edge Routing (${stage.edges.length} edges)</summary>
4106
+ <div style="margin-top:6px;font-size:0.75em;font-family:monospace;">
4107
+ ${stage.edges.filter((e) => e.condition || e.sourcePort || e.isBackEdge).map((e) => html`
4108
+ <div style="padding:3px 0;display:flex;gap:6px;align-items:center;">
4109
+ <span>${e.source}</span>
4110
+ <span style="opacity:0.3;">→</span>
4111
+ <span>${e.target}</span>
4112
+ ${e.sourcePort ? html`<span style="color:#a78bfa;">[${e.sourcePort}]</span>` : ""}
4113
+ ${e.condition ? html`<span style="opacity:0.5;color:${e.conditionValid === false ? '#ef4444' : '#4ade80'};">${e.condition.length > 60 ? e.condition.slice(0, 60) + "…" : e.condition}</span>` : ""}
4114
+ ${e.isBackEdge ? html`<span style="color:#fbbf24;">↩ loop</span>` : ""}
4115
+ ${e.conditionValid === false ? html`<span style="color:#ef4444;">✗ ${e.conditionError}</span>` : ""}
4116
+ </div>
4117
+ `)}
4118
+ </div>
4119
+ </details>
4120
+ `}
4121
+ </div>
4122
+ `}
4123
+ </div>
4124
+ `;})}
4125
+
4126
+ ${/* ── Dry-run results summary ── */ ""}
4127
+ ${dryRunResults && html`
4128
+ <div style="margin-top:10px;border:1px solid #10b98140;border-radius:8px;padding:10px;background:#10b98110;">
4129
+ <div style="font-weight:600;font-size:0.85em;color:#10b981;margin-bottom:6px;">${resolveIcon("check")} Dry-Run Simulation Results</div>
4130
+ ${dryRunResults.map((dr) => html`
4131
+ <div style="font-size:0.8em;padding:3px 0;display:flex;gap:8px;align-items:center;">
4132
+ <span style="font-weight:500;">${dr.workflowName}</span>
4133
+ <span class="exec-plan-badge" style="background:${dr.status === 'completed' ? '#10b98120' : dr.status === 'error' ? '#ef444420' : '#fbbf2420'};color:${dr.status === 'completed' ? '#10b981' : dr.status === 'error' ? '#ef4444' : '#fbbf24'};">
4134
+ ${dr.status}
4135
+ </span>
4136
+ ${dr.error ? html`<span style="color:#ef4444;font-size:0.9em;">${dr.error}</span>` : ""}
4137
+ ${dr.nodes?.length > 0 ? html`<span style="opacity:0.5;">(${dr.nodes.length} nodes simulated)</span>` : ""}
4138
+ </div>
4139
+ `)}
4140
+ </div>
4141
+ `}
4142
+ </div>
4143
+ </div>
4144
+
4145
+ </div>`}
4146
+
4147
+ ${/* ── HISTORY TAB ─────────────────────────────────────────────── */ ""}
4148
+ ${activeTab === "history" && html`<div style="display:contents;">
4149
+
4150
+ ${historyEntries.length > 0 ? html`
4151
+ <div class="task-comments-block modal-form-span jira-panel">
4152
+ <div class="task-attachments-title">History Timeline</div>
4153
+ <div class="task-comments-list">
4154
+ ${historyEntries.map((entry, index) => html`
4155
+ <div class="task-comment-item" key=${`history-${index}`}>
4156
+ <div class="task-comment-meta">
4157
+ ${entry.timestamp ? formatRelative(entry.timestamp) : "Time unknown"}
4158
+ ${entry.source ? ` · ${entry.source}` : ""}
4159
+ </div>
4160
+ <div class="task-comment-body">${entry.label}</div>
4161
+ </div>
4162
+ `)}
4163
+ </div>
4164
+ </div>
4165
+ ` : ""}
4166
+
4167
+ ${relatedLinks.length > 0 && html`
4168
+ <div class="task-comments-block modal-form-span jira-panel">
4169
+ <div class="task-attachments-title">Branch and PR Links</div>
4170
+ <div class="task-comments-list">
4171
+ ${relatedLinks.map((item, index) => html`
4172
+ <div class="task-comment-item" key=${`link-${index}`}>
4173
+ <div class="task-comment-meta">${item.kind}</div>
4174
+ <div class="task-comment-body">
4175
+ ${item.url
4176
+ ? html`<a href=${item.url} target="_blank" rel="noopener">${item.value}</a>`
4177
+ : item.value}
4178
+ </div>
4179
+ </div>
4180
+ `)}
4181
+ </div>
4182
+ </div>
4183
+ `}
4184
+
4185
+ ${/* workflow activity in history tab too */ ""}
4186
+ ${workflowRuns.length > 0 && html`
4187
+ <div class="task-comments-block modal-form-span jira-panel">
4188
+ <div class="task-attachments-title">Workflow Activity</div>
4189
+ <div class="task-comments-list">
4190
+ ${workflowRuns.map((run, index) => html`
4191
+ <div class="task-comment-item" key=${`wf-hist-${index}`}>
4192
+ <div class="task-comment-meta">
4193
+ ${run.workflowId || "workflow"}
4194
+ ${run.runId ? ` · run ${run.runId}` : ""}
4195
+ ${run.timestamp ? ` · ${formatRelative(run.timestamp)}` : ""}
4196
+ </div>
4197
+ <div class="task-comment-body">${run.status || run.result || "No status summary"}</div>
4198
+ </div>
4199
+ `)}
4200
+ </div>
4201
+ </div>
4202
+ `}
4203
+
4204
+ ${historyEntries.length === 0 && workflowRuns.length === 0 && relatedLinks.length === 0 ? html`
4205
+ <div style="padding:24px;text-align:center;opacity:0.5;font-size:0.9em;">No history, workflow runs, or links recorded yet.</div>
4206
+ ` : ""}
4207
+
4208
+ </div>`}
4209
+
3664
4210
  </div>
3665
4211
  <//>
3666
4212
  `;