@flowdesk/opencode-plugin 0.1.13 → 0.1.14

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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-task-output.d.ts +17 -0
  3. package/dist/agent-task-output.d.ts.map +1 -0
  4. package/dist/agent-task-output.js +119 -0
  5. package/dist/agent-task-output.js.map +1 -0
  6. package/dist/agent-task-runner.d.ts +23 -0
  7. package/dist/agent-task-runner.d.ts.map +1 -1
  8. package/dist/agent-task-runner.js +410 -81
  9. package/dist/agent-task-runner.js.map +1 -1
  10. package/dist/auto-continue-preview-tool.d.ts +36 -0
  11. package/dist/auto-continue-preview-tool.d.ts.map +1 -0
  12. package/dist/auto-continue-preview-tool.js +119 -0
  13. package/dist/auto-continue-preview-tool.js.map +1 -0
  14. package/dist/completion-ui-cache.d.ts +6 -0
  15. package/dist/completion-ui-cache.d.ts.map +1 -0
  16. package/dist/completion-ui-cache.js +260 -0
  17. package/dist/completion-ui-cache.js.map +1 -0
  18. package/dist/event-hook-observer.d.ts +14 -0
  19. package/dist/event-hook-observer.d.ts.map +1 -0
  20. package/dist/event-hook-observer.js +193 -0
  21. package/dist/event-hook-observer.js.map +1 -0
  22. package/dist/managed-dispatch-adapter.d.ts.map +1 -1
  23. package/dist/managed-dispatch-adapter.js +7 -3
  24. package/dist/managed-dispatch-adapter.js.map +1 -1
  25. package/dist/model-selection-engine.d.ts +47 -0
  26. package/dist/model-selection-engine.d.ts.map +1 -0
  27. package/dist/model-selection-engine.js +175 -0
  28. package/dist/model-selection-engine.js.map +1 -0
  29. package/dist/provider-usage-live-tool.d.ts +10 -0
  30. package/dist/provider-usage-live-tool.d.ts.map +1 -1
  31. package/dist/provider-usage-live-tool.js +145 -18
  32. package/dist/provider-usage-live-tool.js.map +1 -1
  33. package/dist/server.d.ts +35 -1
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +447 -19
  36. package/dist/server.js.map +1 -1
  37. package/dist/stall-recovery.d.ts +33 -0
  38. package/dist/stall-recovery.d.ts.map +1 -1
  39. package/dist/stall-recovery.js +459 -2
  40. package/dist/stall-recovery.js.map +1 -1
  41. package/dist/status-live-tool.d.ts +54 -0
  42. package/dist/status-live-tool.d.ts.map +1 -1
  43. package/dist/status-live-tool.js +448 -44
  44. package/dist/status-live-tool.js.map +1 -1
  45. package/dist/tui-subtask-activity.d.ts +69 -0
  46. package/dist/tui-subtask-activity.d.ts.map +1 -0
  47. package/dist/tui-subtask-activity.js +266 -0
  48. package/dist/tui-subtask-activity.js.map +1 -0
  49. package/dist/tui-usage-snapshot.d.ts +14 -0
  50. package/dist/tui-usage-snapshot.d.ts.map +1 -1
  51. package/dist/tui-usage-snapshot.js +189 -8
  52. package/dist/tui-usage-snapshot.js.map +1 -1
  53. package/dist/tui.d.ts.map +1 -1
  54. package/dist/tui.js +72 -41
  55. package/dist/tui.js.map +1 -1
  56. package/dist/workflow-assign-tool.d.ts +23 -0
  57. package/dist/workflow-assign-tool.d.ts.map +1 -0
  58. package/dist/workflow-assign-tool.js +117 -0
  59. package/dist/workflow-assign-tool.js.map +1 -0
  60. package/dist/workflow-author-tool.d.ts +29 -0
  61. package/dist/workflow-author-tool.d.ts.map +1 -0
  62. package/dist/workflow-author-tool.js +227 -0
  63. package/dist/workflow-author-tool.js.map +1 -0
  64. package/dist/workflow-dispatch-tool.d.ts.map +1 -1
  65. package/dist/workflow-dispatch-tool.js +32 -2
  66. package/dist/workflow-dispatch-tool.js.map +1 -1
  67. package/dist/workflow-orchestrator.d.ts +31 -0
  68. package/dist/workflow-orchestrator.d.ts.map +1 -0
  69. package/dist/workflow-orchestrator.js +160 -0
  70. package/dist/workflow-orchestrator.js.map +1 -0
  71. package/dist/workflow-scheduler.d.ts.map +1 -1
  72. package/dist/workflow-scheduler.js +3 -1
  73. package/dist/workflow-scheduler.js.map +1 -1
  74. package/dist/workflow-synthesis-tool.d.ts +31 -0
  75. package/dist/workflow-synthesis-tool.d.ts.map +1 -0
  76. package/dist/workflow-synthesis-tool.js +194 -0
  77. package/dist/workflow-synthesis-tool.js.map +1 -0
  78. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -4,18 +4,23 @@ import { join, resolve, sep } from "node:path";
4
4
  import { applyFlowDeskSessionEvidenceWriteIntentsV1, createFlowDeskChatHookAuthorityProbeV1, evaluateFlowDeskChatIntakeV1, getFlowDeskPortableCommandToolName, getRelease1SchemaArtifact, materializeFlowDeskExactModelCacheEvidenceFromProviderAcquisitionEvidenceV1, materializeFlowDeskRuntimeLaneLaunchPlansFromReviewerFanoutEvidenceV1, planFlowDeskReviewerFanoutFromReloadedCacheEvidenceV1, prepareFlowDeskSessionEvidenceWriteIntentV1, reloadFlowDeskSessionEvidenceV1, validateFlowDeskDefaultManagedDispatchAuthorizationV1, validateProjectConfigV1, validateRunRequestV1, } from "@flowdesk/core";
5
5
  import { tool, } from "@opencode-ai/plugin";
6
6
  import { flowdeskPluginId, flowdeskPluginScaffold, hasProductionOpenCodeRegistration, } from "./index.js";
7
+ import { observeFlowDeskOpenCodeEventV1 } from "./event-hook-observer.js";
7
8
  import { recordFlowDeskLaneHeartbeatV1, } from "./lane-heartbeat-writer.js";
8
9
  import { createFlowDeskLocalNonDispatchAdapterSession, flowdeskLocalNonDispatchAdapterProfile, hasFlowDeskLocalPlanningEvidenceV1, } from "./local-adapter.js";
9
10
  import { createFlowDeskManagedDispatchBetaDurableReservationStoreV1, createFlowDeskOpenCodeMetadataProviderAcquisitionClientV1, createFlowDeskOpenCodePromptBackedProviderAcquisitionClientV1, dispatchManagedDispatchBetaPromptV1, materializeFlowDeskManagedFallbackRegatePlanEvidenceV1, orchestrateFlowDeskManagedFallbackRegateV1, runFlowDeskExactModelProviderAcquisitionLiveTestV1, } from "./managed-dispatch-adapter.js";
