pilotswarm 0.5.62 → 0.5.64

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 (93) hide show
  1. package/mcp/dist/src/server.d.ts.map +1 -1
  2. package/mcp/dist/src/server.js +2 -0
  3. package/mcp/dist/src/server.js.map +1 -1
  4. package/mcp/dist/src/tools/features.d.ts +4 -0
  5. package/mcp/dist/src/tools/features.d.ts.map +1 -0
  6. package/mcp/dist/src/tools/features.js +35 -0
  7. package/mcp/dist/src/tools/features.js.map +1 -0
  8. package/package.json +2 -2
  9. package/tui/src/node-sdk-transport.js +13 -0
  10. package/ui/core/src/controller.js +330 -14
  11. package/ui/core/src/history.js +89 -14
  12. package/ui/core/src/moa.js +9 -0
  13. package/ui/core/src/native-tasks.js +197 -0
  14. package/ui/core/src/question-display.js +13 -0
  15. package/ui/core/src/reducer.js +31 -7
  16. package/ui/core/src/selectors.js +46 -13
  17. package/ui/core/src/session-errors.js +13 -6
  18. package/ui/core/src/session-warning.js +14 -0
  19. package/ui/core/src/state.js +7 -2
  20. package/ui/react/src/components.js +2 -1
  21. package/ui/react/src/feature-flags-panel.js +186 -0
  22. package/ui/react/src/native-task-card.js +59 -0
  23. package/ui/react/src/web-app.js +173 -33
  24. package/web/dist/assets/{abnfDiagram-VRR7QNED-DAiG41W-.js → abnfDiagram-VRR7QNED-gIPHF2gV.js} +1 -1
  25. package/web/dist/assets/{arc-CN_GY3DG.js → arc-DMW_q5Ml.js} +1 -1
  26. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DD9qiBOj.js → architectureDiagram-ZJ3FMSHR-DAKsrxcB.js} +1 -1
  27. package/web/dist/assets/{blockDiagram-677ZJIJ3-CZg2R8i6.js → blockDiagram-677ZJIJ3-BUjSLpP3.js} +1 -1
  28. package/web/dist/assets/{c4Diagram-LMCZKHZV-DuZxUZYd.js → c4Diagram-LMCZKHZV-Dw2nPoes.js} +1 -1
  29. package/web/dist/assets/channel-CcOa1LtY.js +1 -0
  30. package/web/dist/assets/{chunk-2Q5K7J3B-BX7lDvmf.js → chunk-2Q5K7J3B-BAL28Rv0.js} +1 -1
  31. package/web/dist/assets/{chunk-32BRIVSS-COupwzAw.js → chunk-32BRIVSS-DDlt9K8s.js} +1 -1
  32. package/web/dist/assets/{chunk-5VM5RSS4-C_IyERib.js → chunk-5VM5RSS4-BLQGJGoP.js} +1 -1
  33. package/web/dist/assets/{chunk-EX3LRPZG-CyfIuYKn.js → chunk-EX3LRPZG-XFMb3HRy.js} +1 -1
  34. package/web/dist/assets/{chunk-JWPE2WC7-Cj5lvEfN.js → chunk-JWPE2WC7-DWnKTQ72.js} +1 -1
  35. package/web/dist/assets/{chunk-MOJQB5TN-l907LpMA.js → chunk-MOJQB5TN-MltzhnwP.js} +1 -1
  36. package/web/dist/assets/{chunk-RYQCIY6F-PLru1fOZ.js → chunk-RYQCIY6F-Ce3q0-mJ.js} +1 -1
  37. package/web/dist/assets/{chunk-V7JOEXUC-C6GCxiE5.js → chunk-V7JOEXUC-ThXLYY88.js} +1 -1
  38. package/web/dist/assets/{chunk-VR4S4FIN-BGelrIxQ.js → chunk-VR4S4FIN-DbhAykiW.js} +1 -1
  39. package/web/dist/assets/{chunk-XXDRQBXY-BmJstjEv.js → chunk-XXDRQBXY-BpkNB0mD.js} +1 -1
  40. package/web/dist/assets/classDiagram-OUVF2IWQ-B2VhHRSV.js +1 -0
  41. package/web/dist/assets/classDiagram-v2-EOCWNBFH-B2VhHRSV.js +1 -0
  42. package/web/dist/assets/{cose-bilkent-JH36ORCC-CRabxOOK.js → cose-bilkent-JH36ORCC-rctqAPvj.js} +1 -1
  43. package/web/dist/assets/{cynefin-VYW2F7L2-CminxBoG.js → cynefin-VYW2F7L2-Dg1cltg6.js} +1 -1
  44. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-yBazsQf4.js → cynefinDiagram-TSTJHNR4-Ci23Sv4Z.js} +1 -1
  45. package/web/dist/assets/{dagre-VKFMJZFB-DcZuZV2n.js → dagre-VKFMJZFB-BpWTGhpv.js} +1 -1
  46. package/web/dist/assets/{diagram-FQU43EPY-CHoFzsw6.js → diagram-FQU43EPY-B69JpiAe.js} +1 -1
  47. package/web/dist/assets/{diagram-G47NLZAW-B8leqMRl.js → diagram-G47NLZAW-BHMk1TOO.js} +1 -1
  48. package/web/dist/assets/{diagram-NH7WQ7WH-DNoVxpa3.js → diagram-NH7WQ7WH-CtvZIIsk.js} +1 -1
  49. package/web/dist/assets/{diagram-OA4YK3LP-gC3Q2X6-.js → diagram-OA4YK3LP-BFvKh800.js} +1 -1
  50. package/web/dist/assets/{diagram-WEI45ONY-CABqyjDl.js → diagram-WEI45ONY-CgB8sWPe.js} +1 -1
  51. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-cSguXaDC.js → ebnfDiagram-CCIWWBDH-DyqPNydM.js} +1 -1
  52. package/web/dist/assets/{erDiagram-Q63AITRT-CoWX37V3.js → erDiagram-Q63AITRT-CwpvZQ3Q.js} +1 -1
  53. package/web/dist/assets/{flowDiagram-23GEKE2U-I9DHYjhG.js → flowDiagram-23GEKE2U-EBKSKxfC.js} +1 -1
  54. package/web/dist/assets/{ganttDiagram-NO4QXBWP-DaFnfYTt.js → ganttDiagram-NO4QXBWP-q5tMPAuC.js} +1 -1
  55. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-CwiXVM_O.js → gitGraphDiagram-IHSO6WYX-DCRbi4KE.js} +1 -1
  56. package/web/dist/assets/index-BfF7e08d.js +24 -0
  57. package/web/dist/assets/index-uhc851H3.css +1 -0
  58. package/web/dist/assets/{infoDiagram-FWYZ7A6U-DtzIrIpW.js → infoDiagram-FWYZ7A6U-Bex5pSIX.js} +1 -1
  59. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-B9nk829n.js → ishikawaDiagram-FXEZZL3T-Dj0li_TY.js} +1 -1
  60. package/web/dist/assets/{journeyDiagram-5HDEW3XC-xLaPm-ow.js → journeyDiagram-5HDEW3XC-BtEGEeVr.js} +1 -1
  61. package/web/dist/assets/{kanban-definition-HUTT4EX6-BnDqLYLC.js → kanban-definition-HUTT4EX6-BTFfZvYt.js} +1 -1
  62. package/web/dist/assets/{linear-Djc7nVo8.js → linear-C3SQCsip.js} +1 -1
  63. package/web/dist/assets/{mermaid.core-B5elhUtV.js → mermaid.core-DALDJDu8.js} +4 -4
  64. package/web/dist/assets/{mindmap-definition-LN4V7U3C-N-B7EJGs.js → mindmap-definition-LN4V7U3C-BZf3uw0u.js} +1 -1
  65. package/web/dist/assets/{papaparse.min-BqM-LJkq.js → papaparse.min-Bh9MZ-fk.js} +1 -1
  66. package/web/dist/assets/{pegDiagram-2B236MQR-BIollSMB.js → pegDiagram-2B236MQR-PuOleiMg.js} +1 -1
  67. package/web/dist/assets/{pieDiagram-ENE6RG2P-DkZY6iCd.js → pieDiagram-ENE6RG2P-CcFORUaW.js} +1 -1
  68. package/web/dist/assets/pilotswarm-B9rLtXpI.js +109 -0
  69. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-iUDmQIyL.js → quadrantDiagram-ABIIQ3AL-CEm0mp4y.js} +1 -1
  70. package/web/dist/assets/{railroadDiagram-RFXS5EU6-BSCGJgV7.js → railroadDiagram-RFXS5EU6-DeH-uX0_.js} +1 -1
  71. package/web/dist/assets/{requirementDiagram-TGXJPOKE-9yqcGRUA.js → requirementDiagram-TGXJPOKE-CMbJVMc1.js} +1 -1
  72. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-SPh1plT9.js → sankeyDiagram-HTMAVEWB-C9gL5w_b.js} +1 -1
  73. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-C0nv5x71.js → sequenceDiagram-DBY2YBRQ-BUo8TT74.js} +1 -1
  74. package/web/dist/assets/{sizeCapture-X5ZJPWSS-DMXfUt1o.js → sizeCapture-X5ZJPWSS-C5lqG0ei.js} +1 -1
  75. package/web/dist/assets/{stateDiagram-2N3HPSRC-vjhtquua.js → stateDiagram-2N3HPSRC-DyeISrEU.js} +1 -1
  76. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-5FZbaloZ.js +1 -0
  77. package/web/dist/assets/{swimlanes-5IMT3BWC-Bqewmkt5.js → swimlanes-5IMT3BWC-DawuBk_a.js} +2 -2
  78. package/web/dist/assets/swimlanesDiagram-G3AALYLV-DL8PgtmM.js +8 -0
  79. package/web/dist/assets/{timeline-definition-FHXFAJF6-DYHsdXUL.js → timeline-definition-FHXFAJF6-BZJ2YJcm.js} +1 -1
  80. package/web/dist/assets/{vennDiagram-L72KCM5P-Cb7cOHSa.js → vennDiagram-L72KCM5P-BMXBVCfe.js} +1 -1
  81. package/web/dist/assets/{wardleyDiagram-EHGQE667-BHpO1byL.js → wardleyDiagram-EHGQE667-DWedKsrQ.js} +1 -1
  82. package/web/dist/assets/{xychartDiagram-FW5EYKEG-DHDvGPtH.js → xychartDiagram-FW5EYKEG--XdfYceC.js} +1 -1
  83. package/web/dist/index.html +3 -3
  84. package/web/runtime.js +19 -0
  85. package/web/server.js +2 -2
  86. package/web/dist/assets/channel-B50-C7oJ.js +0 -1
  87. package/web/dist/assets/classDiagram-OUVF2IWQ-CIJlKS0m.js +0 -1
  88. package/web/dist/assets/classDiagram-v2-EOCWNBFH-CIJlKS0m.js +0 -1
  89. package/web/dist/assets/index-CHlrgp0s.css +0 -1
  90. package/web/dist/assets/index-bf7RCrAf.js +0 -24
  91. package/web/dist/assets/pilotswarm-Di5s6c69.js +0 -105
  92. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-CYIeYmPN.js +0 -1
  93. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BeKhH9C9.js +0 -8
@@ -1,3 +1,5 @@
1
+ import { matchesSessionError } from "./session-warning.js";
2
+
1
3
  // Copilot emits this diagnostic for an accepted turn without visible text.
2
4
  // Keep its durable event and Activity entry, but do not turn it into a chat
3
5
  // warning. Match only this message and the runtime's known retry wrapper.
@@ -27,9 +29,13 @@ export function shouldKeepSessionWarning(previousSession, nextSession) {
27
29
 
28
30
  const timeMs = value => value instanceof Date ? value.getTime()
29
31
  : typeof value === "number" ? value : Date.parse(value || "");
30
- const errorText = event => typeof event?.data === "string" ? event.data
32
+ const isWarningEvent = event => event?.eventType === "session.error"
33
+ || event?.eventType === "session.turn_completed" && event.data?.resultType === "error";
34
+ const errorText = event => event?.eventType === "session.turn_completed"
35
+ ? String(event.data?.resultType === "error" ? event.data.errorMessage || "" : "").trim()
36
+ : typeof event?.data === "string" ? event.data
31
37
  : String(event?.data?.message || event?.data?.error || event?.data?.text || "").trim();
32
- const sameError = (statusText, eventText) => Boolean(eventText) && statusText.includes(eventText);
38
+ const sameError = (statusText, eventText) => matchesSessionError(eventText, statusText);
33
39
 
34
40
  export function buildSessionWarning(event) {
35
41
  const text = errorText(event);
@@ -62,7 +68,7 @@ export function retainSessionWarnings(previous, next, events = [], now = Date.no
62
68
  return { ...next, chatWarnings: [...warnings.slice(0, -1), { ...last, active: false }] };
63
69
  }
64
70
  const since = last?.createdAt || 0;
65
- const event = events.findLast(e => e.eventType === "session.error"
71
+ const event = events.findLast(e => isWarningEvent(e)
66
72
  && timeMs(e.createdAt) > since && sameError(text, errorText(e)));
67
73
  // A retained error field can outlive a terminal/wait state. A later
68
74
  // running snapshot alone is not evidence of another failure episode.
@@ -92,7 +98,7 @@ export function withSessionWarnings(chat, session, events = []) {
92
98
  // Status often wins the race against its durable event. Match only
93
99
  // that boundary, never a later failure after a new user/agent message.
94
100
  const following = events.filter(e => Number(e.seq) > warning.afterSeq);
95
- const boundary = following.find(e => ["session.error", "user.message", "assistant.message"].includes(e.eventType));
101
+ const boundary = following.find(e => isWarningEvent(e) || ["user.message", "assistant.message"].includes(e.eventType));
96
102
  // On initial load the catalog can arrive before any history at all.
97
103
  // Find its matching older event by the captured failure time, without
98
104
  // reusing an event from a previous error episode with identical text.
@@ -103,8 +109,9 @@ export function withSessionWarnings(chat, session, events = []) {
103
109
  && timeMs(m.createdAt) <= warning.createdAt
104
110
  && sameError(warning.text, m.text));
105
111
  const eventSeq = warning.eventSeq ?? recorded?.warningSeq
106
- ?? (boundary?.eventType === "session.error" && sameError(warning.text, errorText(boundary)) ? Number(boundary.seq) : null);
107
- const index = messages.findIndex(m => m.kind === "session-warning" && m.warningSeq === eventSeq);
112
+ ?? (isWarningEvent(boundary) && sameError(warning.text, errorText(boundary)) ? Number(boundary.seq) : null);
113
+ const index = messages.findIndex(m => m.kind === "session-warning"
114
+ && (m.warningSeq === eventSeq || eventSeq != null && m.turnCompletedSeq === eventSeq));
108
115
  const notice = {
109
116
  id: warning.id, kind: "session-warning", role: "system", text: warning.text,
110
117
  createdAt: warning.createdAt,
@@ -0,0 +1,14 @@
1
+ function canonicalErrorText(value) {
2
+ // ManagedSession adds this wrapper when promoting an SDK session.error
3
+ // to a failed turn. The SDK event itself retains the original message.
4
+ return String(value || "").trim().replace(/^Execution failed:\s*/, "");
5
+ }
6
+
7
+ export function matchesSessionError(recorded, current) {
8
+ const error = canonicalErrorText(recorded);
9
+ const statusError = canonicalErrorText(current);
10
+ // Orchestration status can append retry details to the recorded failure.
11
+ // Match only the whole message or its detail suffix, never an arbitrary
12
+ // substring shared by different errors.
13
+ return Boolean(error) && (statusError === error || statusError.startsWith(`${error} (`));
14
+ }
@@ -448,10 +448,14 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
448
448
  activity: 0,
449
449
  filePreview: 0,
450
450
  },
451
- // Per-session chat scroll memory (rows from bottom), restored on
452
- // sessions/selected and evicted with the session's history.
451
+ // Per-session chat scroll memory. While following, the offset is
452
+ // distance from the bottom; once the reader scrolls up it becomes
453
+ // a stable viewport offset from the top. The paired follow flag
454
+ // disambiguates the two modes across session switches.
453
455
  chatScrollBySession: {},
456
+ chatFollowBottomBySession: {},
454
457
  followBottom: {
458
+ chat: true,
455
459
  inspector: true,
456
460
  activity: true,
457
461
  },
@@ -767,6 +771,7 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
767
771
  // replaces the legacy GitHub-key editor in new UI; the legacy
768
772
  // state remains available for rollback compatibility only.
769
773
  section: "providers",
774
+ features: { mode: "mine", userId: null, data: null, users: [], drafts: {}, loading: false, saving: false, error: null, generation: 0, fetchedAt: null, userQuery: "" },
770
775
  // Agent packages (docs/proposals/agent-packages.md) — registry
771
776
  // list, selected package detail, and the workspace viewer.
772
777
  packages: {
@@ -354,6 +354,7 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
354
354
  connectionError: state.connection.error,
355
355
  connectionMode: state.connection.mode,
356
356
  chatScroll: state.ui.scroll.chat,
357
+ followBottom: state.ui.followBottom?.chat !== false,
357
358
  focused: state.ui.focusRegion === "chat",
358
359
  };
359
360
  }, shallowEqualObject);
@@ -449,7 +450,7 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
449
450
  lines: elements,
450
451
  bottomStickyLines,
451
452
  scrollOffset: chatView.chatScroll,
452
- scrollMode: "bottom",
453
+ scrollMode: chatView.followBottom ? "bottom" : "top",
453
454
  paneId: "chat",
454
455
  paneLabel: "Chat",
455
456
  frame,
@@ -0,0 +1,186 @@
1
+ import React from "react";
2
+
3
+ const h = React.createElement;
4
+ const onOff = enabled => enabled ? "On" : "Off";
5
+ const validRevision = value => typeof value === "string" && /^[1-9]\d{0,18}$/.test(value)
6
+ && BigInt(value) <= 9223372036854775807n;
7
+ const isRecord = value => value !== null && typeof value === "object" && !Array.isArray(value);
8
+
9
+ /** Missing diagnostics are unknown, including telemetry redacted by older servers. */
10
+ export function featureWorkerStatus(flag, workers = [], now = Date.now()) {
11
+ const reports = workers.map(worker => {
12
+ const age = now - new Date(worker.updatedAt).getTime();
13
+ const state = worker.state?.["feature-flags"];
14
+ const recent = Number.isFinite(age) && age >= -5_000 && age < 90_000;
15
+ const eligible = worker.phase === "ready" && recent;
16
+ const validState = isRecord(state) && state.protocolVersion === 1
17
+ && typeof state.initialized === "boolean" && Array.isArray(state.supportedKeys)
18
+ && state.supportedKeys.every(key => typeof key === "string") && isRecord(state.appliedRevisions);
19
+ let delivery = "unknown";
20
+ if (eligible && validState && validRevision(flag.revision)) {
21
+ if (!state.supportedKeys.includes(flag.featureKey)) delivery = "unsupported";
22
+ else if (!state.initialized) delivery = "loading";
23
+ else if (validRevision(state.appliedRevisions[flag.featureKey])) {
24
+ delivery = BigInt(state.appliedRevisions[flag.featureKey]) >= BigInt(flag.revision) ? "updated" : "pending";
25
+ }
26
+ }
27
+ const capability = eligible && validState && ["sync", "off"].includes(state.nativeCapability)
28
+ ? state.nativeCapability : "unknown";
29
+ return { workerNodeId: worker.workerNodeId, phase: worker.phase, recent, eligible, delivery, capability,
30
+ revision: validState && validRevision(state.appliedRevisions[flag.featureKey]) ? state.appliedRevisions[flag.featureKey] : null,
31
+ refreshError: eligible && validState && (state.hasRefreshError === true || typeof state.lastError === "string" && state.lastError.length > 0) };
32
+ });
33
+ const eligible = reports.filter(report => report.eligible);
34
+ return { reports, eligible: eligible.length,
35
+ updated: eligible.filter(report => report.delivery === "updated").length,
36
+ unknown: eligible.filter(report => report.delivery === "unknown").length,
37
+ unsupported: eligible.filter(report => report.delivery === "unsupported").length,
38
+ capable: eligible.filter(report => report.capability === "sync").length,
39
+ incapable: eligible.filter(report => report.capability === "off").length,
40
+ capabilityUnknown: eligible.filter(report => report.capability === "unknown").length,
41
+ refreshErrors: eligible.filter(report => report.refreshError).length,
42
+ stale: reports.filter(report => !report.recent && report.phase !== "draining").length,
43
+ draining: reports.filter(report => report.phase === "draining").length,
44
+ other: reports.filter(report => report.recent && !report.eligible && report.phase !== "draining").length };
45
+ }
46
+
47
+ function deliveryText(status, workersError) {
48
+ if (workersError || !status.eligible || status.unknown === status.eligible) return "Settings delivery: status unavailable";
49
+ return `Settings delivery: ${status.updated} of ${status.eligible} reporting workers updated.${status.unknown ? ` Status unavailable for ${status.unknown}.` : ""}${status.unsupported ? ` ${status.unsupported} do not support this feature.` : ""}${status.refreshErrors ? ` ${status.refreshErrors} reported a refresh problem.` : ""}`;
50
+ }
51
+
52
+ function clusterSummary(enabled, allowPersonal) {
53
+ return `${onOff(enabled)} ${allowPersonal ? "by default. Users can choose On or Off." : "for everyone. Personal settings do not apply."}`;
54
+ }
55
+
56
+ function RadioChoices({ legend, name, value, options, disabled, onChange }) {
57
+ return h("fieldset", { className: "ps-feature-choice-group", disabled },
58
+ h("legend", null, legend),
59
+ h("div", { className: "ps-feature-choices" }, options.map(option => h("label", {
60
+ className: `ps-feature-choice${value === option.value ? " is-selected" : ""}`, key: option.value,
61
+ }, h("input", { type: "radio", name, value: option.value, checked: value === option.value,
62
+ onChange: () => onChange(option.value) }), h("span", null, option.label)))));
63
+ }
64
+
65
+ function FeatureFlagRow({ flag, mode, busy, saving, controller, workers, workersError, isAdmin, userName }) {
66
+ const id = React.useId();
67
+ const draft = controller.getFeatureDraft?.(flag.featureKey);
68
+ const dirty = draft !== undefined;
69
+ const conflict = dirty && (draft.needsReview || draft.expectedRevision !== flag.revision);
70
+ const conflictNeedsRefresh = conflict && draft.needsReview && draft.expectedRevision === flag.revision;
71
+ const storedEnabled = flag.cluster?.enabled ?? flag.defaultEnabled;
72
+ const storedOverride = flag.cluster?.allowUserOverride ?? flag.defaultAllowUserOverride;
73
+ const locked = mode !== "cluster" && !storedOverride;
74
+ const savedChoice = flag.user ? String(flag.user.enabled) : "inherit";
75
+ const choice = dirty ? draft.values === null ? "inherit" : String(draft.values.enabled) : savedChoice;
76
+ const clusterDraft = dirty ? draft.values ?? { enabled: flag.defaultEnabled, allowUserOverride: flag.defaultAllowUserOverride }
77
+ : { enabled: storedEnabled, allowUserOverride: storedOverride };
78
+ const target = mode === "mine" ? "you" : userName;
79
+ const futureTarget = mode === "mine" ? "Your" : `${userName}’s`;
80
+ const savedValue = mode === "cluster" ? `${onOff(storedEnabled)} ${storedOverride ? "by default" : "for everyone"}`
81
+ : `${onOff(flag.effective)} for ${target}`;
82
+ const reason = mode === "cluster" ? storedOverride ? "Users can choose On or Off." : "Personal settings do not apply."
83
+ : locked ? "Required by the cluster." : flag.user ? mode === "mine" ? "Using your choice." : "Using the saved personal choice." : "Following the cluster setting.";
84
+ const status = featureWorkerStatus(flag, workers);
85
+ const isNative = flag.featureKey === "copilot.native_tasks";
86
+ const savedOn = mode === "cluster" ? storedEnabled : flag.effective;
87
+ const knownBlocked = isAdmin && isNative && savedOn && !workersError && status.eligible > 0 && status.incapable === status.eligible;
88
+ const editDisabled = busy || !flag.supported;
89
+ let preview = null;
90
+ if (dirty) {
91
+ if (mode === "cluster") preview = `${draft.values === null ? "Restore the default policy. " : ""}${clusterSummary(clusterDraft.enabled, clusterDraft.allowUserOverride)}${!storedOverride && clusterDraft.allowUserOverride ? " Previously saved personal choices will apply again." : ""}`;
92
+ else if (locked) preview = `${futureTarget} future preference will ${choice === "inherit" ? "follow the cluster setting" : `be ${onOff(choice === "true")}`}. ${onOff(storedEnabled)} still applies while the cluster requires it.`;
93
+ else preview = `${onOff(choice === "inherit" ? storedEnabled : choice === "true")} for ${target}. ${choice === "inherit" ? "Following the cluster setting." : "Using the personal choice."}`;
94
+ }
95
+ return h("section", { className: "ps-feature-flag", "data-feature-key": flag.featureKey, "aria-labelledby": `${id}-title` },
96
+ h("div", { className: "ps-feature-flag-heading" }, h("h3", { id: `${id}-title` }, flag.displayName),
97
+ dirty ? h("span", { className: "ps-feature-unsaved" }, "Unsaved changes") : null),
98
+ h("p", { className: "ps-feature-description" }, isNative ? "Lets Copilot delegate short tasks in the same session workspace. On permits use; Copilot decides when to use native tasks." : flag.description),
99
+ h("div", { className: "ps-feature-saved" },
100
+ h("span", { className: "ps-feature-label" }, "Saved setting"),
101
+ h("strong", { className: `ps-feature-value${(mode === "cluster" ? storedEnabled : flag.effective) ? " is-on" : ""}` }, savedValue),
102
+ h("p", { className: "ps-feature-explanation" }, reason),
103
+ mode !== "cluster" ? h("p", { className: "ps-feature-context" }, `Cluster setting: ${onOff(storedEnabled)}. Personal settings ${storedOverride ? "allowed" : "not allowed"}.`) : null,
104
+ locked ? h("p", { className: "ps-feature-retained" }, `Saved future preference: ${savedChoice === "inherit" ? "Use cluster setting" : onOff(flag.user.enabled)}. This will apply if personal settings are allowed again.`) : null),
105
+ !flag.supported ? h("p", { role: "status" }, "This server does not support changing this feature.") : null,
106
+ knownBlocked ? h("p", { className: "ps-feature-blocker", role: "status" }, `The saved setting is On, but none of the ${status.eligible} reporting workers is configured for native tasks. Native tasks cannot run on those workers.`) : null,
107
+ h("div", { className: "ps-feature-editor" }, mode === "cluster" ? [
108
+ h(RadioChoices, { key: "enabled", legend: "Cluster setting", name: `${id}-cluster`, value: String(clusterDraft.enabled), disabled: editDisabled,
109
+ options: [{ value: "false", label: "Off" }, { value: "true", label: "On" }],
110
+ onChange: value => controller.setFeatureDraft(flag.featureKey, { ...clusterDraft, enabled: value === "true" }) }),
111
+ h(RadioChoices, { key: "override", legend: "Personal settings", name: `${id}-override`, value: String(clusterDraft.allowUserOverride), disabled: editDisabled,
112
+ options: [{ value: "true", label: "Allowed" }, { value: "false", label: "Not allowed" }],
113
+ onChange: value => controller.setFeatureDraft(flag.featureKey, { ...clusterDraft, allowUserOverride: value === "true" }) }),
114
+ ] : h(RadioChoices, { legend: locked ? "Preference for when personal settings are allowed" : mode === "mine" ? "Your choice" : `${userName}’s choice`,
115
+ name: `${id}-user`, value: choice, disabled: editDisabled,
116
+ options: [{ value: "inherit", label: `Use cluster setting (currently ${onOff(storedEnabled)})` },
117
+ { value: "true", label: mode === "mine" ? "On for me" : "On for this user" },
118
+ { value: "false", label: mode === "mine" ? "Off for me" : "Off for this user" }],
119
+ onChange: value => controller.setFeatureDraft(flag.featureKey, value === "inherit" ? null : { enabled: value === "true" }) })),
120
+ dirty ? h("div", { className: "ps-feature-preview", role: "status" }, h("strong", null, "After saving: "), preview) : null,
121
+ conflict ? h("p", { className: "ps-feature-conflict", role: "status" }, conflictNeedsRefresh
122
+ ? "Settings changed since this edit began. Refresh settings to load the latest saved value."
123
+ : "Settings changed since this edit began. Review the saved setting and your proposed change before saving.") : null,
124
+ h("div", { className: "ps-feature-actions" },
125
+ h("button", { type: "button", className: "ps-mini-button ps-feature-save", disabled: editDisabled || !dirty || conflict,
126
+ onClick: () => controller.saveFeatureDraft(flag.featureKey) }, saving && dirty ? "Saving…" : locked ? "Save future preference" : "Save changes"),
127
+ h("button", { type: "button", className: "ps-mini-button", disabled: busy || !dirty,
128
+ onClick: () => controller.discardFeatureDraft(flag.featureKey) }, "Discard changes"),
129
+ conflict ? h("button", { type: "button", className: "ps-mini-button", disabled: busy || conflictNeedsRefresh,
130
+ onClick: () => controller.reviewFeatureDraft(flag.featureKey) }, "Review latest settings") : null),
131
+ isAdmin ? h("p", { className: "ps-feature-delivery", role: "status" }, deliveryText(status, workersError)) : null,
132
+ h("details", { className: "ps-feature-details" }, h("summary", null, "Technical details"),
133
+ h("p", null, h("code", null, flag.featureKey), ` · Saved revision ${flag.revision}`),
134
+ h("p", null, "Workers normally check for settings changes within 20 seconds. Settings delivery does not confirm that each session has refreshed its tools."),
135
+ isNative ? h("p", null, "After a worker receives On, native tasks become available on the next turn. After it receives Off, new native tasks are blocked. Tasks already running can finish.") : null,
136
+ isAdmin && isNative ? h("p", null, workersError || !status.eligible || status.capabilityUnknown === status.eligible
137
+ ? "Native task support: status unavailable."
138
+ : `Native task support: ${status.capable} of ${status.eligible} reporting workers configured; ${status.incapable} configured Off; ${status.capabilityUnknown} unknown.`) : null,
139
+ isAdmin && (status.stale || status.draining || status.other) ? h("p", null, `Excluded from current delivery counts: ${status.stale} stale or undated reports; ${status.draining} draining workers; ${status.other} other worker reports.`) : null,
140
+ isAdmin ? h("ul", { className: "ps-feature-worker-list" }, status.reports.map(report => h("li", { key: report.workerNodeId },
141
+ `${report.workerNodeId}: ${report.eligible ? ({ updated: "settings received", pending: "waiting for latest settings", loading: "loading settings", unsupported: "feature unsupported", unknown: "status unavailable" })[report.delivery] : report.phase === "draining" ? "draining; excluded" : !report.recent ? "stale or undated report; status unavailable" : "not reporting ready; excluded"}${report.eligible && report.revision ? `; revision ${report.revision}` : ""}${report.eligible && isNative ? `; native task configuration ${report.capability === "sync" ? "On" : report.capability === "off" ? "Off" : "unknown"}` : ""}${report.refreshError ? "; settings refresh problem" : ""}`))) : null,
142
+ mode === "cluster" ? h("div", { className: "ps-feature-defaults" },
143
+ h("p", null, `Default policy: ${clusterSummary(flag.defaultEnabled, flag.defaultAllowUserOverride)}`),
144
+ h("button", { type: "button", className: "ps-mini-button", disabled: editDisabled || !flag.cluster,
145
+ onClick: () => controller.setFeatureDraft(flag.featureKey, null) }, "Preview default policy"),
146
+ h("p", { className: "ps-feature-explanation" }, "Preview stages removal of the cluster override. Save changes to use the code-defined defaults; personal choices are kept.")) : null));
147
+ }
148
+
149
+ export function FeatureFlagsPanel({ controller, features = {}, isAdmin, workers = [], workersError = null }) {
150
+ const mode = features.mode || "mine";
151
+ const [userQuery, setUserQuery] = React.useState(features.userQuery || "");
152
+ const user = features.users?.find(entry => entry.userId === features.userId);
153
+ const userName = user?.displayName || user?.email || user?.subject || (features.userId ? `user ${features.userId}` : "this user");
154
+ const busy = features.saving || features.loading;
155
+ return h("section", { className: "ps-feature-flags-panel", "aria-label": "Feature flags" },
156
+ h("header", { className: "ps-feature-flag-heading" }, h("h2", null, "Feature flags"),
157
+ h("button", { type: "button", className: "ps-mini-button", disabled: busy, onClick: () => controller.refreshFeatureFlags() }, "Refresh settings")),
158
+ h("div", { role: "tablist", "aria-label": "Feature settings scope", className: "ps-feature-tabs" },
159
+ [{ mode: "mine", label: "My settings" }, ...(isAdmin ? [{ mode: "cluster", label: "Cluster settings" }, { mode: "users", label: "User settings" }] : [])].map(tab => {
160
+ const count = controller.getFeatureDraftCount?.(tab.mode) || 0;
161
+ return h("button", { type: "button", key: tab.mode, role: "tab", "aria-selected": mode === tab.mode, disabled: features.saving,
162
+ className: "ps-mini-button", onClick: () => controller.selectFeatureScope(tab.mode) }, tab.label,
163
+ count ? h("span", { className: "ps-feature-unsaved" }, `${count} unsaved`) : null);
164
+ })),
165
+ mode === "users" && isAdmin ? h("div", { className: "ps-feature-user" },
166
+ h("form", { onSubmit: event => { event.preventDefault(); void controller.searchFeatureUsers(userQuery); } },
167
+ h("label", null, "Find user ", h("input", { type: "search", value: userQuery,
168
+ "aria-label": "Find feature settings user", placeholder: "Name, email or identity", onChange: event => setUserQuery(event.target.value) })),
169
+ h("button", { className: "ps-mini-button", disabled: features.usersLoading || features.saving }, "Search")),
170
+ h("label", null, "User ", h("select", { "aria-label": "Feature settings user", value: features.userId ?? "", disabled: features.saving,
171
+ onChange: event => controller.selectFeatureScope("users", event.target.value ? Number(event.target.value) : null) },
172
+ h("option", { value: "" }, "Select a user"),
173
+ (features.users || []).map(entry => h("option", { key: entry.userId, value: entry.userId }, `${entry.displayName || entry.email || entry.subject}${entry.displayName && entry.email ? ` — ${entry.email}` : ""}${features.selectedUserOutsideResults && entry.userId === features.userId ? " (current selection)" : ""}`)))),
174
+ features.selectedUserOutsideResults ? h("p", { className: "ps-feature-explanation" }, "Current selection kept while you search.") : null,
175
+ user ? h("p", { className: "ps-feature-user-identity" }, h("strong", null, userName), user.email ? h("span", null, user.email) : null)
176
+ : features.userId ? h("p", null, `Selected user ID: ${features.userId}`) : null,
177
+ features.users?.length >= 500 ? h("p", null, "Showing the first 500 users. Refine your search to find another user.") : null) : null,
178
+ features.error ? h("p", { role: "alert" }, features.error) : null,
179
+ features.metadataError ? h("p", { role: "status" }, features.metadataError) : null,
180
+ isAdmin && workersError ? h("p", { role: "status" }, "Worker reports could not be refreshed. Settings delivery status is unavailable.") : null,
181
+ features.notice ? h("p", { role: "status" }, features.notice) : null,
182
+ features.loading ? h("p", { role: "status" }, "Loading feature settings…") : null,
183
+ (mode === "mine" || isAdmin ? features.data?.flags || [] : []).map(flag => h(FeatureFlagRow, { key: `${mode}:${features.userId}:${flag.featureKey}`,
184
+ flag, mode, busy, saving: features.saving, controller, workers, workersError, isAdmin, userName })),
185
+ !features.loading && features.data?.flags?.length === 0 ? h("p", null, "No feature definitions are published.") : null);
186
+ }
@@ -0,0 +1,59 @@
1
+ import React from "react";
2
+ import { NATIVE_TASK_LABELS } from "../../core/src/native-tasks.js";
3
+
4
+ const TERMINAL = new Set(["completed", "failed", "cancelled", "interrupted"]);
5
+ const MARKS = { starting: "◷", running: "◷", waiting: "Ⅱ", completed: "✓", failed: "!", cancelled: "■", interrupted: "↯" };
6
+ function duration(task, now) {
7
+ const ms = task.durationMs ?? (task.startedAt ? (task.completedAt || now) - task.startedAt : null);
8
+ if (!Number.isFinite(ms) || ms < 0) return "";
9
+ const seconds = Math.floor(ms / 1000);
10
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
11
+ }
12
+
13
+ export const NativeTaskCard = React.memo(function NativeTaskCard({ group, colors }) {
14
+ const [showAll, setShowAll] = React.useState(false);
15
+ const [now, setNow] = React.useState(Date.now);
16
+ const active = group.tasks.some(task => !TERMINAL.has(task.status));
17
+ React.useEffect(() => {
18
+ if (!active) return undefined;
19
+ const timer = setInterval(() => setNow(Date.now()), 1000);
20
+ return () => clearInterval(timer);
21
+ }, [active]);
22
+ const counts = group.tasks.reduce((acc, task) => {
23
+ const label = NATIVE_TASK_LABELS[task.status] || "Unknown";
24
+ acc[label] = (acc[label] || 0) + 1;
25
+ return acc;
26
+ }, {});
27
+ const countLabel = Object.entries(counts).map(([label, count]) => `${count} ${label.toLowerCase()}`).join(" · ");
28
+ const tasks = showAll ? group.tasks : group.tasks.slice(0, 3);
29
+ return React.createElement("section", { className: "ps-native-tasks", "aria-label": "Native tasks", "data-task-group": group.id },
30
+ React.createElement("header", { className: "ps-native-tasks-header" },
31
+ React.createElement("span", { className: "ps-native-tasks-title" },
32
+ React.createElement("span", { className: "ps-native-task-branch", "aria-hidden": true }, "⑂"), "Native tasks"),
33
+ React.createElement("span", { className: "ps-native-tasks-count", "aria-live": "polite" }, countLabel)),
34
+ tasks.map(task => {
35
+ const color = colors[task.status] || colors.running;
36
+ const preview = task.error || task.result || task.preview || (task.status === "completed" ? "Task completed." : "");
37
+ const time = duration(task, now);
38
+ return React.createElement("details", { key: task.id, className: "ps-native-task", "data-task-id": task.id, "data-status": task.status,
39
+ style: { "--ps-native-task-accent": color } },
40
+ React.createElement("summary", null,
41
+ React.createElement("span", { className: "ps-native-task-mark", "aria-hidden": true }, MARKS[task.status] || "?"),
42
+ React.createElement("span", { className: "ps-native-task-main" },
43
+ React.createElement("span", { className: "ps-native-task-title" }, task.title),
44
+ preview ? React.createElement("span", { className: "ps-native-task-preview" }, preview.replace(/\s+/g, " ").slice(0, 240)) : null),
45
+ React.createElement("span", { className: "ps-native-task-meta" },
46
+ React.createElement("span", { className: "ps-native-task-status" }, task.telemetryStale ? "Reconnecting" : NATIVE_TASK_LABELS[task.status] || task.status),
47
+ React.createElement("span", null, [time, `${task.toolCalls || 0} calls`].filter(Boolean).join(" · "))),
48
+ React.createElement("span", { className: "ps-native-task-chevron", "aria-hidden": true }, "›")),
49
+ React.createElement("div", { className: "ps-native-task-detail" },
50
+ React.createElement("div", { className: "ps-native-task-scope" }, [task.profile, task.model, task.reasoningEffort, "Same worker"].filter(Boolean).join(" · ")),
51
+ task.result && !task.error ? React.createElement("div", { className: "ps-native-task-scope" }, "Result excerpt") : null,
52
+ React.createElement("p", { className: "ps-native-task-result" }, task.error || task.result || task.preview
53
+ || (task.status === "completed" ? "Task completed. A result excerpt is not available in this recording." : "No result available yet.")),
54
+ task.recentActivity?.length ? React.createElement("ul", { className: "ps-native-task-activity" },
55
+ task.recentActivity.map((activity, i) => React.createElement("li", { key: i }, activity.message))) : null));
56
+ }),
57
+ group.tasks.length > 3 ? React.createElement("button", { type: "button", className: "ps-native-tasks-more", "aria-expanded": showAll,
58
+ onClick: () => setShowAll(value => !value) }, showAll ? "Show fewer tasks" : `Show ${group.tasks.length - 3} more tasks`) : null);
59
+ });