10
11
  import { executeFlowDeskProviderUsageLiveV1, } from "./provider-usage-live-tool.js";
12
+ import { formatFlowDeskTuiUsageSnapshotCompactText, loadFlowDeskTuiUsageSnapshotViewV1, } from "./tui-usage-snapshot.js";
11
13
  import { executeFlowDeskQuickFallbackRunV1, } from "./quick-fallback-run.js";
12
14
  import { executeFlowDeskQuickReviewerRunV1, } from "./quick-reviewer-run.js";
13
15
  import { executeFlowDeskRuntimeReviewerExecutionBridgeV1, redactedRuntimeReviewerExecutionBlocked, runtimeReviewerExecutionExpectationsFromValue, } from "./runtime-reviewer-execution-bridge.js";
14
16
  import { executeFlowDeskAgentTaskV1 } from "./agent-task-runner.js";
17
+ import { executeFlowDeskAutoContinuePreviewToolV1, } from "./auto-continue-preview-tool.js";
15
18
  import { executeFlowDeskControlledWriteApplyToolV1, } from "./controlled-write-tool.js";
16
19
  import { executeFlowDeskStatusLiveV1, } from "./status-live-tool.js";
17
20
  import { executeFlowDeskWorkflowDispatchPlanToolV1, } from "./workflow-dispatch-plan-tool.js";
18
21
  import { executeFlowDeskWorkflowDispatchToolV1, } from "./workflow-dispatch-tool.js";
22
+ import { executeFlowDeskWorkflowOrchestratorV1 } from "./workflow-orchestrator.js";
23
+ import { executeFlowDeskWorkflowSynthesisPreviewV1 } from "./workflow-synthesis-tool.js";
19
24
  import { evaluateGuardedAutoAbortHookV1, evaluateGuardedAutoRetryHookV1, reconcileStalePendingRetryPlansV1, checkSdkSessionApiHealthV1, runFlowDeskWatchdogCycleV1, } from "./stall-recovery.js";
20
25
  import { withTimeout, FlowDeskTimeoutError } from "./shared/with-timeout.js";
21
26
  import { FLOWDESK_PRE_SPIKE_PLUGIN_TOOL_STUBS, getFlowDeskRelease1HandlerReadinessSummary, getFlowDeskRelease1ProductionReadinessSummary, hasPassingFds1SchemaConversionSpike, runFlowDeskPreSpikePluginToolStub, } from "./tool-stubs.js";
@@ -40,6 +45,7 @@ export const flowdeskLaneHeartbeatWriterOption = "laneHeartbeatWriter";
40
45
  export const flowdeskWorkflowDispatchPlanToolOption = "workflowDispatchPlanTool";
41
46
  export const flowdeskWorkflowDispatchOption = "workflowDispatch";
42
47
  export const flowdeskControlledWriteApplyOption = "controlledWriteApply";
48
+ export const flowdeskUiProbeOption = "uiProbe";
43
49
  export const flowdeskDefaultManagedDispatchAuthorizationOption = "defaultManagedDispatchAuthorization";
44
50
  export const flowdeskWatchdogOption = "watchdog";
45
51
  export const flowdeskWatchdogTriggerToolName = "flowdesk_watchdog_trigger";
@@ -57,6 +63,9 @@ export const flowdeskWorkflowDispatchToolName = "flowdesk_workflow_dispatch";
57
63
  export const flowdeskControlledWriteApplyToolName = "flowdesk_controlled_write_apply";
58
64
  export const flowdeskAgentTaskRunOption = "agentTaskRun";
59
65
  export const flowdeskAgentTaskRunToolName = "flowdesk_agent_task_run";
66
+ export const flowdeskWorkflowSynthesisPreviewToolName = "flowdesk_workflow_synthesis_preview";
67
+ export const flowdeskAutoContinuePreviewToolName = "flowdesk_auto_continue_preview";
68
+ export const flowdeskUiProbeToolName = "flowdesk_ui_probe";
60
69
  const flowdeskChatSuggestionDuplicateWindowMs = 10_000;
61
70
  const disabledAuthority = {
62
71
  productionRegistrationEligible: false,
@@ -1175,9 +1184,11 @@ export function createFlowDeskExactModelProviderAcquisitionLiveTestOptInTools(cl
1175
1184
  }),
1176
1185
  };
1177
1186
  }
1178
- export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now), stallAlert, durableSuggestionRoot) {
1187
+ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now), stallAlert, durableSuggestionRoot, providerUsageLiveConfig) {
1179
1188
  const recentSuggestionCards = new Map();
1180
1189
  const recentStallAlerts = new Map();
1190
+ const usageAutoRefreshMaxAgeMs = 3 * 60_000;
1191
+ let lastUsageRefreshAttemptAtMs = 0;
1181
1192
  return async function message(input, output) {
1182
1193
  const inputRecord = isRecord(input) ? input : {};
1183
1194
  const partSessionID = typeof inputRecord.sessionID === "string" ? inputRecord.sessionID : "";
@@ -1192,6 +1203,40 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1192
1203
  const request = intakeRequestFromChatMessage({ ...inputRecord, ...output });
1193
1204
  const preview = previewNaturalLanguageRouting(request, session);
1194
1205
  const nowMs = clockMs(now);
1206
+ if (providerUsageLiveConfig?.durableStateRootDir && nowMs - lastUsageRefreshAttemptAtMs > 30_000) {
1207
+ lastUsageRefreshAttemptAtMs = nowMs;
1208
+ let isStale = false;
1209
+ try {
1210
+ const cachePath = join(providerUsageLiveConfig.durableStateRootDir, ".flowdesk", "ui", "provider-usage-sidebar.json");
1211
+ const cacheContent = JSON.parse(readFileSync(cachePath, "utf8"));
1212
+ if (typeof cacheContent.observed_at === "string" && nowMs - Date.parse(cacheContent.observed_at) > usageAutoRefreshMaxAgeMs)
1213
+ isStale = true;
1214
+ }
1215
+ catch {
1216
+ isStale = true;
1217
+ }
1218
+ if (isStale) {
1219
+ try {
1220
+ await executeFlowDeskProviderUsageLiveV1({ config: { ...providerUsageLiveConfig, persistSidebarCache: true }, request: { providerFamily: "all" } });
1221
+ }
1222
+ catch { }
1223
+ }
1224
+ }
1225
+ const usageTextToAppend = providerUsageLiveConfig?.durableStateRootDir &&
1226
+ providerUsageLiveConfig.appendToChat === true
1227
+ ? formatFlowDeskTuiUsageSnapshotCompactText(loadFlowDeskTuiUsageSnapshotViewV1({
1228
+ rootDir: providerUsageLiveConfig.durableStateRootDir,
1229
+ workflowId: providerUsageLiveConfig.persistWorkflowId,
1230
+ now: () => (typeof now === "function" ? now() : now),
1231
+ }))
1232
+ : undefined;
1233
+ const appendUsageCard = () => {
1234
+ if (usageTextToAppend === undefined)
1235
+ return;
1236
+ if (!Array.isArray(output.parts))
1237
+ output.parts = [];
1238
+ output.parts.push(buildTextPart(usageTextToAppend));
1239
+ };
1195
1240
  for (const [key, recordedAtMs] of recentSuggestionCards) {
1196
1241
  if (nowMs - recordedAtMs > flowdeskChatSuggestionDuplicateWindowMs ||
1197
1242
  nowMs < recordedAtMs)
@@ -1228,7 +1273,8 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1228
1273
  summary.totalLate > 0;
1229
1274
  const progressCardReady = stallAlert?.includeProgressCards === true &&
1230
1275
  summary.workflowSummaries.some((workflow) => (workflow.laneCards?.length ?? 0) > 0);
1231
- if (stalledAlertReady || lateAlertReady || progressCardReady) {
1276
+ const autoNextReady = summary.workflowSummaries.some((workflow) => workflow.autoNextReady === true);
1277
+ if (stalledAlertReady || lateAlertReady || progressCardReady || autoNextReady) {
1232
1278
  stallDedupKey = stallAlertDuplicateKey(request, summary);
1233
1279
  stallTextToAppend = stallAlertText(summary);
1234
1280
  }
@@ -1246,6 +1292,7 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1246
1292
  }
1247
1293
  };
1248
1294
  if (preview.evaluation.response.route_decision === "continue_chat") {
1295
+ appendUsageCard();
1249
1296
  appendStallCard();
1250
1297
  return;
1251
1298
  }
@@ -1256,13 +1303,16 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1256
1303
  recentSuggestionCards.set(duplicateKey, nowMs);
1257
1304
  writeDurableSuggestionPreference(durableSuggestionRoot, duplicateKey, request, preview.evaluation.response, nowMs);
1258
1305
  if (previousAtMs !== undefined &&
1259
- nowMs - previousAtMs <= flowdeskChatSuggestionDuplicateWindowMs)
1306
+ nowMs - previousAtMs <= flowdeskChatSuggestionDuplicateWindowMs) {
1307
+ appendUsageCard();
1260
1308
  return;
1309
+ }
1261
1310
  }
1262
1311
  const result = evaluateNaturalLanguageRouting(request, session);
1263
1312
  if (!Array.isArray(output.parts))
1264
1313
  output.parts = [];
1265
1314
  output.parts.push(buildTextPart(steeringText(result)));
1315
+ appendUsageCard();
1266
1316
  appendStallCard();
1267
1317
  };
1268
1318
  }
@@ -1361,12 +1411,14 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
1361
1411
  const stalledEntry = scopedEntries.find((entry) => entry.classification === "stalled");
1362
1412
  const lateEntry = scopedEntries.find((entry) => entry.classification === "progressing_late");
1363
1413
  const primary = stalledEntry ?? lateEntry;
1364
- const scopedLaneCards = (workflow.laneProgressCards ?? []).filter((lane) => lane.classification !== "terminal" &&
1414
+ const autoNextReady = workflow.laneProgressAggregate?.autoNextStepEligible === true;
1415
+ const scopedLaneCards = (workflow.laneProgressCards ?? []).filter((lane) => (autoNextReady || lane.classification !== "terminal") &&
1365
1416
  laneInCurrentSession(lane.laneId));
1366
1417
  const scopedStalledCount = scopedEntries.filter((entry) => entry.classification === "stalled").length;
1367
1418
  const scopedLateCount = scopedEntries.filter((entry) => entry.classification === "progressing_late").length;
1368
1419
  const shouldShowWorkflow = scopedStalledCount > 0 ||
1369
1420
  (stallAlert.includeProgressingLate === true && scopedLateCount > 0) ||
1421
+ (autoNextReady && scopedLaneCards.length > 0) ||
1370
1422
  (stallAlert.includeProgressCards === true && scopedLaneCards.length > 0);
1371
1423
  if (!shouldShowWorkflow)
1372
1424
  return undefined;
@@ -1443,18 +1495,36 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
1443
1495
  ...(primary?.failureHint === undefined
1444
1496
  ? {}
1445
1497
  : { failureHint: primary.failureHint }),
1498
+ ...(workflow.laneProgressAggregate === undefined
1499
+ ? {}
1500
+ : { laneProgressAggregate: workflow.laneProgressAggregate }),
1501
+ ...(autoNextReady ? { autoNextReady: true } : {}),
1502
+ ...(workflow.latestWorkflowSynthesisTasksSummarized === undefined
1503
+ ? {}
1504
+ : { synthesisTasksSummarized: workflow.latestWorkflowSynthesisTasksSummarized }),
1505
+ ...(workflow.latestWorkflowSynthesisConflictDetected === undefined
1506
+ ? {}
1507
+ : { synthesisConflictDetected: workflow.latestWorkflowSynthesisConflictDetected }),
1446
1508
  ...(stallAlert.includeProgressCards === true
1447
1509
  ? {
1448
1510
  laneCards: scopedLaneCards
1449
1511
  .slice(0, stallAlert.maxProgressCards ?? 3)
1450
1512
  .map((lane) => ({
1451
1513
  laneId: lane.laneId,
1514
+ taskId: lane.taskId,
1452
1515
  state: lane.state,
1453
1516
  classification: lane.classification,
1454
1517
  secondsSinceLastSignal: lane.secondsSinceLastSignal,
1455
1518
  agentRef: lane.agentRef,
1456
1519
  providerQualifiedModelId: lane.providerQualifiedModelId,
1520
+ promptPreview: lane.promptPreview,
1521
+ nudgeCount: lane.nudgeCount,
1522
+ progressPhase: lane.progressPhase,
1523
+ progressLabel: lane.progressLabel,
1457
1524
  verdictLabel: lane.verdictLabel,
1525
+ completionStatus: lane.completionStatus,
1526
+ outputKind: lane.outputKind,
1527
+ usableForSynthesis: lane.usableForSynthesis,
1458
1528
  failureHint: lane.failureHint,
1459
1529
  })),
1460
1530
  }
@@ -1524,6 +1594,9 @@ function stallAlertText(summary) {
1524
1594
  else if (summary.worstClassification === "progressing_late") {
1525
1595
  lines.push(`Late-progressing lanes detected: ${summary.totalLate} late, ${summary.totalStalled} stalled.`);
1526
1596
  }
1597
+ else if (summary.workflowSummaries.some((workflow) => workflow.autoNextReady === true)) {
1598
+ lines.push("All FlowDesk subtasks completed normally. Auto-next synthesis is ready.");
1599
+ }
1527
1600
  else if (progressCardCount > 0) {
1528
1601
  lines.push(`Lane progress: ${progressCardCount} lane(s) visible on the main screen.`);
1529
1602
  }
@@ -1534,19 +1607,56 @@ function stallAlertText(summary) {
1534
1607
  const secs = workflow.secondsSinceLastSignal ?? 0;
1535
1608
  const minutes = Math.floor(secs / 60);
1536
1609
  const hint = workflow.failureHint ?? "no recent heartbeat";
1537
- const counts = workflow.stalledLaneCount > 0
1538
- ? `${workflow.stalledLaneCount} stalled`
1539
- : `${workflow.lateLaneCount} progressing-late`;
1540
- lines.push(`- workflow ${workflow.workflowId}: ${counts} (last signal ~${minutes}m ago, ${hint}).`);
1610
+ if (workflow.autoNextReady === true) {
1611
+ const synthesis = workflow.synthesisTasksSummarized === undefined
1612
+ ? "synthesis not yet recorded"
1613
+ : `synthesis recorded for ${workflow.synthesisTasksSummarized} task(s)`;
1614
+ const conflict = workflow.synthesisConflictDetected === undefined
1615
+ ? " conflict=unknown"
1616
+ : workflow.synthesisConflictDetected === true ? " conflict=true" : " conflict=false";
1617
+ const nextAction = workflow.laneProgressAggregate?.nextActionAvailable === true
1618
+ ? ` next_action=${workflow.laneProgressAggregate.nextActionKind ?? "available"}_ready`
1619
+ : "";
1620
+ lines.push(`- workflow ${workflow.workflowId}: auto-next ready (${synthesis},${conflict}${nextAction}).`);
1621
+ }
1622
+ else {
1623
+ const counts = workflow.stalledLaneCount > 0
1624
+ ? `${workflow.stalledLaneCount} stalled`
1625
+ : `${workflow.lateLaneCount} progressing-late`;
1626
+ lines.push(`- workflow ${workflow.workflowId}: ${counts} (last signal ~${minutes}m ago, ${hint}).`);
1627
+ }
1628
+ if (workflow.laneProgressAggregate !== undefined) {
1629
+ const aggregate = workflow.laneProgressAggregate;
1630
+ const nextAction = aggregate.nextActionAvailable === true
1631
+ ? `, next_action=${aggregate.nextActionKind ?? "available"}_ready`
1632
+ : "";
1633
+ lines.push(` tasks: expected=${aggregate.expected}, terminal=${aggregate.terminal}, completed=${aggregate.normalCompleted}, failed=${aggregate.failed}, awaiting_permission=${aggregate.awaitingPermission}, auto_next=${aggregate.autoNextStepEligible}${nextAction}`);
1634
+ }
1541
1635
  for (const lane of workflow.laneCards?.slice(0, 3) ?? []) {
1542
1636
  const age = lane.secondsSinceLastSignal === undefined
1543
1637
  ? "unknown"
1544
1638
  : `~${Math.floor(lane.secondsSinceLastSignal / 60)}m ago`;
1545
1639
  const model = lane.providerQualifiedModelId ?? "(unknown)";
1546
1640
  const agent = lane.agentRef ?? "(unknown)";
1641
+ const task = lane.taskId ?? lane.laneId;
1642
+ const prompt = lane.promptPreview ?? "(hidden)";
1643
+ const nudge = lane.nudgeCount === undefined ? "?" : String(lane.nudgeCount);
1644
+ const progress = lane.progressLabel === undefined
1645
+ ? "(none)"
1646
+ : `${lane.progressPhase ?? "progress"}: ${lane.progressLabel}`;
1547
1647
  const verdict = lane.verdictLabel ?? "(none)";
1648
+ const resultQuality = lane.completionStatus === undefined && lane.outputKind === undefined && lane.usableForSynthesis === undefined
1649
+ ? "(none)"
1650
+ : `${lane.completionStatus ?? "unknown"}/${lane.outputKind ?? "unknown"}/synthesis=${lane.usableForSynthesis === undefined ? "unknown" : String(lane.usableForSynthesis)}`;
1548
1651
  const issue = lane.failureHint === undefined ? "" : ` issue=${lane.failureHint}`;
1549
- lines.push(` - lane ${lane.laneId}: ${lane.state ?? "unknown"}/${lane.classification}, last signal ${age}, agent=${agent}, model=${model}, verdict=${verdict}${issue}`);
1652
+ lines.push(` - lane ${lane.laneId}: ${lane.state ?? "unknown"}/${lane.classification}`);
1653
+ lines.push(` task: ${task}`);
1654
+ lines.push(` prompt: ${prompt}`);
1655
+ lines.push(` agent: ${agent}`);
1656
+ lines.push(` model: ${model}`);
1657
+ lines.push(` progress: ${progress}`);
1658
+ lines.push(` result: ${resultQuality}`);
1659
+ lines.push(` last signal: ${age}; nudges=${nudge}; verdict=${verdict}${issue}`);
1550
1660
  }
1551
1661
  }
1552
1662
  if (summary.autoAbortSummaries !== undefined && summary.autoAbortSummaries.length > 0) {
@@ -2074,6 +2184,32 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
2074
2184
  return undefined;
2075
2185
  const client = input.client;
2076
2186
  const rootDir = input.durableStateRoot;
2187
+ const promptPreview = (text, max = 120) => {
2188
+ const compact = text.replace(/\s+/g, " ").trim();
2189
+ return compact.length > max ? `${compact.slice(0, max - 1)}…` : compact;
2190
+ };
2191
+ const taskSummaryForUser = (input) => {
2192
+ const lines = [
2193
+ input.status === "task_completed"
2194
+ ? "FlowDesk task completed."
2195
+ : input.status === "task_launched"
2196
+ ? "FlowDesk task launched."
2197
+ : "FlowDesk task failed.",
2198
+ `workflow: ${input.workflowId}`,
2199
+ `lane: ${input.laneId}`,
2200
+ `task: ${input.taskId}`,
2201
+ `prompt: ${promptPreview(input.promptText)}`,
2202
+ `agent: ${input.agentName}`,
2203
+ `model: ${input.providerQualifiedModelId}`,
2204
+ ];
2205
+ if (input.asyncMode === true)
2206
+ lines.push("progress: use /flowdesk-status or flowdesk_status_live to follow durable lane evidence");
2207
+ if (input.resultText !== undefined)
2208
+ lines.push(`result: ${promptPreview(input.resultText, 200)}`);
2209
+ if (input.failureCategory !== undefined)
2210
+ lines.push(`failure: ${input.failureCategory}${input.redactedReason === undefined ? "" : ` (${input.redactedReason})`}`);
2211
+ return lines.join("\n");
2212
+ };
2077
2213
  return {
2078
2214
  [flowdeskAgentTaskRunToolName]: tool({
2079
2215
  description: [
@@ -2081,7 +2217,7 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
2081
2217
  "Use this to delegate a well-defined subtask to a specific model (e.g. Claude Opus for security analysis, GPT for architecture review).",
2082
2218
  "Requires developerModeAcknowledged=true and allowProviderCall=true per call.",
2083
2219
  "WHEN TO USE: user asks to delegate a specific task to a specific model/agent.",
2084
- "WHEN NOT TO USE: multi-step workflows (use flowdesk_workflow_dispatch), code review (use flowdesk_quick_reviewer_run).",
2220
+ "WHEN NOT TO USE: multi-step workflows (use flowdesk_workflow_dispatch).",
2085
2221
  "After calling, use flowdesk_status_live to check the lane status.",
2086
2222
  ].join(" "),
2087
2223
  args: {
@@ -2092,6 +2228,8 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
2092
2228
  parentSessionId: tool.schema.string().optional().describe("Parent session id"),
2093
2229
  developerModeAcknowledged: tool.schema.boolean(),
2094
2230
  allowProviderCall: tool.schema.boolean(),
2231
+ nudgeQuietPeriodMs: tool.schema.number().optional().describe("Milliseconds of silence before sending a nudge prompt. Default 20000ms (20s). Recommended: always pass 20000. At 20s silence → nudge 1, 40s → nudge 2, 60s+ → lane fails and watchdog retries."),
2232
+ asyncMode: tool.schema.boolean().optional().describe("When true, return laneId immediately after launch. Watchdog polls child session, sends noReply nudges at 20s/40s, and aborts at 60s+. Coordinator uses flowdesk_status_live to detect completion. Recommended for all orchestration calls."),
2095
2233
  },
2096
2234
  async execute(args, ctx) {
2097
2235
  const record = isRecord(args) ? args : {};
@@ -2111,6 +2249,9 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
2111
2249
  : typeof ctxRecord.sessionID === "string" && ctxRecord.sessionID.length > 0
2112
2250
  ? ctxRecord.sessionID
2113
2251
  : "";
2252
+ const nudgeQuietPeriodMs = typeof record.nudgeQuietPeriodMs === "number" && record.nudgeQuietPeriodMs > 0
2253
+ ? Math.floor(record.nudgeQuietPeriodMs) : undefined;
2254
+ const asyncMode = record.asyncMode === true;
2114
2255
  const taskId = `task-${Date.now().toString(36)}`;
2115
2256
  const laneId = `lane-task-${Date.now().toString(36)}`;
2116
2257
  const result = await executeFlowDeskAgentTaskV1({
@@ -2123,19 +2264,37 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
2123
2264
  parentSessionId,
2124
2265
  rootDir,
2125
2266
  client,
2267
+ asyncMode,
2268
+ _nudgeQuietPeriodMs: nudgeQuietPeriodMs,
2126
2269
  });
2270
+ const failureCategory = result.status === "task_failed" ? result.failureCategory : undefined;
2271
+ const redactedReason = result.status === "task_failed" ? result.redactedReason : undefined;
2127
2272
  return JSON.stringify({
2128
2273
  workflowId,
2129
2274
  laneId,
2130
2275
  taskId,
2131
2276
  status: result.status,
2277
+ taskPreview: promptPreview(taskDescription),
2278
+ agentName,
2279
+ providerQualifiedModelId,
2280
+ ...(result.status === "task_launched" ? { childSessionId: result.childSessionId, asyncMode: true, safeNextActions: ["/flowdesk-status"] } : {}),
2132
2281
  resultText: result.status === "task_completed" ? result.resultText.slice(0, 4_096) : undefined,
2133
2282
  resultTruncated: result.status === "task_completed" && result.resultText.length > 4_096,
2134
- failureCategory: result.status === "task_failed" ? result.failureCategory : undefined,
2135
- redactedReason: result.status === "task_failed" ? result.redactedReason : undefined,
2136
- summaryForUser: result.status === "task_completed"
2137
- ? `Task completed on ${agentName} (${providerQualifiedModelId}). Result: ${result.resultText.slice(0, 200)}${result.resultText.length > 200 ? "..." : ""}`
2138
- : `Task failed on ${agentName}: ${result.failureCategory}`,
2283
+ failureCategory,
2284
+ redactedReason,
2285
+ summaryForUser: taskSummaryForUser({
2286
+ status: result.status,
2287
+ workflowId,
2288
+ laneId,
2289
+ taskId,
2290
+ agentName,
2291
+ providerQualifiedModelId,
2292
+ promptText: taskDescription,
2293
+ ...(result.status === "task_completed" ? { resultText: result.resultText } : {}),
2294
+ ...(result.status === "task_launched" ? { asyncMode: true } : {}),
2295
+ ...(failureCategory === undefined ? {} : { failureCategory }),
2296
+ ...(redactedReason === undefined ? {} : { redactedReason }),
2297
+ }),
2139
2298
  });
2140
2299
  },
2141
2300
  }),
@@ -2317,6 +2476,8 @@ function providerUsageLiveConfigFromOptions(options) {
2317
2476
  config.geminiProjectId = value.geminiProjectId;
2318
2477
  if (value.persistSnapshots === true)
2319
2478
  config.persistSnapshots = true;
2479
+ if (value.appendToChat === true)
2480
+ config.appendToChat = true;
2320
2481
  const explicitRoot = typeof value.durableStateRootDir === "string" &&
2321
2482
  value.durableStateRootDir.trim().length > 0
2322
2483
  ? value.durableStateRootDir
@@ -2474,6 +2635,67 @@ function controlledWriteApplyConfigFromOptions(input, options) {
2474
2635
  const workspaceRoot = optionWorkspaceRoot ?? inputWorkspaceRoot ?? process.cwd();
2475
2636
  return { durableStateRoot, workspaceRoot };
2476
2637
  }
2638
+ export const flowdeskOrchestrateToolName = "flowdesk_orchestrate";
2639
+ export const flowdeskOrchestrateOption = "workflowOrchestrate";
2640
+ function orchestrateToolConfigFromOptions(input, options) {
2641
+ const value = options?.[flowdeskOrchestrateOption];
2642
+ if (!isRecord(value) || value.enabled !== true || value.devBetaActualLaneLaunch !== true)
2643
+ return undefined;
2644
+ const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0 ? value.rootDir : undefined;
2645
+ const rootDir = explicitRoot ?? durableStateRootFromOptions(options);
2646
+ if (rootDir === undefined)
2647
+ return undefined;
2648
+ const client = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
2649
+ return client === undefined ? undefined : { rootDir, client };
2650
+ }
2651
+ function createFlowDeskOrchestrateOptInTools(config) {
2652
+ return {
2653
+ [flowdeskOrchestrateToolName]: tool({
2654
+ description: [
2655
+ "Run a full FlowDesk multi-task orchestration: Author → Assign → Schedule → Synthesize.",
2656
+ "WHEN TO USE: the user gives a natural-language goal that requires decomposing into multiple tasks, assigning agents/models based on provider usage, executing them in dependency order, and aggregating results.",
2657
+ "INVOKE WITH: goalSummary (clear bounded goal text), parentSessionId, and optional workflowId.",
2658
+ "AFTER CALLING: surface summaryForUser and report synthesisId, taskCount, conflictDetected. All authority flags remain false.",
2659
+ "WHEN NOT TO USE: simple single-task requests, status checks, usage queries, or review fan-out.",
2660
+ ].join(" "),
2661
+ args: {
2662
+ goalSummary: tool.schema.string().describe("Bounded natural-language goal for the workflow. Max 500 chars."),
2663
+ parentSessionId: tool.schema.string().describe("Current OpenCode session id (ses-...)."),
2664
+ workflowId: tool.schema.string().optional().describe("Optional stable workflow id. Auto-generated when omitted."),
2665
+ providerQualifiedModelId: tool.schema.string().optional().describe("Concrete provider/model id for author and synthesis calls. Defaults to openai/gpt-5.5."),
2666
+ agentName: tool.schema.string().optional().describe("Agent name for author and synthesis calls. Defaults to reviewer-gpt-frontier."),
2667
+ developerModeAcknowledged: tool.schema.boolean().describe("Must be true to allow provider calls."),
2668
+ allowProviderCall: tool.schema.boolean().describe("Must be true to allow provider calls."),
2669
+ allowActualLaneLaunch: tool.schema.boolean().describe("Must be true to allow actual lane launch."),
2670
+ },
2671
+ async execute(input) {
2672
+ if (!isRecord(input))
2673
+ return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "invalid input", safeNextActions: ["/flowdesk-doctor"] });
2674
+ if (input.developerModeAcknowledged !== true || input.allowProviderCall !== true || input.allowActualLaneLaunch !== true)
2675
+ return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "developerModeAcknowledged, allowProviderCall, and allowActualLaneLaunch must all be true", safeNextActions: ["/flowdesk-doctor"] });
2676
+ const goalSummary = typeof input.goalSummary === "string" ? input.goalSummary.slice(0, 500) : "";
2677
+ if (!goalSummary.trim())
2678
+ return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "goalSummary is required", safeNextActions: ["/flowdesk-doctor"] });
2679
+ const parentSessionId = typeof input.parentSessionId === "string" ? input.parentSessionId : "";
2680
+ if (!parentSessionId.trim())
2681
+ return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "parentSessionId is required", safeNextActions: ["/flowdesk-doctor"] });
2682
+ const providerQualifiedModelId = typeof input.providerQualifiedModelId === "string" ? input.providerQualifiedModelId : "openai/gpt-5.5";
2683
+ const agentName = typeof input.agentName === "string" ? input.agentName : "reviewer-gpt-frontier";
2684
+ const workflowId = typeof input.workflowId === "string" ? input.workflowId : undefined;
2685
+ const result = await executeFlowDeskWorkflowOrchestratorV1({
2686
+ workflowId,
2687
+ goalSummary,
2688
+ parentSessionId,
2689
+ rootDir: config.rootDir,
2690
+ client: config.client,
2691
+ providerQualifiedModelId,
2692
+ agentName,
2693
+ });
2694
+ return JSON.stringify(result);
2695
+ },
2696
+ }),
2697
+ };
2698
+ }
2477
2699
  export function createFlowDeskWorkflowDispatchPlanOptInTools(config) {
2478
2700
  return {
2479
2701
  [flowdeskWorkflowDispatchPlanToolName]: tool({
@@ -2516,6 +2738,33 @@ export function createFlowDeskWorkflowDispatchPlanOptInTools(config) {
2516
2738
  }),
2517
2739
  };
2518
2740
  }
2741
+ export function createFlowDeskAutoContinuePreviewOptInTools(config) {
2742
+ return {
2743
+ [flowdeskAutoContinuePreviewToolName]: tool({
2744
+ description: [
2745
+ "Preview the next pending task from durable FlowDesk workflow_dispatch_plan evidence without executing it.",
2746
+ "WHEN TO USE: the user asks whether FlowDesk can continue remaining planned work or asks what would run next from a durable plan.",
2747
+ "WHEN NOT TO USE: requests to actually dispatch, call a provider, launch a lane, fallback/reselect, mutate TUI/chat, or write files. This tool is preview-only and never performs continuation.",
2748
+ "INVOKE WITH: workflowId and optional maxSteps. The configured durable state root is used. Todo source is durable workflow_dispatch_plan evidence only, not transient chat/todowrite state.",
2749
+ "AFTER CALLING: surface summaryForUser, nextTaskId/title/summary, pendingTaskCount, and authority. Never claim automatic execution, dispatch, provider call, fallback, hard chat, or TUI action authority.",
2750
+ ].join(" "),
2751
+ args: {
2752
+ workflowId: tool.schema.string().describe("Workflow id with durable workflow_dispatch_plan evidence."),
2753
+ maxSteps: tool.schema.number().optional().describe("Bounded preview cap. Clamped to 1..5; no steps are executed."),
2754
+ },
2755
+ async execute(input) {
2756
+ const result = executeFlowDeskAutoContinuePreviewToolV1({
2757
+ config,
2758
+ request: {
2759
+ workflowId: typeof input.workflowId === "string" ? input.workflowId : undefined,
2760
+ maxSteps: typeof input.maxSteps === "number" ? input.maxSteps : undefined,
2761
+ },
2762
+ });
2763
+ return JSON.stringify(result);
2764
+ },
2765
+ }),
2766
+ };
2767
+ }
2519
2768
  export function createFlowDeskWorkflowDispatchOptInTools(config) {
2520
2769
  return {
2521
2770
  [flowdeskWorkflowDispatchToolName]: tool({
@@ -2769,6 +3018,134 @@ export function createFlowDeskStatusLiveOptInTools(config) {
2769
3018
  }),
2770
3019
  };
2771
3020
  }
3021
+ export function createFlowDeskWorkflowSynthesisPreviewTools(config) {
3022
+ return {
3023
+ [flowdeskWorkflowSynthesisPreviewToolName]: tool({
3024
+ description: [
3025
+ "Build and persist a provider-free FlowDesk synthesis preview from existing task_result evidence. This local tool never calls providers, launches lanes, dispatches runtime work, performs fallback/reselection, writes workspace files, or grants execution authority.",
3026
+ "WHEN TO USE: a workflow reports next_action=synthesis_ready and the user asks to summarize, synthesize, continue with the next synthesis step, or see the completed task results without making a model/provider call.",
3027
+ "WHEN NOT TO USE: requests for model-authored synthesis, real dispatch, provider calls, write/apply, fallback/retry, or replacing raw task results.",
3028
+ "INVOKE WITH: workflowId for a workflow that already has task_result evidence under the configured durable root.",
3029
+ "AFTER CALLING: surface summaryForUser, synthesisId, tasksSummarized, conflictDetected, safeNextActions, and authority. State that this is a provider-free preview derived from durable task_result evidence.",
3030
+ ].join(" "),
3031
+ args: {
3032
+ workflowId: tool.schema.string().describe("Workflow id with existing task_result evidence to synthesize locally."),
3033
+ },
3034
+ async execute(input) {
3035
+ const workflowId = isRecord(input) && typeof input.workflowId === "string" ? input.workflowId : "";
3036
+ const result = executeFlowDeskWorkflowSynthesisPreviewV1({ workflowId, rootDir: config.rootDir });
3037
+ return JSON.stringify(result);
3038
+ },
3039
+ }),
3040
+ };
3041
+ }
3042
+ function isFlowDeskUiProbeEnabled(options) {
3043
+ const raw = options?.[flowdeskUiProbeOption];
3044
+ return process.env.FLOWDESK_UI_PROBE === "1" || raw === true || (isRecord(raw) && raw.enabled === true);
3045
+ }
3046
+ function boundedUiProbeString(value, maxLength = 160) {
3047
+ if (typeof value !== "string" || value.length === 0)
3048
+ return undefined;
3049
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
3050
+ }
3051
+ function summarizeFlowDeskUiProbeEventV1(event, observedAt) {
3052
+ const rec = isRecord(event) ? event : {};
3053
+ const properties = Object.keys(rec).sort().slice(0, 20);
3054
+ const nested = (name) => {
3055
+ const value = rec[name];
3056
+ return isRecord(value) ? value : undefined;
3057
+ };
3058
+ const session = nested("session") ?? nested("sessionID") ?? nested("sessionId");
3059
+ const message = nested("message");
3060
+ const part = nested("part");
3061
+ const sessionId = boundedUiProbeString(rec.sessionID) ??
3062
+ boundedUiProbeString(rec.sessionId) ??
3063
+ boundedUiProbeString(session?.id);
3064
+ return {
3065
+ observedAt,
3066
+ eventType: boundedUiProbeString(rec.type, 120) ?? boundedUiProbeString(rec.event, 120) ?? "unknown",
3067
+ ...(sessionId === undefined ? {} : { sessionId }),
3068
+ ...(boundedUiProbeString(message?.id) === undefined ? {} : { messageId: boundedUiProbeString(message?.id) }),
3069
+ ...(boundedUiProbeString(part?.id) === undefined ? {} : { partId: boundedUiProbeString(part?.id) }),
3070
+ keys: properties,
3071
+ };
3072
+ }
3073
+ function createFlowDeskUiProbeTools(observations) {
3074
+ return {
3075
+ [flowdeskUiProbeToolName]: tool({
3076
+ description: [
3077
+ "Developer-only FlowDesk UI renderer/event probe. Enabled only when FLOWDESK_UI_PROBE=1.",
3078
+ "It emits bounded markdown/details/link/code-fence/long-log samples and reports redacted OpenCode event observations captured by the plugin event hook.",
3079
+ "No dispatch, provider call, write authority, fallback authority, or hard chat control is granted.",
3080
+ ].join(" "),
3081
+ args: {
3082
+ mode: tool.schema
3083
+ .string()
3084
+ .optional()
3085
+ .describe("Probe mode: render, events, or all. Defaults to all."),
3086
+ limit: tool.schema
3087
+ .number()
3088
+ .optional()
3089
+ .describe("Maximum recent event observations to return; clamped to 1..50."),
3090
+ },
3091
+ async execute(input) {
3092
+ const mode = isRecord(input) && typeof input.mode === "string" ? input.mode : "all";
3093
+ const requestedLimit = isRecord(input) && typeof input.limit === "number" ? Math.floor(input.limit) : 20;
3094
+ const limit = Math.max(1, Math.min(50, requestedLimit));
3095
+ const includeRender = mode === "all" || mode === "render";
3096
+ const includeEvents = mode === "all" || mode === "events";
3097
+ const longLog = Array.from({ length: 40 }, (_, index) => {
3098
+ const n = String(index + 1).padStart(2, "0");
3099
+ return `${n}. ui-probe timeline event ${n}: message/status/log renderer sample`;
3100
+ });
3101
+ return JSON.stringify({
3102
+ schema_version: "flowdesk.ui_probe_result.v1",
3103
+ status: "ui_probe_collected",
3104
+ probeEnabled: true,
3105
+ mode,
3106
+ authority: {
3107
+ realOpenCodeDispatch: false,
3108
+ providerCall: false,
3109
+ runtimeExecution: false,
3110
+ actualLaneLaunch: false,
3111
+ fallbackAuthority: false,
3112
+ hardCancelOrNoReplyAuthority: false,
3113
+ },
3114
+ ...(includeRender
3115
+ ? {
3116
+ renderSamples: {
3117
+ markdownDetails: [
3118
+ "<details>",
3119
+ "<summary>FlowDesk UI probe: details summary</summary>",
3120
+ "",
3121
+ "[FlowDesk markdown link probe](https://example.com/flowdesk-ui-probe)",
3122
+ "",
3123
+ "```text",
3124
+ ...longLog.slice(0, 8),
3125
+ "```",
3126
+ "</details>",
3127
+ ].join("\n"),
3128
+ longLogText: ["FlowDesk UI probe long log", ...longLog].join("\n"),
3129
+ observationInstructions: [
3130
+ "1. Check whether markdownDetails renders as collapsible UI or plain text.",
3131
+ "2. Click the markdown link and re-run mode=events to see whether any click/navigation event reached Hooks.event.",
3132
+ "3. Check whether longLogText is folded, scrollable, or fully expanded by OpenCode's tool-output renderer.",
3133
+ ].join("\n"),
3134
+ },
3135
+ }
3136
+ : {}),
3137
+ ...(includeEvents
3138
+ ? {
3139
+ recentEventObservations: observations.slice(-limit),
3140
+ recentEventObservationCount: Math.min(observations.length, limit),
3141
+ totalBufferedEventObservationCount: observations.length,
3142
+ }
3143
+ : {}),
3144
+ });
3145
+ },
3146
+ }),
3147
+ };
3148
+ }
2772
3149
  export function createFlowDeskProviderUsageLiveOptInTools(config) {
2773
3150
  return {
2774
3151
  [flowdeskProviderUsageLiveToolName]: tool({
@@ -2778,7 +3155,7 @@ export function createFlowDeskProviderUsageLiveOptInTools(config) {
2778
3155
  "ALSO PROACTIVELY USE: before starting a large multi-step task that depends on a specific provider (e.g. extensive refactor, long agentic loop, multi-perspective review), call this tool first to check whether the chosen provider has enough headroom; if worstAlertLevel is critical or exhausted, warn the user and suggest switching providers or waiting for reset.",
2779
3156
  "WHEN NOT TO USE: general chat, status of an in-progress workflow (use status instead), or any non-usage question.",
2780
3157
  "INVOKE WITH: optional providerFamily ('claude', 'openai', 'gemini', or 'all'; default 'all'). The plugin user has already opted in to provider-native usage collection at configuration time, so this tool can be called automatically without per-call confirmation.",
2781
- "AFTER CALLING: summarize per-provider availability for the user in plain language. For each provider include the bucket label (claude-5h, claude-weekly, openai-gpt-5h, gemini-pro-5h, gemini-pro-weekly, gemini-flash-daily, gemini-flash-lite-daily), remainingPercent, reset time, alertLevel, and recommendation. If any provider returned non_dispatchable, exhausted, critical, stale, or unknown, note it explicitly and surface the overallRecommendation. Never echo raw tokens or raw payloads.",
3158
+ "AFTER CALLING: summarize per-provider availability for the user in plain language. For each provider include the bucket label (claude-5h, claude-weekly, openai-gpt-5h, gemini-pro-daily, gemini-pro-weekly, gemini-flash-daily, gemini-flash-lite-daily), remainingPercent, reset time, alertLevel, and recommendation. If any provider returned non_dispatchable, exhausted, critical, stale, or unknown, note it explicitly and surface the overallRecommendation. Never echo raw tokens or raw payloads.",
2782
3159
  ].join(" "),
2783
3160
  args: {
2784
3161
  providerFamily: tool.schema
@@ -3076,8 +3453,10 @@ const flowdeskServerPlugin = async (input, options) => {
3076
3453
  const statusLiveConfig = isStatusLiveEnabled(options)
3077
3454
  ? statusLiveConfigFromOptions(options)
3078
3455
  : undefined;
3079
- if (statusLiveConfig !== undefined)
3456
+ if (statusLiveConfig !== undefined) {
3080
3457
  Object.assign(tools, createFlowDeskStatusLiveOptInTools(statusLiveConfig));
3458
+ Object.assign(tools, createFlowDeskWorkflowSynthesisPreviewTools({ rootDir: statusLiveConfig.rootDir }));
3459
+ }
3081
3460
  const quickFallbackRunConfig = isQuickFallbackRunEnabled(options)
3082
3461
  ? quickFallbackRunConfigFromOptions(options)
3083
3462
  : undefined;
@@ -3093,6 +3472,8 @@ const flowdeskServerPlugin = async (input, options) => {
3093
3472
  : undefined;
3094
3473
  if (workflowDispatchPlanConfig !== undefined)
3095
3474
  Object.assign(tools, createFlowDeskWorkflowDispatchPlanOptInTools(workflowDispatchPlanConfig));
3475
+ if (workflowDispatchPlanConfig !== undefined)
3476
+ Object.assign(tools, createFlowDeskAutoContinuePreviewOptInTools(workflowDispatchPlanConfig));
3096
3477
  const workflowDispatchConfig = workflowDispatchToolConfigFromOptions(input, options);
3097
3478
  if (workflowDispatchConfig !== undefined)
3098
3479
  Object.assign(tools, createFlowDeskWorkflowDispatchOptInTools(workflowDispatchConfig));
@@ -3110,6 +3491,13 @@ const flowdeskServerPlugin = async (input, options) => {
3110
3491
  if (agentTaskRunTools !== undefined)
3111
3492
  Object.assign(tools, agentTaskRunTools);
3112
3493
  }
3494
+ const orchestrateConfig = orchestrateToolConfigFromOptions(input, options);
3495
+ if (orchestrateConfig !== undefined)
3496
+ Object.assign(tools, createFlowDeskOrchestrateOptInTools(orchestrateConfig));
3497
+ const uiProbeEnabled = isFlowDeskUiProbeEnabled(options);
3498
+ const uiProbeEventObservations = [];
3499
+ if (uiProbeEnabled)
3500
+ Object.assign(tools, createFlowDeskUiProbeTools(uiProbeEventObservations));
3113
3501
  // P8 Background Watchdog
3114
3502
  const watchdogConfig = watchdogConfigFromOptions(options);
3115
3503
  const chatStallAlertRaw = options?.[flowdeskChatMessageStallAlertOption];
@@ -3210,14 +3598,54 @@ const flowdeskServerPlugin = async (input, options) => {
3210
3598
  },
3211
3599
  });
3212
3600
  }
3601
+ const eventRootDir = durableStateRootFromOptions(options);
3602
+ const eventHook = eventRootDir === undefined && !uiProbeEnabled
3603
+ ? undefined
3604
+ : async (input) => {
3605
+ if (uiProbeEnabled) {
3606
+ uiProbeEventObservations.push(summarizeFlowDeskUiProbeEventV1(input.event, new Date().toISOString()));
3607
+ if (uiProbeEventObservations.length > 200)
3608
+ uiProbeEventObservations.splice(0, uiProbeEventObservations.length - 200);
3609
+ }
3610
+ if (eventRootDir !== undefined) {
3611
+ await observeFlowDeskOpenCodeEventV1({ rootDir: eventRootDir, event: input.event });
3612
+ }
3613
+ };
3213
3614
  if (!naturalLanguageRoutingEnabled)
3214
- return { tool: tools };
3615
+ return eventHook === undefined ? { tool: tools } : { tool: tools, event: eventHook };
3215
3616
  const stallAlertOption = chatMessageStallAlertOptionsFrom(options, statusLiveConfig, isRecord(input) && isManagedDispatchBetaClient(input.client)
3216
3617
  ? input.client
3217
3618
  : undefined);
3619
+ // Background sidebar cache refresh interval — runs every 3 minutes regardless of chat activity
3620
+ if (providerUsageLiveConfig?.durableStateRootDir) {
3621
+ const capturedUsageConfig = providerUsageLiveConfig;
3622
+ const sidebarRefreshIntervalMs = 3 * 60_000;
3623
+ const sidebarRefreshInterval = setInterval(() => {
3624
+ const cachePath = join(capturedUsageConfig.durableStateRootDir, ".flowdesk", "ui", "provider-usage-sidebar.json");
3625
+ let isStale = false;
3626
+ try {
3627
+ const cache = JSON.parse(readFileSync(cachePath, "utf8"));
3628
+ if (typeof cache.observed_at === "string" && Date.now() - Date.parse(cache.observed_at) > sidebarRefreshIntervalMs)
3629
+ isStale = true;
3630
+ }
3631
+ catch {
3632
+ isStale = true;
3633
+ }
3634
+ if (isStale) {
3635
+ executeFlowDeskProviderUsageLiveV1({
3636
+ config: { ...capturedUsageConfig, persistSidebarCache: true },
3637
+ request: { providerFamily: "all" },
3638
+ }).catch(() => { });
3639
+ }
3640
+ }, sidebarRefreshIntervalMs);
3641
+ sidebarRefreshInterval.unref();
3642
+ process.once("exit", () => clearInterval(sidebarRefreshInterval));
3643
+ process.once("SIGTERM", () => clearInterval(sidebarRefreshInterval));
3644
+ }
3218
3645
  return {
3219
3646
  tool: tools,
3220
- "chat.message": createFlowDeskNaturalLanguageChatMessageHook(() => new Date(), localSession, stallAlertOption, durableStateRootFromOptions(options)),
3647
+ ...(eventHook === undefined ? {} : { event: eventHook }),
3648
+ "chat.message": createFlowDeskNaturalLanguageChatMessageHook(() => new Date(), localSession, stallAlertOption, durableStateRootFromOptions(options), providerUsageLiveConfig),
3221
3649
  };
3222
3650
  };
3223
3651
  export const flowdeskChatMessageStallAlertOption = "chatMessageStallAlert";