@flanksource/clicky-ui 0.3.40 → 0.3.42
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.
- package/README.md +24 -0
- package/dist/ai.cjs +14 -0
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.js +14 -0
- package/dist/ai.js.map +1 -1
- package/dist/comments/CommentCard.cjs +3 -1
- package/dist/comments/CommentCard.cjs.map +1 -1
- package/dist/comments/CommentCard.d.ts.map +1 -1
- package/dist/comments/CommentCard.js +4 -2
- package/dist/comments/CommentCard.js.map +1 -1
- package/dist/comments/CommentCardParts.cjs +16 -0
- package/dist/comments/CommentCardParts.cjs.map +1 -1
- package/dist/comments/CommentCardParts.d.ts +4 -1
- package/dist/comments/CommentCardParts.d.ts.map +1 -1
- package/dist/comments/CommentCardParts.js +16 -0
- package/dist/comments/CommentCardParts.js.map +1 -1
- package/dist/comments/comment-types.cjs.map +1 -1
- package/dist/comments/comment-types.d.ts +4 -0
- package/dist/comments/comment-types.d.ts.map +1 -1
- package/dist/comments/comment-types.js.map +1 -1
- package/dist/components/AccordionList.cjs +5 -1
- package/dist/components/AccordionList.cjs.map +1 -1
- package/dist/components/AccordionList.d.ts +3 -1
- package/dist/components/AccordionList.d.ts.map +1 -1
- package/dist/components/AccordionList.js +5 -1
- package/dist/components/AccordionList.js.map +1 -1
- package/dist/components/Combobox.cjs +54 -8
- package/dist/components/Combobox.cjs.map +1 -1
- package/dist/components/Combobox.d.ts.map +1 -1
- package/dist/components/Combobox.js +56 -10
- package/dist/components/Combobox.js.map +1 -1
- package/dist/components/ComboboxMenu.cjs +139 -113
- package/dist/components/ComboboxMenu.cjs.map +1 -1
- package/dist/components/ComboboxMenu.d.ts +3 -1
- package/dist/components/ComboboxMenu.d.ts.map +1 -1
- package/dist/components/ComboboxMenu.js +139 -113
- package/dist/components/ComboboxMenu.js.map +1 -1
- package/dist/components/ComboboxMenuTrigger.cjs +87 -0
- package/dist/components/ComboboxMenuTrigger.cjs.map +1 -0
- package/dist/components/ComboboxMenuTrigger.d.ts +23 -0
- package/dist/components/ComboboxMenuTrigger.d.ts.map +1 -0
- package/dist/components/ComboboxMenuTrigger.js +87 -0
- package/dist/components/ComboboxMenuTrigger.js.map +1 -0
- package/dist/components/JsonSchemaForm.cjs +39 -11
- package/dist/components/JsonSchemaForm.cjs.map +1 -1
- package/dist/components/JsonSchemaForm.d.ts +1 -1
- package/dist/components/JsonSchemaForm.d.ts.map +1 -1
- package/dist/components/JsonSchemaForm.js +38 -10
- package/dist/components/JsonSchemaForm.js.map +1 -1
- package/dist/components/SegmentedControl.cjs +20 -3
- package/dist/components/SegmentedControl.cjs.map +1 -1
- package/dist/components/SegmentedControl.d.ts +11 -0
- package/dist/components/SegmentedControl.d.ts.map +1 -1
- package/dist/components/SegmentedControl.js +21 -4
- package/dist/components/SegmentedControl.js.map +1 -1
- package/dist/components/combobox-types.d.ts +8 -0
- package/dist/components/combobox-types.d.ts.map +1 -1
- package/dist/components/json-schema-form-accordion-array.cjs +3 -1
- package/dist/components/json-schema-form-accordion-array.cjs.map +1 -1
- package/dist/components/json-schema-form-accordion-array.d.ts +4 -1
- package/dist/components/json-schema-form-accordion-array.d.ts.map +1 -1
- package/dist/components/json-schema-form-accordion-array.js +3 -1
- package/dist/components/json-schema-form-accordion-array.js.map +1 -1
- package/dist/components/json-schema-form-array-view.cjs +63 -0
- package/dist/components/json-schema-form-array-view.cjs.map +1 -0
- package/dist/components/json-schema-form-array-view.d.ts +21 -0
- package/dist/components/json-schema-form-array-view.d.ts.map +1 -0
- package/dist/components/json-schema-form-array-view.js +63 -0
- package/dist/components/json-schema-form-array-view.js.map +1 -0
- package/dist/components/json-schema-form-array.cjs +3 -7
- package/dist/components/json-schema-form-array.cjs.map +1 -1
- package/dist/components/json-schema-form-array.d.ts.map +1 -1
- package/dist/components/json-schema-form-array.js +3 -7
- package/dist/components/json-schema-form-array.js.map +1 -1
- package/dist/components/json-schema-form-conditionals.cjs +67 -0
- package/dist/components/json-schema-form-conditionals.cjs.map +1 -0
- package/dist/components/json-schema-form-conditionals.d.ts +9 -0
- package/dist/components/json-schema-form-conditionals.d.ts.map +1 -0
- package/dist/components/json-schema-form-conditionals.js +67 -0
- package/dist/components/json-schema-form-conditionals.js.map +1 -0
- package/dist/components/json-schema-form-debug-badge.cjs +108 -0
- package/dist/components/json-schema-form-debug-badge.cjs.map +1 -0
- package/dist/components/json-schema-form-debug-badge.d.ts +10 -0
- package/dist/components/json-schema-form-debug-badge.d.ts.map +1 -0
- package/dist/components/json-schema-form-debug-badge.js +108 -0
- package/dist/components/json-schema-form-debug-badge.js.map +1 -0
- package/dist/components/json-schema-form-debug-glyph-map.cjs +129 -0
- package/dist/components/json-schema-form-debug-glyph-map.cjs.map +1 -0
- package/dist/components/json-schema-form-debug-glyph-map.d.ts +20 -0
- package/dist/components/json-schema-form-debug-glyph-map.d.ts.map +1 -0
- package/dist/components/json-schema-form-debug-glyph-map.js +129 -0
- package/dist/components/json-schema-form-debug-glyph-map.js.map +1 -0
- package/dist/components/json-schema-form-debug-glyph.cjs +13 -0
- package/dist/components/json-schema-form-debug-glyph.cjs.map +1 -0
- package/dist/components/json-schema-form-debug-glyph.d.ts +6 -0
- package/dist/components/json-schema-form-debug-glyph.d.ts.map +1 -0
- package/dist/components/json-schema-form-debug-glyph.js +13 -0
- package/dist/components/json-schema-form-debug-glyph.js.map +1 -0
- package/dist/components/json-schema-form-debug-groups.cjs +56 -0
- package/dist/components/json-schema-form-debug-groups.cjs.map +1 -0
- package/dist/components/json-schema-form-debug-groups.d.ts +8 -0
- package/dist/components/json-schema-form-debug-groups.d.ts.map +1 -0
- package/dist/components/json-schema-form-debug-groups.js +56 -0
- package/dist/components/json-schema-form-debug-groups.js.map +1 -0
- package/dist/components/json-schema-form-debug-keywords.cjs +159 -0
- package/dist/components/json-schema-form-debug-keywords.cjs.map +1 -0
- package/dist/components/json-schema-form-debug-keywords.d.ts +6 -0
- package/dist/components/json-schema-form-debug-keywords.d.ts.map +1 -0
- package/dist/components/json-schema-form-debug-keywords.js +159 -0
- package/dist/components/json-schema-form-debug-keywords.js.map +1 -0
- package/dist/components/json-schema-form-debug-listeners.cjs +102 -0
- package/dist/components/json-schema-form-debug-listeners.cjs.map +1 -0
- package/dist/components/json-schema-form-debug-listeners.d.ts +6 -0
- package/dist/components/json-schema-form-debug-listeners.d.ts.map +1 -0
- package/dist/components/json-schema-form-debug-listeners.js +102 -0
- package/dist/components/json-schema-form-debug-listeners.js.map +1 -0
- package/dist/components/json-schema-form-debug.cjs +31 -0
- package/dist/components/json-schema-form-debug.cjs.map +1 -0
- package/dist/components/json-schema-form-debug.d.ts +11 -0
- package/dist/components/json-schema-form-debug.d.ts.map +1 -0
- package/dist/components/json-schema-form-debug.js +31 -0
- package/dist/components/json-schema-form-debug.js.map +1 -0
- package/dist/components/json-schema-form-defaults.cjs +68 -0
- package/dist/components/json-schema-form-defaults.cjs.map +1 -0
- package/dist/components/json-schema-form-defaults.d.ts +4 -0
- package/dist/components/json-schema-form-defaults.d.ts.map +1 -0
- package/dist/components/json-schema-form-defaults.js +68 -0
- package/dist/components/json-schema-form-defaults.js.map +1 -0
- package/dist/components/json-schema-form-enum-choice.cjs +114 -0
- package/dist/components/json-schema-form-enum-choice.cjs.map +1 -0
- package/dist/components/json-schema-form-enum-choice.d.ts +14 -0
- package/dist/components/json-schema-form-enum-choice.d.ts.map +1 -0
- package/dist/components/json-schema-form-enum-choice.js +114 -0
- package/dist/components/json-schema-form-enum-choice.js.map +1 -0
- package/dist/components/json-schema-form-errors.cjs +49 -31
- package/dist/components/json-schema-form-errors.cjs.map +1 -1
- package/dist/components/json-schema-form-errors.d.ts +4 -2
- package/dist/components/json-schema-form-errors.d.ts.map +1 -1
- package/dist/components/json-schema-form-errors.js +51 -33
- package/dist/components/json-schema-form-errors.js.map +1 -1
- package/dist/components/json-schema-form-fields.cjs +3 -93
- package/dist/components/json-schema-form-fields.cjs.map +1 -1
- package/dist/components/json-schema-form-fields.d.ts.map +1 -1
- package/dist/components/json-schema-form-fields.js +1 -91
- package/dist/components/json-schema-form-fields.js.map +1 -1
- package/dist/components/json-schema-form-filter.cjs +2 -1
- package/dist/components/json-schema-form-filter.cjs.map +1 -1
- package/dist/components/json-schema-form-filter.js +2 -1
- package/dist/components/json-schema-form-filter.js.map +1 -1
- package/dist/components/json-schema-form-item-summary.cjs +6 -2
- package/dist/components/json-schema-form-item-summary.cjs.map +1 -1
- package/dist/components/json-schema-form-item-summary.d.ts.map +1 -1
- package/dist/components/json-schema-form-item-summary.js +6 -2
- package/dist/components/json-schema-form-item-summary.js.map +1 -1
- package/dist/components/json-schema-form-layout.cjs +28 -19
- package/dist/components/json-schema-form-layout.cjs.map +1 -1
- package/dist/components/json-schema-form-layout.d.ts +8 -6
- package/dist/components/json-schema-form-layout.d.ts.map +1 -1
- package/dist/components/json-schema-form-layout.js +29 -20
- package/dist/components/json-schema-form-layout.js.map +1 -1
- package/dist/components/json-schema-form-listener-paths.cjs +52 -0
- package/dist/components/json-schema-form-listener-paths.cjs.map +1 -0
- package/dist/components/json-schema-form-listener-paths.d.ts +10 -0
- package/dist/components/json-schema-form-listener-paths.d.ts.map +1 -0
- package/dist/components/json-schema-form-listener-paths.js +52 -0
- package/dist/components/json-schema-form-listener-paths.js.map +1 -0
- package/dist/components/json-schema-form-listener-types.d.ts +22 -0
- package/dist/components/json-schema-form-listener-types.d.ts.map +1 -0
- package/dist/components/json-schema-form-listeners.cjs +256 -0
- package/dist/components/json-schema-form-listeners.cjs.map +1 -0
- package/dist/components/json-schema-form-listeners.d.ts +13 -0
- package/dist/components/json-schema-form-listeners.d.ts.map +1 -0
- package/dist/components/json-schema-form-listeners.js +256 -0
- package/dist/components/json-schema-form-listeners.js.map +1 -0
- package/dist/components/json-schema-form-object-array-view.cjs +62 -0
- package/dist/components/json-schema-form-object-array-view.cjs.map +1 -0
- package/dist/components/json-schema-form-object-array-view.d.ts +14 -0
- package/dist/components/json-schema-form-object-array-view.d.ts.map +1 -0
- package/dist/components/json-schema-form-object-array-view.js +62 -0
- package/dist/components/json-schema-form-object-array-view.js.map +1 -0
- package/dist/components/json-schema-form-object.cjs +3 -0
- package/dist/components/json-schema-form-object.cjs.map +1 -1
- package/dist/components/json-schema-form-object.d.ts.map +1 -1
- package/dist/components/json-schema-form-object.js +3 -0
- package/dist/components/json-schema-form-object.js.map +1 -1
- package/dist/components/json-schema-form-preferences.cjs +1 -0
- package/dist/components/json-schema-form-preferences.cjs.map +1 -1
- package/dist/components/json-schema-form-preferences.d.ts +1 -0
- package/dist/components/json-schema-form-preferences.d.ts.map +1 -1
- package/dist/components/json-schema-form-preferences.js +1 -0
- package/dist/components/json-schema-form-preferences.js.map +1 -1
- package/dist/components/json-schema-form-render.cjs +36 -12
- package/dist/components/json-schema-form-render.cjs.map +1 -1
- package/dist/components/json-schema-form-render.d.ts.map +1 -1
- package/dist/components/json-schema-form-render.js +38 -14
- package/dist/components/json-schema-form-render.js.map +1 -1
- package/dist/components/json-schema-form-resolve.cjs +6 -99
- package/dist/components/json-schema-form-resolve.cjs.map +1 -1
- package/dist/components/json-schema-form-resolve.d.ts +1 -8
- package/dist/components/json-schema-form-resolve.d.ts.map +1 -1
- package/dist/components/json-schema-form-resolve.js +5 -98
- package/dist/components/json-schema-form-resolve.js.map +1 -1
- package/dist/components/json-schema-form-runtime-types.d.ts +16 -0
- package/dist/components/json-schema-form-runtime-types.d.ts.map +1 -1
- package/dist/components/json-schema-form-table-array.cjs +67 -16
- package/dist/components/json-schema-form-table-array.cjs.map +1 -1
- package/dist/components/json-schema-form-table-array.d.ts +4 -1
- package/dist/components/json-schema-form-table-array.d.ts.map +1 -1
- package/dist/components/json-schema-form-table-array.js +70 -19
- package/dist/components/json-schema-form-table-array.js.map +1 -1
- package/dist/components/json-schema-form-tone.cjs +12 -0
- package/dist/components/json-schema-form-tone.cjs.map +1 -1
- package/dist/components/json-schema-form-tone.js +12 -0
- package/dist/components/json-schema-form-tone.js.map +1 -1
- package/dist/components/json-schema-form-types.d.ts +17 -0
- package/dist/components/json-schema-form-types.d.ts.map +1 -1
- package/dist/components/json-schema-form-utils.cjs +7 -1
- package/dist/components/json-schema-form-utils.cjs.map +1 -1
- package/dist/components/json-schema-form-utils.d.ts +2 -1
- package/dist/components/json-schema-form-utils.d.ts.map +1 -1
- package/dist/components/json-schema-form-utils.js +7 -1
- package/dist/components/json-schema-form-utils.js.map +1 -1
- package/dist/components.cjs +3 -2
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.ts +3 -2
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +2 -1
- package/dist/components.js.map +1 -1
- package/dist/data/DataTable.cjs +9 -3
- package/dist/data/DataTable.cjs.map +1 -1
- package/dist/data/DataTable.d.ts +6 -0
- package/dist/data/DataTable.d.ts.map +1 -1
- package/dist/data/DataTable.js +9 -3
- package/dist/data/DataTable.js.map +1 -1
- package/dist/data/FixtureEditor/FixtureEditor.cjs +43 -24
- package/dist/data/FixtureEditor/FixtureEditor.cjs.map +1 -1
- package/dist/data/FixtureEditor/FixtureEditor.d.ts.map +1 -1
- package/dist/data/FixtureEditor/FixtureEditor.js +43 -24
- package/dist/data/FixtureEditor/FixtureEditor.js.map +1 -1
- package/dist/data/GraphDiagram.cjs +125 -138
- package/dist/data/GraphDiagram.cjs.map +1 -1
- package/dist/data/GraphDiagram.d.ts +30 -23
- package/dist/data/GraphDiagram.d.ts.map +1 -1
- package/dist/data/GraphDiagram.js +128 -141
- package/dist/data/GraphDiagram.js.map +1 -1
- package/dist/data/GraphDiagramEdges.cjs +208 -0
- package/dist/data/GraphDiagramEdges.cjs.map +1 -0
- package/dist/data/GraphDiagramEdges.d.ts +40 -0
- package/dist/data/GraphDiagramEdges.d.ts.map +1 -0
- package/dist/data/GraphDiagramEdges.js +208 -0
- package/dist/data/GraphDiagramEdges.js.map +1 -0
- package/dist/data/GraphDiagramNodes.cjs +142 -0
- package/dist/data/GraphDiagramNodes.cjs.map +1 -0
- package/dist/data/GraphDiagramNodes.d.ts +22 -0
- package/dist/data/GraphDiagramNodes.d.ts.map +1 -0
- package/dist/data/GraphDiagramNodes.js +142 -0
- package/dist/data/GraphDiagramNodes.js.map +1 -0
- package/dist/data/Tree.cjs +10 -2
- package/dist/data/Tree.cjs.map +1 -1
- package/dist/data/Tree.d.ts +6 -1
- package/dist/data/Tree.d.ts.map +1 -1
- package/dist/data/Tree.js +10 -2
- package/dist/data/Tree.js.map +1 -1
- package/dist/data/ai/PromptRunEditor/RuntimeRows.cjs +8 -3
- package/dist/data/ai/PromptRunEditor/RuntimeRows.cjs.map +1 -1
- package/dist/data/ai/PromptRunEditor/RuntimeRows.d.ts.map +1 -1
- package/dist/data/ai/PromptRunEditor/RuntimeRows.js +8 -3
- package/dist/data/ai/PromptRunEditor/RuntimeRows.js.map +1 -1
- package/dist/data/ai/PromptRunEditor/index.cjs +37 -43
- package/dist/data/ai/PromptRunEditor/index.cjs.map +1 -1
- package/dist/data/ai/PromptRunEditor/index.d.ts +3 -1
- package/dist/data/ai/PromptRunEditor/index.d.ts.map +1 -1
- package/dist/data/ai/PromptRunEditor/index.js +38 -44
- package/dist/data/ai/PromptRunEditor/index.js.map +1 -1
- package/dist/data/ai/PromptRunEditor/runtimeActions.cjs +16 -5
- package/dist/data/ai/PromptRunEditor/runtimeActions.cjs.map +1 -1
- package/dist/data/ai/PromptRunEditor/runtimeActions.d.ts +4 -1
- package/dist/data/ai/PromptRunEditor/runtimeActions.d.ts.map +1 -1
- package/dist/data/ai/PromptRunEditor/runtimeActions.js +17 -6
- package/dist/data/ai/PromptRunEditor/runtimeActions.js.map +1 -1
- package/dist/data/ai/SessionInspector.approvals-model.cjs +12 -0
- package/dist/data/ai/SessionInspector.approvals-model.cjs.map +1 -1
- package/dist/data/ai/SessionInspector.approvals-model.d.ts +11 -2
- package/dist/data/ai/SessionInspector.approvals-model.d.ts.map +1 -1
- package/dist/data/ai/SessionInspector.approvals-model.js +12 -0
- package/dist/data/ai/SessionInspector.approvals-model.js.map +1 -1
- package/dist/data/ai/SessionInspector.approvals.cjs +25 -1
- package/dist/data/ai/SessionInspector.approvals.cjs.map +1 -1
- package/dist/data/ai/SessionInspector.approvals.d.ts.map +1 -1
- package/dist/data/ai/SessionInspector.approvals.js +26 -2
- package/dist/data/ai/SessionInspector.approvals.js.map +1 -1
- package/dist/data/ai/SessionInspector.cjs +143 -167
- package/dist/data/ai/SessionInspector.cjs.map +1 -1
- package/dist/data/ai/SessionInspector.collection-types.d.ts +5 -0
- package/dist/data/ai/SessionInspector.collection-types.d.ts.map +1 -1
- package/dist/data/ai/SessionInspector.collection.cjs +18 -5
- package/dist/data/ai/SessionInspector.collection.cjs.map +1 -1
- package/dist/data/ai/SessionInspector.collection.d.ts +2 -0
- package/dist/data/ai/SessionInspector.collection.d.ts.map +1 -1
- package/dist/data/ai/SessionInspector.collection.fixtures.d.ts +6 -0
- package/dist/data/ai/SessionInspector.collection.fixtures.d.ts.map +1 -0
- package/dist/data/ai/SessionInspector.collection.js +18 -5
- package/dist/data/ai/SessionInspector.collection.js.map +1 -1
- package/dist/data/ai/SessionInspector.d.ts +24 -2
- package/dist/data/ai/SessionInspector.d.ts.map +1 -1
- package/dist/data/ai/SessionInspector.hierarchy-state.cjs +105 -7
- package/dist/data/ai/SessionInspector.hierarchy-state.cjs.map +1 -1
- package/dist/data/ai/SessionInspector.hierarchy-state.d.ts +12 -1
- package/dist/data/ai/SessionInspector.hierarchy-state.d.ts.map +1 -1
- package/dist/data/ai/SessionInspector.hierarchy-state.js +107 -9
- package/dist/data/ai/SessionInspector.hierarchy-state.js.map +1 -1
- package/dist/data/ai/SessionInspector.hierarchy.cjs +1 -1
- package/dist/data/ai/SessionInspector.hierarchy.cjs.map +1 -1
- package/dist/data/ai/SessionInspector.hierarchy.js +1 -1
- package/dist/data/ai/SessionInspector.hierarchy.js.map +1 -1
- package/dist/data/ai/SessionInspector.js +144 -168
- package/dist/data/ai/SessionInspector.js.map +1 -1
- package/dist/data/ai/SessionInspector.legacy.cjs +116 -0
- package/dist/data/ai/SessionInspector.legacy.cjs.map +1 -0
- package/dist/data/ai/SessionInspector.legacy.d.ts +19 -0
- package/dist/data/ai/SessionInspector.legacy.d.ts.map +1 -0
- package/dist/data/ai/SessionInspector.legacy.js +116 -0
- package/dist/data/ai/SessionInspector.legacy.js.map +1 -0
- package/dist/data/ai/SessionInspector.panels.cjs +3 -0
- package/dist/data/ai/SessionInspector.panels.cjs.map +1 -1
- package/dist/data/ai/SessionInspector.panels.d.ts.map +1 -1
- package/dist/data/ai/SessionInspector.panels.js +3 -0
- package/dist/data/ai/SessionInspector.panels.js.map +1 -1
- package/dist/data/ai/SessionInspector.plan.cjs +29 -12
- package/dist/data/ai/SessionInspector.plan.cjs.map +1 -1
- package/dist/data/ai/SessionInspector.plan.d.ts.map +1 -1
- package/dist/data/ai/SessionInspector.plan.js +30 -13
- package/dist/data/ai/SessionInspector.plan.js.map +1 -1
- package/dist/data/ai/SessionInspector.remote-view.cjs +44 -0
- package/dist/data/ai/SessionInspector.remote-view.cjs.map +1 -0
- package/dist/data/ai/SessionInspector.remote-view.d.ts +11 -0
- package/dist/data/ai/SessionInspector.remote-view.d.ts.map +1 -0
- package/dist/data/ai/SessionInspector.remote-view.js +44 -0
- package/dist/data/ai/SessionInspector.remote-view.js.map +1 -0
- package/dist/data/ai/SessionInspector.remote.cjs +275 -0
- package/dist/data/ai/SessionInspector.remote.cjs.map +1 -0
- package/dist/data/ai/SessionInspector.remote.d.ts +57 -0
- package/dist/data/ai/SessionInspector.remote.d.ts.map +1 -0
- package/dist/data/ai/SessionInspector.remote.js +275 -0
- package/dist/data/ai/SessionInspector.remote.js.map +1 -0
- package/dist/data/ai/SessionInspector.summary.cjs +9 -159
- package/dist/data/ai/SessionInspector.summary.cjs.map +1 -1
- package/dist/data/ai/SessionInspector.summary.d.ts +0 -3
- package/dist/data/ai/SessionInspector.summary.d.ts.map +1 -1
- package/dist/data/ai/SessionInspector.summary.js +11 -161
- package/dist/data/ai/SessionInspector.summary.js.map +1 -1
- package/dist/data/ai/SessionInspector.tab-state.cjs +15 -0
- package/dist/data/ai/SessionInspector.tab-state.cjs.map +1 -0
- package/dist/data/ai/SessionInspector.tab-state.d.ts +3 -0
- package/dist/data/ai/SessionInspector.tab-state.d.ts.map +1 -0
- package/dist/data/ai/SessionInspector.tab-state.js +15 -0
- package/dist/data/ai/SessionInspector.tab-state.js.map +1 -0
- package/dist/data/ai/SessionInspector.tabs.model.cjs +31 -18
- package/dist/data/ai/SessionInspector.tabs.model.cjs.map +1 -1
- package/dist/data/ai/SessionInspector.tabs.model.d.ts +3 -1
- package/dist/data/ai/SessionInspector.tabs.model.d.ts.map +1 -1
- package/dist/data/ai/SessionInspector.tabs.model.js +32 -19
- package/dist/data/ai/SessionInspector.tabs.model.js.map +1 -1
- package/dist/data/ai/SessionInspector.transcript.cjs +4 -0
- package/dist/data/ai/SessionInspector.transcript.cjs.map +1 -1
- package/dist/data/ai/SessionInspector.transcript.d.ts.map +1 -1
- package/dist/data/ai/SessionInspector.transcript.js +4 -0
- package/dist/data/ai/SessionInspector.transcript.js.map +1 -1
- package/dist/data/ai/SessionInspector.verification.cjs +53 -0
- package/dist/data/ai/SessionInspector.verification.cjs.map +1 -0
- package/dist/data/ai/SessionInspector.verification.d.ts +7 -0
- package/dist/data/ai/SessionInspector.verification.d.ts.map +1 -0
- package/dist/data/ai/SessionInspector.verification.js +53 -0
- package/dist/data/ai/SessionInspector.verification.js.map +1 -0
- package/dist/data/ai/SessionViewer.approval-body.cjs +146 -0
- package/dist/data/ai/SessionViewer.approval-body.cjs.map +1 -0
- package/dist/data/ai/SessionViewer.approval-body.d.ts +11 -0
- package/dist/data/ai/SessionViewer.approval-body.d.ts.map +1 -0
- package/dist/data/ai/SessionViewer.approval-body.js +146 -0
- package/dist/data/ai/SessionViewer.approval-body.js.map +1 -0
- package/dist/data/ai/SessionViewer.approval-forms.cjs +150 -0
- package/dist/data/ai/SessionViewer.approval-forms.cjs.map +1 -0
- package/dist/data/ai/SessionViewer.approval-forms.d.ts +23 -0
- package/dist/data/ai/SessionViewer.approval-forms.d.ts.map +1 -0
- package/dist/data/ai/SessionViewer.approval-forms.js +150 -0
- package/dist/data/ai/SessionViewer.approval-forms.js.map +1 -0
- package/dist/data/ai/SessionViewer.cjs +57 -20
- package/dist/data/ai/SessionViewer.cjs.map +1 -1
- package/dist/data/ai/SessionViewer.d.ts +13 -3
- package/dist/data/ai/SessionViewer.d.ts.map +1 -1
- package/dist/data/ai/SessionViewer.decision-controls.cjs +313 -0
- package/dist/data/ai/SessionViewer.decision-controls.cjs.map +1 -0
- package/dist/data/ai/SessionViewer.decision-controls.d.ts +43 -0
- package/dist/data/ai/SessionViewer.decision-controls.d.ts.map +1 -0
- package/dist/data/ai/SessionViewer.decision-controls.js +313 -0
- package/dist/data/ai/SessionViewer.decision-controls.js.map +1 -0
- package/dist/data/ai/SessionViewer.decision.cjs +21 -0
- package/dist/data/ai/SessionViewer.decision.cjs.map +1 -0
- package/dist/data/ai/SessionViewer.decision.d.ts +12 -0
- package/dist/data/ai/SessionViewer.decision.d.ts.map +1 -0
- package/dist/data/ai/SessionViewer.decision.js +21 -0
- package/dist/data/ai/SessionViewer.decision.js.map +1 -0
- package/dist/data/ai/SessionViewer.input.cjs +14 -0
- package/dist/data/ai/SessionViewer.input.cjs.map +1 -1
- package/dist/data/ai/SessionViewer.input.d.ts +5 -0
- package/dist/data/ai/SessionViewer.input.d.ts.map +1 -1
- package/dist/data/ai/SessionViewer.input.js +14 -0
- package/dist/data/ai/SessionViewer.input.js.map +1 -1
- package/dist/data/ai/SessionViewer.js +58 -21
- package/dist/data/ai/SessionViewer.js.map +1 -1
- package/dist/data/ai/SessionViewer.model.cjs +7 -1
- package/dist/data/ai/SessionViewer.model.cjs.map +1 -1
- package/dist/data/ai/SessionViewer.model.d.ts +25 -3
- package/dist/data/ai/SessionViewer.model.d.ts.map +1 -1
- package/dist/data/ai/SessionViewer.model.js +7 -1
- package/dist/data/ai/SessionViewer.model.js.map +1 -1
- package/dist/data/ai/SessionViewer.rows.cjs +6 -3
- package/dist/data/ai/SessionViewer.rows.cjs.map +1 -1
- package/dist/data/ai/SessionViewer.rows.d.ts +2 -1
- package/dist/data/ai/SessionViewer.rows.d.ts.map +1 -1
- package/dist/data/ai/SessionViewer.rows.js +6 -3
- package/dist/data/ai/SessionViewer.rows.js.map +1 -1
- package/dist/data/ai/SessionViewer.tool-row.cjs +29 -237
- package/dist/data/ai/SessionViewer.tool-row.cjs.map +1 -1
- package/dist/data/ai/SessionViewer.tool-row.d.ts.map +1 -1
- package/dist/data/ai/SessionViewer.tool-row.js +27 -235
- package/dist/data/ai/SessionViewer.tool-row.js.map +1 -1
- package/dist/data/ai/SessionViewer.unified.d.ts +17 -0
- package/dist/data/ai/SessionViewer.unified.d.ts.map +1 -1
- package/dist/data/ai/SessionViewerMenu.cjs +8 -2
- package/dist/data/ai/SessionViewerMenu.cjs.map +1 -1
- package/dist/data/ai/SessionViewerMenu.d.ts +4 -1
- package/dist/data/ai/SessionViewerMenu.d.ts.map +1 -1
- package/dist/data/ai/SessionViewerMenu.js +8 -2
- package/dist/data/ai/SessionViewerMenu.js.map +1 -1
- package/dist/data/ai/SpecRuntimeEditor/CommitSection.cjs +28 -17
- package/dist/data/ai/SpecRuntimeEditor/CommitSection.cjs.map +1 -1
- package/dist/data/ai/SpecRuntimeEditor/CommitSection.d.ts +10 -2
- package/dist/data/ai/SpecRuntimeEditor/CommitSection.d.ts.map +1 -1
- package/dist/data/ai/SpecRuntimeEditor/CommitSection.js +29 -18
- package/dist/data/ai/SpecRuntimeEditor/CommitSection.js.map +1 -1
- package/dist/data/ai/SpecRuntimeEditor/index.cjs +9 -1
- package/dist/data/ai/SpecRuntimeEditor/index.cjs.map +1 -1
- package/dist/data/ai/SpecRuntimeEditor/index.d.ts +9 -2
- package/dist/data/ai/SpecRuntimeEditor/index.d.ts.map +1 -1
- package/dist/data/ai/SpecRuntimeEditor/index.js +9 -1
- package/dist/data/ai/SpecRuntimeEditor/index.js.map +1 -1
- package/dist/data/ai/SpecRuntimeEditor/permission-mode-visuals.cjs +1 -1
- package/dist/data/ai/SpecRuntimeEditor/permission-mode-visuals.js +1 -1
- package/dist/data/ai/SpecRuntimeEditor/summaries.cjs.map +1 -1
- package/dist/data/ai/SpecRuntimeEditor/summaries.d.ts +8 -2
- package/dist/data/ai/SpecRuntimeEditor/summaries.d.ts.map +1 -1
- package/dist/data/ai/SpecRuntimeEditor/summaries.js.map +1 -1
- package/dist/data/ai/SpecRuntimeEditor/update.cjs +14 -6
- package/dist/data/ai/SpecRuntimeEditor/update.cjs.map +1 -1
- package/dist/data/ai/SpecRuntimeEditor/update.d.ts +1 -0
- package/dist/data/ai/SpecRuntimeEditor/update.d.ts.map +1 -1
- package/dist/data/ai/SpecRuntimeEditor/update.js +14 -6
- package/dist/data/ai/SpecRuntimeEditor/update.js.map +1 -1
- package/dist/data/ai/approval-request.cjs +75 -0
- package/dist/data/ai/approval-request.cjs.map +1 -0
- package/dist/data/ai/approval-request.d.ts +155 -0
- package/dist/data/ai/approval-request.d.ts.map +1 -0
- package/dist/data/ai/approval-request.js +75 -0
- package/dist/data/ai/approval-request.js.map +1 -0
- package/dist/data/ai/index.d.ts +6 -1
- package/dist/data/ai/index.d.ts.map +1 -1
- package/dist/data/ai/runtime-preset-menu/SavePresetDialog.cjs +125 -0
- package/dist/data/ai/runtime-preset-menu/SavePresetDialog.cjs.map +1 -0
- package/dist/data/ai/runtime-preset-menu/SavePresetDialog.d.ts +11 -0
- package/dist/data/ai/runtime-preset-menu/SavePresetDialog.d.ts.map +1 -0
- package/dist/data/ai/runtime-preset-menu/SavePresetDialog.js +125 -0
- package/dist/data/ai/runtime-preset-menu/SavePresetDialog.js.map +1 -0
- package/dist/data/ai/runtime-preset-menu/index.d.ts +3 -0
- package/dist/data/ai/runtime-preset-menu/index.d.ts.map +1 -0
- package/dist/data/ai/runtime-preset-menu/model.cjs +73 -0
- package/dist/data/ai/runtime-preset-menu/model.cjs.map +1 -0
- package/dist/data/ai/runtime-preset-menu/model.d.ts +13 -0
- package/dist/data/ai/runtime-preset-menu/model.d.ts.map +1 -0
- package/dist/data/ai/runtime-preset-menu/model.js +73 -0
- package/dist/data/ai/runtime-preset-menu/model.js.map +1 -0
- package/dist/data/ai/runtime-preset-menu/use-runtime-preset-menu.cjs +149 -0
- package/dist/data/ai/runtime-preset-menu/use-runtime-preset-menu.cjs.map +1 -0
- package/dist/data/ai/runtime-preset-menu/use-runtime-preset-menu.d.ts +15 -0
- package/dist/data/ai/runtime-preset-menu/use-runtime-preset-menu.d.ts.map +1 -0
- package/dist/data/ai/runtime-preset-menu/use-runtime-preset-menu.js +149 -0
- package/dist/data/ai/runtime-preset-menu/use-runtime-preset-menu.js.map +1 -0
- package/dist/data/ai/runtime-spec-fields.cjs +166 -0
- package/dist/data/ai/runtime-spec-fields.cjs.map +1 -0
- package/dist/data/ai/runtime-spec-fields.d.ts +13 -0
- package/dist/data/ai/runtime-spec-fields.d.ts.map +1 -0
- package/dist/data/ai/runtime-spec-fields.js +166 -0
- package/dist/data/ai/runtime-spec-fields.js.map +1 -0
- package/dist/data/ai/session-categories.cjs +4 -0
- package/dist/data/ai/session-categories.cjs.map +1 -1
- package/dist/data/ai/session-categories.d.ts +1 -0
- package/dist/data/ai/session-categories.d.ts.map +1 -1
- package/dist/data/ai/session-categories.js +4 -0
- package/dist/data/ai/session-categories.js.map +1 -1
- package/dist/data/ai/use-session-scroll.cjs +49 -21
- package/dist/data/ai/use-session-scroll.cjs.map +1 -1
- package/dist/data/ai/use-session-scroll.d.ts +2 -0
- package/dist/data/ai/use-session-scroll.d.ts.map +1 -1
- package/dist/data/ai/use-session-scroll.js +49 -21
- package/dist/data/ai/use-session-scroll.js.map +1 -1
- package/dist/data/chat/Chat.runtime.cjs +1 -1
- package/dist/data/chat/Chat.runtime.cjs.map +1 -1
- package/dist/data/chat/Chat.runtime.d.ts.map +1 -1
- package/dist/data/chat/Chat.runtime.js +1 -1
- package/dist/data/chat/Chat.runtime.js.map +1 -1
- package/dist/data/graph-columns-layout.cjs +216 -0
- package/dist/data/graph-columns-layout.cjs.map +1 -0
- package/dist/data/graph-columns-layout.d.ts +56 -0
- package/dist/data/graph-columns-layout.d.ts.map +1 -0
- package/dist/data/graph-columns-layout.js +216 -0
- package/dist/data/graph-columns-layout.js.map +1 -0
- package/dist/data/graph-diagram-model.cjs +73 -0
- package/dist/data/graph-diagram-model.cjs.map +1 -0
- package/dist/data/graph-diagram-model.d.ts +90 -0
- package/dist/data/graph-diagram-model.d.ts.map +1 -0
- package/dist/data/graph-diagram-model.js +73 -0
- package/dist/data/graph-diagram-model.js.map +1 -0
- package/dist/data/graph-edge-routing.cjs +235 -0
- package/dist/data/graph-edge-routing.cjs.map +1 -0
- package/dist/data/graph-edge-routing.d.ts +73 -0
- package/dist/data/graph-edge-routing.d.ts.map +1 -0
- package/dist/data/graph-edge-routing.js +235 -0
- package/dist/data/graph-edge-routing.js.map +1 -0
- package/dist/data/graph-layout.cjs +0 -82
- package/dist/data/graph-layout.cjs.map +1 -1
- package/dist/data/graph-layout.d.ts +1 -28
- package/dist/data/graph-layout.d.ts.map +1 -1
- package/dist/data/graph-layout.js +1 -83
- package/dist/data/graph-layout.js.map +1 -1
- package/dist/data/har/HarPanel.cjs +129 -33
- package/dist/data/har/HarPanel.cjs.map +1 -1
- package/dist/data/har/HarPanel.d.ts +13 -1
- package/dist/data/har/HarPanel.d.ts.map +1 -1
- package/dist/data/har/HarPanel.js +130 -34
- package/dist/data/har/HarPanel.js.map +1 -1
- package/dist/data/har/fixtures.d.ts +12 -0
- package/dist/data/har/fixtures.d.ts.map +1 -1
- package/dist/data/har/types.d.ts +12 -0
- package/dist/data/har/types.d.ts.map +1 -1
- package/dist/data/runtime/RuntimeBar.cjs +67 -84
- package/dist/data/runtime/RuntimeBar.cjs.map +1 -1
- package/dist/data/runtime/RuntimeBar.d.ts +4 -5
- package/dist/data/runtime/RuntimeBar.d.ts.map +1 -1
- package/dist/data/runtime/RuntimeBar.fields.cjs +170 -0
- package/dist/data/runtime/RuntimeBar.fields.cjs.map +1 -0
- package/dist/data/runtime/RuntimeBar.fields.d.ts +16 -0
- package/dist/data/runtime/RuntimeBar.fields.d.ts.map +1 -0
- package/dist/data/runtime/RuntimeBar.fields.js +170 -0
- package/dist/data/runtime/RuntimeBar.fields.js.map +1 -0
- package/dist/data/runtime/RuntimeBar.js +70 -87
- package/dist/data/runtime/RuntimeBar.js.map +1 -1
- package/dist/data/runtime/RuntimeBar.selection.cjs +56 -0
- package/dist/data/runtime/RuntimeBar.selection.cjs.map +1 -0
- package/dist/data/runtime/RuntimeBar.selection.d.ts +20 -0
- package/dist/data/runtime/RuntimeBar.selection.d.ts.map +1 -0
- package/dist/data/runtime/RuntimeBar.selection.js +56 -0
- package/dist/data/runtime/RuntimeBar.selection.js.map +1 -0
- package/dist/data/runtime/RuntimeBarActions.cjs +10 -6
- package/dist/data/runtime/RuntimeBarActions.cjs.map +1 -1
- package/dist/data/runtime/RuntimeBarActions.d.ts +8 -1
- package/dist/data/runtime/RuntimeBarActions.d.ts.map +1 -1
- package/dist/data/runtime/RuntimeBarActions.js +10 -6
- package/dist/data/runtime/RuntimeBarActions.js.map +1 -1
- package/dist/data/runtime/RuntimeBarIdentity.cjs +166 -0
- package/dist/data/runtime/RuntimeBarIdentity.cjs.map +1 -0
- package/dist/data/runtime/RuntimeBarIdentity.d.ts +25 -0
- package/dist/data/runtime/RuntimeBarIdentity.d.ts.map +1 -0
- package/dist/data/runtime/RuntimeBarIdentity.js +166 -0
- package/dist/data/runtime/RuntimeBarIdentity.js.map +1 -0
- package/dist/data/runtime/RuntimeBarLayout.cjs +95 -0
- package/dist/data/runtime/RuntimeBarLayout.cjs.map +1 -0
- package/dist/data/runtime/RuntimeBarLayout.d.ts +9 -0
- package/dist/data/runtime/RuntimeBarLayout.d.ts.map +1 -0
- package/dist/data/runtime/RuntimeBarLayout.js +95 -0
- package/dist/data/runtime/RuntimeBarLayout.js.map +1 -0
- package/dist/data/runtime/RuntimeBarSegment.cjs +25 -14
- package/dist/data/runtime/RuntimeBarSegment.cjs.map +1 -1
- package/dist/data/runtime/RuntimeBarSegment.d.ts +4 -0
- package/dist/data/runtime/RuntimeBarSegment.d.ts.map +1 -1
- package/dist/data/runtime/RuntimeBarSegment.js +25 -14
- package/dist/data/runtime/RuntimeBarSegment.js.map +1 -1
- package/dist/data/runtime/RuntimeLimitInput.cjs +41 -0
- package/dist/data/runtime/RuntimeLimitInput.cjs.map +1 -0
- package/dist/data/runtime/RuntimeLimitInput.d.ts +9 -0
- package/dist/data/runtime/RuntimeLimitInput.d.ts.map +1 -0
- package/dist/data/runtime/RuntimeLimitInput.js +41 -0
- package/dist/data/runtime/RuntimeLimitInput.js.map +1 -0
- package/dist/data/runtime/use-runtime-bar-overflow.cjs +70 -0
- package/dist/data/runtime/use-runtime-bar-overflow.cjs.map +1 -0
- package/dist/data/runtime/use-runtime-bar-overflow.d.ts +13 -0
- package/dist/data/runtime/use-runtime-bar-overflow.d.ts.map +1 -0
- package/dist/data/runtime/use-runtime-bar-overflow.js +70 -0
- package/dist/data/runtime/use-runtime-bar-overflow.js.map +1 -0
- package/dist/data/use-edge-focus.cjs +20 -0
- package/dist/data/use-edge-focus.cjs.map +1 -0
- package/dist/data/use-edge-focus.d.ts +23 -0
- package/dist/data/use-edge-focus.d.ts.map +1 -0
- package/dist/data/use-edge-focus.js +20 -0
- package/dist/data/use-edge-focus.js.map +1 -0
- package/dist/data/use-pan-zoom.cjs +159 -0
- package/dist/data/use-pan-zoom.cjs.map +1 -0
- package/dist/data/use-pan-zoom.d.ts +79 -0
- package/dist/data/use-pan-zoom.d.ts.map +1 -0
- package/dist/data/use-pan-zoom.js +159 -0
- package/dist/data/use-pan-zoom.js.map +1 -0
- package/dist/data/version-info.cjs +3 -3
- package/dist/data/version-info.js +3 -3
- package/dist/data.cjs +5 -1
- package/dist/data.cjs.map +1 -1
- package/dist/data.d.ts +4 -3
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +6 -2
- package/dist/data.js.map +1 -1
- package/dist/devtools/useDebugStream.cjs +5 -1
- package/dist/devtools/useDebugStream.cjs.map +1 -1
- package/dist/devtools/useDebugStream.d.ts.map +1 -1
- package/dist/devtools/useDebugStream.js +6 -2
- package/dist/devtools/useDebugStream.js.map +1 -1
- package/dist/hooks/event-source-provider.cjs +9 -0
- package/dist/hooks/event-source-provider.cjs.map +1 -0
- package/dist/hooks/event-source-provider.d.ts +15 -0
- package/dist/hooks/event-source-provider.d.ts.map +1 -0
- package/dist/hooks/event-source-provider.js +9 -0
- package/dist/hooks/event-source-provider.js.map +1 -0
- package/dist/hooks/event-source.cjs +11 -0
- package/dist/hooks/event-source.cjs.map +1 -0
- package/dist/hooks/event-source.d.ts +22 -0
- package/dist/hooks/event-source.d.ts.map +1 -0
- package/dist/hooks/event-source.js +11 -0
- package/dist/hooks/event-source.js.map +1 -0
- package/dist/hooks/use-log-tail.cjs +5 -1
- package/dist/hooks/use-log-tail.cjs.map +1 -1
- package/dist/hooks/use-log-tail.d.ts.map +1 -1
- package/dist/hooks/use-log-tail.js +5 -1
- package/dist/hooks/use-log-tail.js.map +1 -1
- package/dist/hooks/use-prompts.cjs +5 -1
- package/dist/hooks/use-prompts.cjs.map +1 -1
- package/dist/hooks/use-prompts.d.ts.map +1 -1
- package/dist/hooks/use-prompts.js +6 -2
- package/dist/hooks/use-prompts.js.map +1 -1
- package/dist/hooks/use-task-run.cjs +9 -2
- package/dist/hooks/use-task-run.cjs.map +1 -1
- package/dist/hooks/use-task-run.d.ts.map +1 -1
- package/dist/hooks/use-task-run.js +10 -3
- package/dist/hooks/use-task-run.js.map +1 -1
- package/dist/hooks.cjs +4 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +4 -0
- package/dist/hooks.js.map +1 -1
- package/dist/icons/programmingCatalog.cjs +34 -34
- package/dist/icons/programmingCatalog.cjs.map +1 -1
- package/dist/icons/programmingCatalog.js +34 -34
- package/dist/icons/programmingCatalog.js.map +1 -1
- package/dist/index.cjs +14 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/monaco/MonacoEditor.cjs +4 -1
- package/dist/monaco/MonacoEditor.cjs.map +1 -1
- package/dist/monaco/MonacoEditor.d.ts +1 -1
- package/dist/monaco/MonacoEditor.d.ts.map +1 -1
- package/dist/monaco/MonacoEditor.js +4 -1
- package/dist/monaco/MonacoEditor.js.map +1 -1
- package/dist/monaco/types.d.ts +2 -1
- package/dist/monaco/types.d.ts.map +1 -1
- package/dist/overlay/CommandPalette.cjs +14 -4
- package/dist/overlay/CommandPalette.cjs.map +1 -1
- package/dist/overlay/CommandPalette.d.ts +5 -1
- package/dist/overlay/CommandPalette.d.ts.map +1 -1
- package/dist/overlay/CommandPalette.js +14 -4
- package/dist/overlay/CommandPalette.js.map +1 -1
- package/dist/overlay/CommandPaletteList.cjs +5 -0
- package/dist/overlay/CommandPaletteList.cjs.map +1 -1
- package/dist/overlay/CommandPaletteList.d.ts +4 -2
- package/dist/overlay/CommandPaletteList.d.ts.map +1 -1
- package/dist/overlay/CommandPaletteList.js +5 -0
- package/dist/overlay/CommandPaletteList.js.map +1 -1
- package/dist/overlay/DropdownMenu.cjs +2 -0
- package/dist/overlay/DropdownMenu.cjs.map +1 -1
- package/dist/overlay/DropdownMenu.d.ts +4 -0
- package/dist/overlay/DropdownMenu.d.ts.map +1 -1
- package/dist/overlay/DropdownMenu.js +2 -0
- package/dist/overlay/DropdownMenu.js.map +1 -1
- package/dist/overlay/DropdownMenuSubmenu.cjs +31 -15
- package/dist/overlay/DropdownMenuSubmenu.cjs.map +1 -1
- package/dist/overlay/DropdownMenuSubmenu.d.ts.map +1 -1
- package/dist/overlay/DropdownMenuSubmenu.js +31 -15
- package/dist/overlay/DropdownMenuSubmenu.js.map +1 -1
- package/dist/rpc/OperationCatalog.cjs +32 -27
- package/dist/rpc/OperationCatalog.cjs.map +1 -1
- package/dist/rpc/OperationCatalog.d.ts +11 -1
- package/dist/rpc/OperationCatalog.d.ts.map +1 -1
- package/dist/rpc/OperationCatalog.js +32 -27
- package/dist/rpc/OperationCatalog.js.map +1 -1
- package/dist/rpc/OperationEntityContextPicker.cjs +62 -256
- package/dist/rpc/OperationEntityContextPicker.cjs.map +1 -1
- package/dist/rpc/OperationEntityContextPicker.d.ts.map +1 -1
- package/dist/rpc/OperationEntityContextPicker.js +64 -258
- package/dist/rpc/OperationEntityContextPicker.js.map +1 -1
- package/dist/rpc/OperationEntityPicker.cjs +185 -0
- package/dist/rpc/OperationEntityPicker.cjs.map +1 -0
- package/dist/rpc/OperationEntityPicker.d.ts +51 -0
- package/dist/rpc/OperationEntityPicker.d.ts.map +1 -0
- package/dist/rpc/OperationEntityPicker.js +185 -0
- package/dist/rpc/OperationEntityPicker.js.map +1 -0
- package/dist/rpc/OperationEntityPicker.model.cjs +39 -0
- package/dist/rpc/OperationEntityPicker.model.cjs.map +1 -0
- package/dist/rpc/OperationEntityPicker.model.d.ts +27 -0
- package/dist/rpc/OperationEntityPicker.model.d.ts.map +1 -0
- package/dist/rpc/OperationEntityPicker.model.js +39 -0
- package/dist/rpc/OperationEntityPicker.model.js.map +1 -0
- package/dist/rpc/operationCatalogRowSelection.cjs +85 -0
- package/dist/rpc/operationCatalogRowSelection.cjs.map +1 -0
- package/dist/rpc/operationCatalogRowSelection.d.ts +57 -0
- package/dist/rpc/operationCatalogRowSelection.d.ts.map +1 -0
- package/dist/rpc/operationCatalogRowSelection.js +85 -0
- package/dist/rpc/operationCatalogRowSelection.js.map +1 -0
- package/dist/rpc/operationEntityPicker.fixtures.d.ts +13 -0
- package/dist/rpc/operationEntityPicker.fixtures.d.ts.map +1 -0
- package/dist/rpc/useOperationPages.cjs +6 -1
- package/dist/rpc/useOperationPages.cjs.map +1 -1
- package/dist/rpc/useOperationPages.d.ts +3 -1
- package/dist/rpc/useOperationPages.d.ts.map +1 -1
- package/dist/rpc/useOperationPages.js +6 -1
- package/dist/rpc/useOperationPages.js.map +1 -1
- package/dist/rpc.cjs +2 -0
- package/dist/rpc.cjs.map +1 -1
- package/dist/rpc.d.ts +2 -0
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +2 -0
- package/dist/rpc.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +4 -1
- package/schemas/json-schema-form.schema.json +571 -0
- package/dist/data/runtime/RuntimeBarCombo.cjs +0 -423
- package/dist/data/runtime/RuntimeBarCombo.cjs.map +0 -1
- package/dist/data/runtime/RuntimeBarCombo.d.ts +0 -33
- package/dist/data/runtime/RuntimeBarCombo.d.ts.map +0 -1
- package/dist/data/runtime/RuntimeBarCombo.js +0 -423
- package/dist/data/runtime/RuntimeBarCombo.js.map +0 -1
- package/dist/data/runtime/RuntimeBarLimits.cjs +0 -262
- package/dist/data/runtime/RuntimeBarLimits.cjs.map +0 -1
- package/dist/data/runtime/RuntimeBarLimits.d.ts +0 -17
- package/dist/data/runtime/RuntimeBarLimits.d.ts.map +0 -1
- package/dist/data/runtime/RuntimeBarLimits.js +0 -262
- package/dist/data/runtime/RuntimeBarLimits.js.map +0 -1
- package/dist/data/runtime/RuntimeBarSegments.cjs +0 -346
- package/dist/data/runtime/RuntimeBarSegments.cjs.map +0 -1
- package/dist/data/runtime/RuntimeBarSegments.d.ts +0 -37
- package/dist/data/runtime/RuntimeBarSegments.d.ts.map +0 -1
- package/dist/data/runtime/RuntimeBarSegments.js +0 -346
- package/dist/data/runtime/RuntimeBarSegments.js.map +0 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useContext, createContext } from "react";
|
|
2
|
+
const defaultEventSourceFactory = (url) => new EventSource(url);
|
|
3
|
+
const EventSourceFactoryContext = createContext(defaultEventSourceFactory);
|
|
4
|
+
function useEventSourceFactory() {
|
|
5
|
+
return useContext(EventSourceFactoryContext);
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
EventSourceFactoryContext,
|
|
9
|
+
useEventSourceFactory
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=event-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-source.js","sources":["../../src/hooks/event-source.ts"],"sourcesContent":["import { createContext, useContext } from \"react\";\n\n// event-source is the injection seam every SSE-opening hook in clicky-ui\n// (useTaskRun/useTaskRuns, usePrompts, useLogTail, useDebugStream) reads\n// through instead of calling `new EventSource(url)` directly. A host that\n// needs to route every SSE connection through its own transport — e.g. to\n// multiplex several streams over one connection and work around the\n// six-connections-per-origin cap HTTP/1.1 imposes — wraps its tree in\n// `EventSourceProvider` (./event-source-provider) once, and every hook picks\n// the substitute up without per-call plumbing. Without a provider, hooks fall\n// back to the browser's native `EventSource`.\n//\n// The Provider component lives in its own file (event-source-provider.tsx) so\n// this one stays JSX-free and exports only the context/hook — matching\n// use-theme.ts/theme-provider.tsx and use-density.ts/density-provider.tsx.\n\n/**\n * The subset of the DOM `EventSource` surface the hooks in this package\n * actually use. A host's injected factory only has to return something that\n * satisfies this — never the full `EventSource` interface — so a consumer\n * hook must never narrow with `instanceof EventSource`.\n */\nexport interface EventSourceLike {\n addEventListener(type: string, listener: EventListenerOrEventListenerObject): void;\n removeEventListener(type: string, listener: EventListenerOrEventListenerObject): void;\n close(): void;\n readonly readyState: number;\n readonly url: string;\n onopen: ((ev: Event) => unknown) | null;\n onmessage: ((ev: MessageEvent) => unknown) | null;\n onerror: ((ev: Event) => unknown) | null;\n}\n\n/** Builds an EventSource-compatible connection for a URL. */\nexport type EventSourceFactory = (url: string) => EventSourceLike;\n\nconst defaultEventSourceFactory: EventSourceFactory = (url) => new EventSource(url);\n\nexport const EventSourceFactoryContext = createContext<EventSourceFactory>(defaultEventSourceFactory);\n\n/** The active `EventSourceFactory` — the provided one, or `(url) => new EventSource(url)`. */\nexport function useEventSourceFactory(): EventSourceFactory {\n return useContext(EventSourceFactoryContext);\n}\n"],"names":[],"mappings":";AAoCA,MAAM,4BAAgD,CAAC,QAAQ,IAAI,YAAY,GAAG;AAE3E,MAAM,4BAA4B,cAAkC,yBAAyB;AAG7F,SAAS,wBAA4C;AAC1D,SAAO,WAAW,yBAAyB;AAC7C;"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const react = require("react");
|
|
4
4
|
const sessionTypes = require("../rpc/sessionTypes.cjs");
|
|
5
|
+
const eventSource = require("./event-source.cjs");
|
|
5
6
|
function isTerminalSessionState(state) {
|
|
6
7
|
return state !== void 0 && sessionTypes.isSessionTerminal(state);
|
|
7
8
|
}
|
|
@@ -62,6 +63,9 @@ function useLogTail(options) {
|
|
|
62
63
|
maxRowsRef.current = maxRows;
|
|
63
64
|
const onEventRef = react.useRef(onEvent);
|
|
64
65
|
onEventRef.current = onEvent;
|
|
66
|
+
const eventSourceFactory = eventSource.useEventSourceFactory();
|
|
67
|
+
const eventSourceFactoryRef = react.useRef(eventSourceFactory);
|
|
68
|
+
eventSourceFactoryRef.current = eventSourceFactory;
|
|
65
69
|
const paramsQuery = encodeTailParams(params);
|
|
66
70
|
react.useEffect(() => {
|
|
67
71
|
if (!wantFollow) return;
|
|
@@ -118,7 +122,7 @@ function useLogTail(options) {
|
|
|
118
122
|
}
|
|
119
123
|
};
|
|
120
124
|
const subscribe = (id) => {
|
|
121
|
-
const es =
|
|
125
|
+
const es = eventSourceFactoryRef.current(`${basePath}/sessions/${encodeURIComponent(id)}/events`);
|
|
122
126
|
source = es;
|
|
123
127
|
setStatus("streaming");
|
|
124
128
|
es.addEventListener("event", (e) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-log-tail.cjs","sources":["../../src/hooks/use-log-tail.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ClickyRow } from \"../data/Clicky\";\nimport { isSessionState, isSessionTerminal, type SessionState } from \"../rpc/sessionTypes\";\n\n// use-log-tail is the clicky-ui client for a *follow* session: the server opens\n// a live source for a query profile and streams rows out of it until someone\n// closes it. The resource underneath is unforgiving. A follow session holds a\n// websocket to Loki, or a log stream to a kubelet, for as long as it exists,\n// and the server caps how many may run at once (ErrMaxSessions, surfaced as\n// HTTP 409). The server owns that slot's release: a view session ends shortly\n// after its last subscriber disconnects, so every exit path — unmount and a\n// parameter change included — only has to abort the start request and close\n// the EventSource.\n//\n// Events travel over SSE only. The events route serves SSE (or ndjson on\n// request) and resumes through Last-Event-ID; there is no JSON-array polling\n// shape to fall back to, so a browser without EventSource fails loudly.\n//\n// Each SSE frame carries an `id:`, so a reconnecting EventSource sends\n// Last-Event-ID and the server resumes from that sequence — re-delivering\n// frames the client may already hold. Sequences are the server's, not a client\n// index: they do not start at 1 and they do not reset, so the accumulator\n// dedupes against the highest sequence it has applied rather than against a\n// position in an array.\n\n/** The sessions API's state, under the name this hook has always exported. */\nexport type LogSessionState = SessionState;\n\n/** SessionInfo as returned by POST /profile/{name}/sessions and the `done` frame. */\nexport interface LogSessionInfo {\n id: string;\n profile: string;\n kind: string;\n state: LogSessionState;\n params?: Record<string, unknown>;\n error?: string;\n eventCount: number;\n startedAt: string;\n stoppedAt?: string;\n}\n\n/**\n * One `event:` frame. It carries either a single `row` or a batch under `rows`\n * — never both — plus an `error` when the source failed mid-stream without the\n * session itself ending.\n *\n * `clickyRow` is the presented row — `{cells, detail?}`, built by the exact\n * function that fills in `node.rows[n]` of the list's own\n * `application/json+clicky` document — riding next to the raw `row` on a\n * trace/follow session's per-row events. A plain log tail (or a top-session\n * snapshot event) never sets it; only a reader that reconciles rows against a\n * presented table, i.e. OperationCatalog's follow mode, depends on it.\n */\nexport interface LogTailEvent {\n sessionId: string;\n sequence: number;\n time?: string;\n row?: Record<string, unknown>;\n rows?: Record<string, unknown>[];\n /** The presented ClickyRow for `row`, when the profile sends one. */\n clickyRow?: ClickyRow;\n error?: string;\n}\n\n/**\n * Where a failure came from, because the three are not interchangeable to a\n * reader: `start` means the session never existed (409 is the cap), `stream`\n * means the session is alive but something in it went wrong, and `session`\n * means the server ended it.\n */\nexport interface LogTailError {\n scope: \"start\" | \"stream\" | \"session\";\n message: string;\n /** HTTP status when a request produced the failure. 409 is the session cap. */\n httpStatus?: number;\n}\n\n/** Renderable transport/session state. The terminal values are the server's own. */\nexport type LogTailStatus =\n | \"idle\"\n | \"starting\"\n | \"streaming\"\n | \"connection lost — retrying\"\n | \"completed\"\n | \"failed\"\n | \"stopped\"\n | \"interrupted\";\n\n/**\n * `stopping` is a stop in flight: the source may still flush rows, so the tail\n * keeps reading until the done frame.\n */\nexport function isTerminalSessionState(state: LogSessionState | undefined): boolean {\n return state !== undefined && isSessionTerminal(state);\n}\n\nfunction isTerminalTailStatus(status: LogTailStatus): boolean {\n return isSessionState(status) && isSessionTerminal(status);\n}\n\nexport interface LogTailBuffer {\n /** Rows in arrival order, oldest first — the order this surface tails in. */\n rows: Record<string, unknown>[];\n /**\n * Each entry's presented ClickyRow, index-aligned with `rows` (same length,\n * same order) — `undefined` where the event carried none. Additive: a plain\n * log tail never reads this and `rows` itself is unchanged, so existing\n * `useLogTail` consumers keep working exactly as before.\n */\n clickyRows: (ClickyRow | undefined)[];\n /** Highest sequence applied, or null before the first frame. Never assumed to start at 1. */\n lastSequence: number | null;\n /** Rows evicted by the cap. Kept so the UI can say so instead of quietly shortening history. */\n dropped: number;\n}\n\nexport const emptyLogTailBuffer: LogTailBuffer = {\n rows: [],\n clickyRows: [],\n lastSequence: null,\n dropped: 0,\n};\n\n/**\n * Renders profile parameters as the tail of a session request's query string,\n * `\"\"` when there are none, and each entry already `&`-prefixed so it appends\n * to a query that always begins with `?follow=true`.\n *\n * Keys are sorted so two callers passing the same parameters in a different\n * order produce the same string — the effect that opens the session depends on\n * it, and an unstable spelling would tear down a live tail and start a new one\n * on a render that changed nothing.\n *\n * A null or undefined value is omitted rather than sent as the text \"null\": the\n * server reads every query key as a supplied parameter, so spelling an absent\n * one out states a filter the caller did not ask for.\n */\nexport function encodeTailParams(params: Record<string, unknown> | undefined): string {\n if (!params) return \"\";\n const query = new URLSearchParams();\n for (const key of Object.keys(params).sort()) {\n const value = params[key];\n if (value === undefined || value === null) continue;\n query.append(key, String(value));\n }\n const encoded = query.toString();\n return encoded === \"\" ? \"\" : `&${encoded}`;\n}\n\n/**\n * Applies one frame to the buffer, dropping anything at or below the highest\n * sequence already seen. A watermark is enough — and cheaper than remembering\n * every id — because SSE is ordered within a connection and the only source of\n * duplicates is a reconnect, whose replay starts at Last-Event-ID and so can\n * only re-deliver frames the buffer has already passed.\n *\n * Returns the buffer unchanged when the frame adds nothing, so React can bail\n * out of the render.\n */\nexport function appendTailEvent(\n buffer: LogTailBuffer,\n event: LogTailEvent,\n maxRows: number,\n): LogTailBuffer {\n if (buffer.lastSequence !== null && event.sequence <= buffer.lastSequence) return buffer;\n const incoming = event.rows ?? (event.row ? [event.row] : []);\n if (incoming.length === 0) return { ...buffer, lastSequence: event.sequence };\n\n // A batched `rows` frame has no per-item clickyRow equivalent — only the\n // single-`row` shape trace/follow sessions use ever carries one — so a\n // batch pads with `undefined` to stay index-aligned with `rows`.\n const incomingClickyRows: (ClickyRow | undefined)[] = event.rows\n ? event.rows.map(() => undefined)\n : [event.clickyRow];\n\n const merged = buffer.rows.concat(incoming);\n const mergedClickyRows = buffer.clickyRows.concat(incomingClickyRows);\n const overflow = Math.max(0, merged.length - Math.max(1, maxRows));\n return {\n rows: overflow > 0 ? merged.slice(overflow) : merged,\n clickyRows: overflow > 0 ? mergedClickyRows.slice(overflow) : mergedClickyRows,\n lastSequence: event.sequence,\n dropped: buffer.dropped + overflow,\n };\n}\n\nexport interface UseLogTailOptions {\n /** Query profile to follow, e.g. \"k8s-pod-logs\". */\n profile: string;\n /** Profile parameters, sent as the session request's query string. */\n params?: Record<string, unknown> | undefined;\n /** Base path of the session API, e.g. \"/api/v1\". */\n basePath?: string | undefined;\n /** Start/stop is the caller's: flipping this to false closes the stream; the server reaps the unsubscribed session. */\n following?: boolean | undefined;\n /** Rows kept in memory; older ones are evicted and counted in `droppedRows`. */\n maxRows?: number | undefined;\n /**\n * Called once per applied `event` frame, in sequence order, before the frame\n * reaches the buffer — so a reducer sees every row even after `maxRows` starts\n * evicting. A replayed sequence is not delivered again, and `done` frames are\n * never delivered. The latest callback is always used; changing its identity\n * does not restart the session. A throw is reported as a `stream` error.\n */\n onEvent?: ((event: LogTailEvent) => void) | undefined;\n}\n\nexport interface UseLogTailResult {\n /** Accumulated rows, oldest first. */\n rows: Record<string, unknown>[];\n /** Each row's presented ClickyRow, index-aligned with `rows`; `undefined`\n * where the event carried none (a plain log tail, or a top-session\n * snapshot event). */\n clickyRows: (ClickyRow | undefined)[];\n status: LogTailStatus;\n error: LogTailError | null;\n sessionId: string | null;\n /** True while the caller wants rows and the session has not reached a terminal state. */\n following: boolean;\n /** Rows the cap evicted; non-zero means the tail no longer holds the whole run. */\n droppedRows: number;\n /** Highest sequence applied — the resume point a reconnect would ask for. */\n lastSequence: number | null;\n /** Latest SessionInfo the server sent, for eventCount / startedAt / state. */\n session: LogSessionInfo | null;\n}\n\nconst DEFAULT_BASE = \"/api/v1\";\nconst DEFAULT_MAX_ROWS = 5_000;\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nexport function useLogTail(options: UseLogTailOptions): UseLogTailResult {\n const {\n profile,\n params,\n basePath = DEFAULT_BASE,\n following: wantFollow = false,\n maxRows = DEFAULT_MAX_ROWS,\n onEvent,\n } = options;\n\n const [buffer, setBuffer] = useState<LogTailBuffer>(emptyLogTailBuffer);\n const [status, setStatus] = useState<LogTailStatus>(\"idle\");\n const [error, setError] = useState<LogTailError | null>(null);\n const [session, setSession] = useState<LogSessionInfo | null>(null);\n\n // The cap is read when a frame lands rather than closed over, so changing how\n // much history to keep never tears down a live session to do it.\n const maxRowsRef = useRef(maxRows);\n maxRowsRef.current = maxRows;\n const onEventRef = useRef(onEvent);\n onEventRef.current = onEvent;\n\n // Profile parameters travel in the query string because that is where the\n // session endpoint reads them from: it builds its params by walking\n // r.URL.Query() and never looks at the body, so a posted body is a filter the\n // server silently ignores — the tail would run the profile's defaults while\n // the reader believed they were following what they had filtered to.\n //\n // Serialized here rather than in the effect so it is also the effect's\n // dependency: two renders with equal parameters must not restart the session.\n const paramsQuery = encodeTailParams(params);\n\n useEffect(() => {\n if (!wantFollow) return;\n\n // Each start is a distinct server session replaying from its own first\n // sequence, so the previous run's rows cannot be continued — carrying them\n // over would interleave two unrelated sequence spaces in one list.\n setBuffer(emptyLogTailBuffer);\n setError(null);\n setSession(null);\n\n // Checked before the session exists: a session nobody can subscribe to\n // would hold a server slot until the reaper noticed.\n if (!hasEventSource()) {\n setStatus(\"failed\");\n setError({\n scope: \"start\",\n message: `cannot follow ${profile}: this browser has no EventSource, and session events are served only as SSE`,\n });\n return;\n }\n setStatus(\"starting\");\n\n let cancelled = false;\n const aborter = new AbortController();\n let source: EventSource | undefined;\n let ended = false;\n // onEvent's own watermark, tracked here rather than read from the buffer:\n // the buffer only dedupes inside a state updater, which React may run twice,\n // so a callback there would double-count. Same rule as appendTailEvent.\n let delivered: number | null = null;\n\n const deliver = (event: LogTailEvent) => {\n if (delivered !== null && event.sequence <= delivered) return;\n delivered = event.sequence;\n try {\n onEventRef.current?.(event);\n } catch (cause) {\n setError({ scope: \"stream\", message: `onEvent threw on sequence ${event.sequence}: ${String(cause)}` });\n }\n };\n\n const apply = (event: LogTailEvent) => {\n // An event-level error does not end the session, so it is reported beside\n // the rows rather than in place of them. Set before delivery so a reducer\n // that throws on the same frame is the error left standing.\n if (event.error) setError({ scope: \"stream\", message: event.error });\n deliver(event);\n setBuffer((prev) => appendTailEvent(prev, event, maxRowsRef.current));\n };\n\n // Unlike the task stream this does not skip what it cannot parse: a tail\n // that silently discards frames is indistinguishable from a quiet log\n // source, which is the one thing a reader must never be left guessing at.\n const readFrame = (label: string, e: MessageEvent): unknown => {\n try {\n return JSON.parse(e.data as string);\n } catch (cause) {\n setError({ scope: \"stream\", message: `unparseable ${label} frame: ${String(cause)}` });\n return undefined;\n }\n };\n\n const settle = (info: LogSessionInfo) => {\n setSession(info);\n if (!isTerminalSessionState(info.state)) return;\n ended = true;\n setStatus(info.state as LogTailStatus);\n if (info.state === \"failed\") {\n setError({\n scope: \"session\",\n message: info.error || `session ${info.id} failed without reporting a reason`,\n });\n }\n };\n\n const subscribe = (id: string) => {\n const es = new EventSource(`${basePath}/sessions/${encodeURIComponent(id)}/events`);\n source = es;\n setStatus(\"streaming\");\n es.addEventListener(\"event\", (e) => {\n const frame = readFrame(\"event\", e as MessageEvent);\n if (frame) apply(frame as LogTailEvent);\n });\n es.addEventListener(\"done\", (e) => {\n const info = readFrame(\"done\", e as MessageEvent);\n if (info) settle(info as LogSessionInfo);\n es.close();\n });\n // A dropped connection is not a failure: EventSource reconnects with\n // Last-Event-ID and the server replays. It is reported through `status`\n // only, so a blip never leaves a sticky error the reader has to dismiss —\n // and never after the session ended, where it would overwrite the reason\n // it ended with a reconnect that is not going to happen.\n es.onerror = () => {\n if (!ended) setStatus(\"connection lost — retrying\");\n };\n };\n\n const start = async () => {\n let info: LogSessionInfo;\n try {\n const res = await fetch(\n `${basePath}/profile/${encodeURIComponent(profile)}/sessions?follow=true${paramsQuery}`,\n { method: \"POST\", headers: { Accept: \"application/json\" }, signal: aborter.signal },\n );\n if (cancelled) return;\n if (!res.ok) {\n const detail = (await res.text()).trim() || \"no detail\";\n setStatus(\"failed\");\n setError({\n scope: \"start\",\n httpStatus: res.status,\n message:\n res.status === 409\n ? `the server is already running as many follow sessions as it allows (HTTP 409): ${detail}`\n : `could not start a follow session for ${profile} (HTTP ${res.status}): ${detail}`,\n });\n return;\n }\n info = (await res.json()) as LogSessionInfo;\n } catch (cause) {\n // Our own abort on unmount or a parameter change: nothing is left to report to.\n if (cancelled) return;\n setStatus(\"failed\");\n setError({\n scope: \"start\",\n message: `could not reach ${basePath} to start a follow session for ${profile}: ${String(cause)}`,\n });\n return;\n }\n\n // The caller stopped following while the POST was in flight. If the\n // session was created, nothing ever subscribes, so the server ends it.\n if (cancelled) return;\n setSession(info);\n subscribe(info.id);\n };\n\n void start();\n // Aborting the start and closing the stream is the whole release: a view\n // session without a subscriber ends server-side.\n return () => {\n cancelled = true;\n aborter.abort();\n source?.close();\n };\n }, [profile, paramsQuery, basePath, wantFollow]);\n\n // Stopping returns the surface to idle, but a session that ended on its own\n // keeps its terminal status so the reason it ended stays on screen.\n useEffect(() => {\n if (!wantFollow) setStatus((prev) => (isTerminalTailStatus(prev) ? prev : \"idle\"));\n }, [wantFollow]);\n\n return {\n rows: buffer.rows,\n clickyRows: buffer.clickyRows,\n status,\n error,\n sessionId: session?.id ?? null,\n following: wantFollow && !isTerminalTailStatus(status),\n droppedRows: buffer.dropped,\n lastSequence: buffer.lastSequence,\n session,\n };\n}\n"],"names":["isSessionTerminal","isSessionState","useState","useRef","useEffect"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-log-tail.cjs","sources":["../../src/hooks/use-log-tail.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ClickyRow } from \"../data/Clicky\";\nimport { isSessionState, isSessionTerminal, type SessionState } from \"../rpc/sessionTypes\";\nimport { useEventSourceFactory, type EventSourceLike } from \"./event-source\";\n\n// use-log-tail is the clicky-ui client for a *follow* session: the server opens\n// a live source for a query profile and streams rows out of it until someone\n// closes it. The resource underneath is unforgiving. A follow session holds a\n// websocket to Loki, or a log stream to a kubelet, for as long as it exists,\n// and the server caps how many may run at once (ErrMaxSessions, surfaced as\n// HTTP 409). The server owns that slot's release: a view session ends shortly\n// after its last subscriber disconnects, so every exit path — unmount and a\n// parameter change included — only has to abort the start request and close\n// the EventSource.\n//\n// Events travel over SSE only. The events route serves SSE (or ndjson on\n// request) and resumes through Last-Event-ID; there is no JSON-array polling\n// shape to fall back to, so a browser without EventSource fails loudly.\n//\n// Each SSE frame carries an `id:`, so a reconnecting EventSource sends\n// Last-Event-ID and the server resumes from that sequence — re-delivering\n// frames the client may already hold. Sequences are the server's, not a client\n// index: they do not start at 1 and they do not reset, so the accumulator\n// dedupes against the highest sequence it has applied rather than against a\n// position in an array.\n\n/** The sessions API's state, under the name this hook has always exported. */\nexport type LogSessionState = SessionState;\n\n/** SessionInfo as returned by POST /profile/{name}/sessions and the `done` frame. */\nexport interface LogSessionInfo {\n id: string;\n profile: string;\n kind: string;\n state: LogSessionState;\n params?: Record<string, unknown>;\n error?: string;\n eventCount: number;\n startedAt: string;\n stoppedAt?: string;\n}\n\n/**\n * One `event:` frame. It carries either a single `row` or a batch under `rows`\n * — never both — plus an `error` when the source failed mid-stream without the\n * session itself ending.\n *\n * `clickyRow` is the presented row — `{cells, detail?}`, built by the exact\n * function that fills in `node.rows[n]` of the list's own\n * `application/json+clicky` document — riding next to the raw `row` on a\n * trace/follow session's per-row events. A plain log tail (or a top-session\n * snapshot event) never sets it; only a reader that reconciles rows against a\n * presented table, i.e. OperationCatalog's follow mode, depends on it.\n */\nexport interface LogTailEvent {\n sessionId: string;\n sequence: number;\n time?: string;\n row?: Record<string, unknown>;\n rows?: Record<string, unknown>[];\n /** The presented ClickyRow for `row`, when the profile sends one. */\n clickyRow?: ClickyRow;\n error?: string;\n}\n\n/**\n * Where a failure came from, because the three are not interchangeable to a\n * reader: `start` means the session never existed (409 is the cap), `stream`\n * means the session is alive but something in it went wrong, and `session`\n * means the server ended it.\n */\nexport interface LogTailError {\n scope: \"start\" | \"stream\" | \"session\";\n message: string;\n /** HTTP status when a request produced the failure. 409 is the session cap. */\n httpStatus?: number;\n}\n\n/** Renderable transport/session state. The terminal values are the server's own. */\nexport type LogTailStatus =\n | \"idle\"\n | \"starting\"\n | \"streaming\"\n | \"connection lost — retrying\"\n | \"completed\"\n | \"failed\"\n | \"stopped\"\n | \"interrupted\";\n\n/**\n * `stopping` is a stop in flight: the source may still flush rows, so the tail\n * keeps reading until the done frame.\n */\nexport function isTerminalSessionState(state: LogSessionState | undefined): boolean {\n return state !== undefined && isSessionTerminal(state);\n}\n\nfunction isTerminalTailStatus(status: LogTailStatus): boolean {\n return isSessionState(status) && isSessionTerminal(status);\n}\n\nexport interface LogTailBuffer {\n /** Rows in arrival order, oldest first — the order this surface tails in. */\n rows: Record<string, unknown>[];\n /**\n * Each entry's presented ClickyRow, index-aligned with `rows` (same length,\n * same order) — `undefined` where the event carried none. Additive: a plain\n * log tail never reads this and `rows` itself is unchanged, so existing\n * `useLogTail` consumers keep working exactly as before.\n */\n clickyRows: (ClickyRow | undefined)[];\n /** Highest sequence applied, or null before the first frame. Never assumed to start at 1. */\n lastSequence: number | null;\n /** Rows evicted by the cap. Kept so the UI can say so instead of quietly shortening history. */\n dropped: number;\n}\n\nexport const emptyLogTailBuffer: LogTailBuffer = {\n rows: [],\n clickyRows: [],\n lastSequence: null,\n dropped: 0,\n};\n\n/**\n * Renders profile parameters as the tail of a session request's query string,\n * `\"\"` when there are none, and each entry already `&`-prefixed so it appends\n * to a query that always begins with `?follow=true`.\n *\n * Keys are sorted so two callers passing the same parameters in a different\n * order produce the same string — the effect that opens the session depends on\n * it, and an unstable spelling would tear down a live tail and start a new one\n * on a render that changed nothing.\n *\n * A null or undefined value is omitted rather than sent as the text \"null\": the\n * server reads every query key as a supplied parameter, so spelling an absent\n * one out states a filter the caller did not ask for.\n */\nexport function encodeTailParams(params: Record<string, unknown> | undefined): string {\n if (!params) return \"\";\n const query = new URLSearchParams();\n for (const key of Object.keys(params).sort()) {\n const value = params[key];\n if (value === undefined || value === null) continue;\n query.append(key, String(value));\n }\n const encoded = query.toString();\n return encoded === \"\" ? \"\" : `&${encoded}`;\n}\n\n/**\n * Applies one frame to the buffer, dropping anything at or below the highest\n * sequence already seen. A watermark is enough — and cheaper than remembering\n * every id — because SSE is ordered within a connection and the only source of\n * duplicates is a reconnect, whose replay starts at Last-Event-ID and so can\n * only re-deliver frames the buffer has already passed.\n *\n * Returns the buffer unchanged when the frame adds nothing, so React can bail\n * out of the render.\n */\nexport function appendTailEvent(\n buffer: LogTailBuffer,\n event: LogTailEvent,\n maxRows: number,\n): LogTailBuffer {\n if (buffer.lastSequence !== null && event.sequence <= buffer.lastSequence) return buffer;\n const incoming = event.rows ?? (event.row ? [event.row] : []);\n if (incoming.length === 0) return { ...buffer, lastSequence: event.sequence };\n\n // A batched `rows` frame has no per-item clickyRow equivalent — only the\n // single-`row` shape trace/follow sessions use ever carries one — so a\n // batch pads with `undefined` to stay index-aligned with `rows`.\n const incomingClickyRows: (ClickyRow | undefined)[] = event.rows\n ? event.rows.map(() => undefined)\n : [event.clickyRow];\n\n const merged = buffer.rows.concat(incoming);\n const mergedClickyRows = buffer.clickyRows.concat(incomingClickyRows);\n const overflow = Math.max(0, merged.length - Math.max(1, maxRows));\n return {\n rows: overflow > 0 ? merged.slice(overflow) : merged,\n clickyRows: overflow > 0 ? mergedClickyRows.slice(overflow) : mergedClickyRows,\n lastSequence: event.sequence,\n dropped: buffer.dropped + overflow,\n };\n}\n\nexport interface UseLogTailOptions {\n /** Query profile to follow, e.g. \"k8s-pod-logs\". */\n profile: string;\n /** Profile parameters, sent as the session request's query string. */\n params?: Record<string, unknown> | undefined;\n /** Base path of the session API, e.g. \"/api/v1\". */\n basePath?: string | undefined;\n /** Start/stop is the caller's: flipping this to false closes the stream; the server reaps the unsubscribed session. */\n following?: boolean | undefined;\n /** Rows kept in memory; older ones are evicted and counted in `droppedRows`. */\n maxRows?: number | undefined;\n /**\n * Called once per applied `event` frame, in sequence order, before the frame\n * reaches the buffer — so a reducer sees every row even after `maxRows` starts\n * evicting. A replayed sequence is not delivered again, and `done` frames are\n * never delivered. The latest callback is always used; changing its identity\n * does not restart the session. A throw is reported as a `stream` error.\n */\n onEvent?: ((event: LogTailEvent) => void) | undefined;\n}\n\nexport interface UseLogTailResult {\n /** Accumulated rows, oldest first. */\n rows: Record<string, unknown>[];\n /** Each row's presented ClickyRow, index-aligned with `rows`; `undefined`\n * where the event carried none (a plain log tail, or a top-session\n * snapshot event). */\n clickyRows: (ClickyRow | undefined)[];\n status: LogTailStatus;\n error: LogTailError | null;\n sessionId: string | null;\n /** True while the caller wants rows and the session has not reached a terminal state. */\n following: boolean;\n /** Rows the cap evicted; non-zero means the tail no longer holds the whole run. */\n droppedRows: number;\n /** Highest sequence applied — the resume point a reconnect would ask for. */\n lastSequence: number | null;\n /** Latest SessionInfo the server sent, for eventCount / startedAt / state. */\n session: LogSessionInfo | null;\n}\n\nconst DEFAULT_BASE = \"/api/v1\";\nconst DEFAULT_MAX_ROWS = 5_000;\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nexport function useLogTail(options: UseLogTailOptions): UseLogTailResult {\n const {\n profile,\n params,\n basePath = DEFAULT_BASE,\n following: wantFollow = false,\n maxRows = DEFAULT_MAX_ROWS,\n onEvent,\n } = options;\n\n const [buffer, setBuffer] = useState<LogTailBuffer>(emptyLogTailBuffer);\n const [status, setStatus] = useState<LogTailStatus>(\"idle\");\n const [error, setError] = useState<LogTailError | null>(null);\n const [session, setSession] = useState<LogSessionInfo | null>(null);\n\n // The cap is read when a frame lands rather than closed over, so changing how\n // much history to keep never tears down a live session to do it.\n const maxRowsRef = useRef(maxRows);\n maxRowsRef.current = maxRows;\n const onEventRef = useRef(onEvent);\n onEventRef.current = onEvent;\n\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n // Profile parameters travel in the query string because that is where the\n // session endpoint reads them from: it builds its params by walking\n // r.URL.Query() and never looks at the body, so a posted body is a filter the\n // server silently ignores — the tail would run the profile's defaults while\n // the reader believed they were following what they had filtered to.\n //\n // Serialized here rather than in the effect so it is also the effect's\n // dependency: two renders with equal parameters must not restart the session.\n const paramsQuery = encodeTailParams(params);\n\n useEffect(() => {\n if (!wantFollow) return;\n\n // Each start is a distinct server session replaying from its own first\n // sequence, so the previous run's rows cannot be continued — carrying them\n // over would interleave two unrelated sequence spaces in one list.\n setBuffer(emptyLogTailBuffer);\n setError(null);\n setSession(null);\n\n // Checked before the session exists: a session nobody can subscribe to\n // would hold a server slot until the reaper noticed.\n if (!hasEventSource()) {\n setStatus(\"failed\");\n setError({\n scope: \"start\",\n message: `cannot follow ${profile}: this browser has no EventSource, and session events are served only as SSE`,\n });\n return;\n }\n setStatus(\"starting\");\n\n let cancelled = false;\n const aborter = new AbortController();\n let source: EventSourceLike | undefined;\n let ended = false;\n // onEvent's own watermark, tracked here rather than read from the buffer:\n // the buffer only dedupes inside a state updater, which React may run twice,\n // so a callback there would double-count. Same rule as appendTailEvent.\n let delivered: number | null = null;\n\n const deliver = (event: LogTailEvent) => {\n if (delivered !== null && event.sequence <= delivered) return;\n delivered = event.sequence;\n try {\n onEventRef.current?.(event);\n } catch (cause) {\n setError({ scope: \"stream\", message: `onEvent threw on sequence ${event.sequence}: ${String(cause)}` });\n }\n };\n\n const apply = (event: LogTailEvent) => {\n // An event-level error does not end the session, so it is reported beside\n // the rows rather than in place of them. Set before delivery so a reducer\n // that throws on the same frame is the error left standing.\n if (event.error) setError({ scope: \"stream\", message: event.error });\n deliver(event);\n setBuffer((prev) => appendTailEvent(prev, event, maxRowsRef.current));\n };\n\n // Unlike the task stream this does not skip what it cannot parse: a tail\n // that silently discards frames is indistinguishable from a quiet log\n // source, which is the one thing a reader must never be left guessing at.\n const readFrame = (label: string, e: MessageEvent): unknown => {\n try {\n return JSON.parse(e.data as string);\n } catch (cause) {\n setError({ scope: \"stream\", message: `unparseable ${label} frame: ${String(cause)}` });\n return undefined;\n }\n };\n\n const settle = (info: LogSessionInfo) => {\n setSession(info);\n if (!isTerminalSessionState(info.state)) return;\n ended = true;\n setStatus(info.state as LogTailStatus);\n if (info.state === \"failed\") {\n setError({\n scope: \"session\",\n message: info.error || `session ${info.id} failed without reporting a reason`,\n });\n }\n };\n\n const subscribe = (id: string) => {\n const es = eventSourceFactoryRef.current(`${basePath}/sessions/${encodeURIComponent(id)}/events`);\n source = es;\n setStatus(\"streaming\");\n es.addEventListener(\"event\", (e) => {\n const frame = readFrame(\"event\", e as MessageEvent);\n if (frame) apply(frame as LogTailEvent);\n });\n es.addEventListener(\"done\", (e) => {\n const info = readFrame(\"done\", e as MessageEvent);\n if (info) settle(info as LogSessionInfo);\n es.close();\n });\n // A dropped connection is not a failure: EventSource reconnects with\n // Last-Event-ID and the server replays. It is reported through `status`\n // only, so a blip never leaves a sticky error the reader has to dismiss —\n // and never after the session ended, where it would overwrite the reason\n // it ended with a reconnect that is not going to happen.\n es.onerror = () => {\n if (!ended) setStatus(\"connection lost — retrying\");\n };\n };\n\n const start = async () => {\n let info: LogSessionInfo;\n try {\n const res = await fetch(\n `${basePath}/profile/${encodeURIComponent(profile)}/sessions?follow=true${paramsQuery}`,\n { method: \"POST\", headers: { Accept: \"application/json\" }, signal: aborter.signal },\n );\n if (cancelled) return;\n if (!res.ok) {\n const detail = (await res.text()).trim() || \"no detail\";\n setStatus(\"failed\");\n setError({\n scope: \"start\",\n httpStatus: res.status,\n message:\n res.status === 409\n ? `the server is already running as many follow sessions as it allows (HTTP 409): ${detail}`\n : `could not start a follow session for ${profile} (HTTP ${res.status}): ${detail}`,\n });\n return;\n }\n info = (await res.json()) as LogSessionInfo;\n } catch (cause) {\n // Our own abort on unmount or a parameter change: nothing is left to report to.\n if (cancelled) return;\n setStatus(\"failed\");\n setError({\n scope: \"start\",\n message: `could not reach ${basePath} to start a follow session for ${profile}: ${String(cause)}`,\n });\n return;\n }\n\n // The caller stopped following while the POST was in flight. If the\n // session was created, nothing ever subscribes, so the server ends it.\n if (cancelled) return;\n setSession(info);\n subscribe(info.id);\n };\n\n void start();\n // Aborting the start and closing the stream is the whole release: a view\n // session without a subscriber ends server-side.\n return () => {\n cancelled = true;\n aborter.abort();\n source?.close();\n };\n }, [profile, paramsQuery, basePath, wantFollow]);\n\n // Stopping returns the surface to idle, but a session that ended on its own\n // keeps its terminal status so the reason it ended stays on screen.\n useEffect(() => {\n if (!wantFollow) setStatus((prev) => (isTerminalTailStatus(prev) ? prev : \"idle\"));\n }, [wantFollow]);\n\n return {\n rows: buffer.rows,\n clickyRows: buffer.clickyRows,\n status,\n error,\n sessionId: session?.id ?? null,\n following: wantFollow && !isTerminalTailStatus(status),\n droppedRows: buffer.dropped,\n lastSequence: buffer.lastSequence,\n session,\n };\n}\n"],"names":["isSessionTerminal","isSessionState","useState","useRef","useEventSourceFactory","useEffect"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-log-tail.d.ts","sourceRoot":"","sources":["../../src/hooks/use-log-tail.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAqC,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"use-log-tail.d.ts","sourceRoot":"","sources":["../../src/hooks/use-log-tail.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAqC,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAwB3F,8EAA8E;AAC9E,MAAM,MAAM,eAAe,GAAG,YAAY,CAAC;AAE3C,qFAAqF;AACrF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,eAAe,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACjC,qEAAqE;IACrE,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,oFAAoF;AACpF,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,UAAU,GACV,WAAW,GACX,4BAA4B,GAC5B,WAAW,GACX,QAAQ,GACR,SAAS,GACT,aAAa,CAAC;AAElB;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,eAAe,GAAG,SAAS,GAAG,OAAO,CAElF;AAMD,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC;;;;;OAKG;IACH,UAAU,EAAE,CAAC,SAAS,GAAG,SAAS,CAAC,EAAE,CAAC;IACtC,6FAA6F;IAC7F,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gGAAgG;IAChG,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,kBAAkB,EAAE,aAKhC,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,MAAM,CAUpF;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,MAAM,GACd,aAAa,CAqBf;AAED,MAAM,WAAW,iBAAiB;IAChC,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC7C,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,uHAAuH;IACvH,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CACvD;AAED,MAAM,WAAW,gBAAgB;IAC/B,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC;;2BAEuB;IACvB,UAAU,EAAE,CAAC,SAAS,GAAG,SAAS,CAAC,EAAE,CAAC;IACtC,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yFAAyF;IACzF,SAAS,EAAE,OAAO,CAAC;IACnB,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,8EAA8E;IAC9E,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;CAChC;AASD,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAyMvE"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useRef, useEffect } from "react";
|
|
2
2
|
import { isSessionTerminal, isSessionState } from "../rpc/sessionTypes.js";
|
|
3
|
+
import { useEventSourceFactory } from "./event-source.js";
|
|
3
4
|
function isTerminalSessionState(state) {
|
|
4
5
|
return state !== void 0 && isSessionTerminal(state);
|
|
5
6
|
}
|
|
@@ -60,6 +61,9 @@ function useLogTail(options) {
|
|
|
60
61
|
maxRowsRef.current = maxRows;
|
|
61
62
|
const onEventRef = useRef(onEvent);
|
|
62
63
|
onEventRef.current = onEvent;
|
|
64
|
+
const eventSourceFactory = useEventSourceFactory();
|
|
65
|
+
const eventSourceFactoryRef = useRef(eventSourceFactory);
|
|
66
|
+
eventSourceFactoryRef.current = eventSourceFactory;
|
|
63
67
|
const paramsQuery = encodeTailParams(params);
|
|
64
68
|
useEffect(() => {
|
|
65
69
|
if (!wantFollow) return;
|
|
@@ -116,7 +120,7 @@ function useLogTail(options) {
|
|
|
116
120
|
}
|
|
117
121
|
};
|
|
118
122
|
const subscribe = (id) => {
|
|
119
|
-
const es =
|
|
123
|
+
const es = eventSourceFactoryRef.current(`${basePath}/sessions/${encodeURIComponent(id)}/events`);
|
|
120
124
|
source = es;
|
|
121
125
|
setStatus("streaming");
|
|
122
126
|
es.addEventListener("event", (e) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-log-tail.js","sources":["../../src/hooks/use-log-tail.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ClickyRow } from \"../data/Clicky\";\nimport { isSessionState, isSessionTerminal, type SessionState } from \"../rpc/sessionTypes\";\n\n// use-log-tail is the clicky-ui client for a *follow* session: the server opens\n// a live source for a query profile and streams rows out of it until someone\n// closes it. The resource underneath is unforgiving. A follow session holds a\n// websocket to Loki, or a log stream to a kubelet, for as long as it exists,\n// and the server caps how many may run at once (ErrMaxSessions, surfaced as\n// HTTP 409). The server owns that slot's release: a view session ends shortly\n// after its last subscriber disconnects, so every exit path — unmount and a\n// parameter change included — only has to abort the start request and close\n// the EventSource.\n//\n// Events travel over SSE only. The events route serves SSE (or ndjson on\n// request) and resumes through Last-Event-ID; there is no JSON-array polling\n// shape to fall back to, so a browser without EventSource fails loudly.\n//\n// Each SSE frame carries an `id:`, so a reconnecting EventSource sends\n// Last-Event-ID and the server resumes from that sequence — re-delivering\n// frames the client may already hold. Sequences are the server's, not a client\n// index: they do not start at 1 and they do not reset, so the accumulator\n// dedupes against the highest sequence it has applied rather than against a\n// position in an array.\n\n/** The sessions API's state, under the name this hook has always exported. */\nexport type LogSessionState = SessionState;\n\n/** SessionInfo as returned by POST /profile/{name}/sessions and the `done` frame. */\nexport interface LogSessionInfo {\n id: string;\n profile: string;\n kind: string;\n state: LogSessionState;\n params?: Record<string, unknown>;\n error?: string;\n eventCount: number;\n startedAt: string;\n stoppedAt?: string;\n}\n\n/**\n * One `event:` frame. It carries either a single `row` or a batch under `rows`\n * — never both — plus an `error` when the source failed mid-stream without the\n * session itself ending.\n *\n * `clickyRow` is the presented row — `{cells, detail?}`, built by the exact\n * function that fills in `node.rows[n]` of the list's own\n * `application/json+clicky` document — riding next to the raw `row` on a\n * trace/follow session's per-row events. A plain log tail (or a top-session\n * snapshot event) never sets it; only a reader that reconciles rows against a\n * presented table, i.e. OperationCatalog's follow mode, depends on it.\n */\nexport interface LogTailEvent {\n sessionId: string;\n sequence: number;\n time?: string;\n row?: Record<string, unknown>;\n rows?: Record<string, unknown>[];\n /** The presented ClickyRow for `row`, when the profile sends one. */\n clickyRow?: ClickyRow;\n error?: string;\n}\n\n/**\n * Where a failure came from, because the three are not interchangeable to a\n * reader: `start` means the session never existed (409 is the cap), `stream`\n * means the session is alive but something in it went wrong, and `session`\n * means the server ended it.\n */\nexport interface LogTailError {\n scope: \"start\" | \"stream\" | \"session\";\n message: string;\n /** HTTP status when a request produced the failure. 409 is the session cap. */\n httpStatus?: number;\n}\n\n/** Renderable transport/session state. The terminal values are the server's own. */\nexport type LogTailStatus =\n | \"idle\"\n | \"starting\"\n | \"streaming\"\n | \"connection lost — retrying\"\n | \"completed\"\n | \"failed\"\n | \"stopped\"\n | \"interrupted\";\n\n/**\n * `stopping` is a stop in flight: the source may still flush rows, so the tail\n * keeps reading until the done frame.\n */\nexport function isTerminalSessionState(state: LogSessionState | undefined): boolean {\n return state !== undefined && isSessionTerminal(state);\n}\n\nfunction isTerminalTailStatus(status: LogTailStatus): boolean {\n return isSessionState(status) && isSessionTerminal(status);\n}\n\nexport interface LogTailBuffer {\n /** Rows in arrival order, oldest first — the order this surface tails in. */\n rows: Record<string, unknown>[];\n /**\n * Each entry's presented ClickyRow, index-aligned with `rows` (same length,\n * same order) — `undefined` where the event carried none. Additive: a plain\n * log tail never reads this and `rows` itself is unchanged, so existing\n * `useLogTail` consumers keep working exactly as before.\n */\n clickyRows: (ClickyRow | undefined)[];\n /** Highest sequence applied, or null before the first frame. Never assumed to start at 1. */\n lastSequence: number | null;\n /** Rows evicted by the cap. Kept so the UI can say so instead of quietly shortening history. */\n dropped: number;\n}\n\nexport const emptyLogTailBuffer: LogTailBuffer = {\n rows: [],\n clickyRows: [],\n lastSequence: null,\n dropped: 0,\n};\n\n/**\n * Renders profile parameters as the tail of a session request's query string,\n * `\"\"` when there are none, and each entry already `&`-prefixed so it appends\n * to a query that always begins with `?follow=true`.\n *\n * Keys are sorted so two callers passing the same parameters in a different\n * order produce the same string — the effect that opens the session depends on\n * it, and an unstable spelling would tear down a live tail and start a new one\n * on a render that changed nothing.\n *\n * A null or undefined value is omitted rather than sent as the text \"null\": the\n * server reads every query key as a supplied parameter, so spelling an absent\n * one out states a filter the caller did not ask for.\n */\nexport function encodeTailParams(params: Record<string, unknown> | undefined): string {\n if (!params) return \"\";\n const query = new URLSearchParams();\n for (const key of Object.keys(params).sort()) {\n const value = params[key];\n if (value === undefined || value === null) continue;\n query.append(key, String(value));\n }\n const encoded = query.toString();\n return encoded === \"\" ? \"\" : `&${encoded}`;\n}\n\n/**\n * Applies one frame to the buffer, dropping anything at or below the highest\n * sequence already seen. A watermark is enough — and cheaper than remembering\n * every id — because SSE is ordered within a connection and the only source of\n * duplicates is a reconnect, whose replay starts at Last-Event-ID and so can\n * only re-deliver frames the buffer has already passed.\n *\n * Returns the buffer unchanged when the frame adds nothing, so React can bail\n * out of the render.\n */\nexport function appendTailEvent(\n buffer: LogTailBuffer,\n event: LogTailEvent,\n maxRows: number,\n): LogTailBuffer {\n if (buffer.lastSequence !== null && event.sequence <= buffer.lastSequence) return buffer;\n const incoming = event.rows ?? (event.row ? [event.row] : []);\n if (incoming.length === 0) return { ...buffer, lastSequence: event.sequence };\n\n // A batched `rows` frame has no per-item clickyRow equivalent — only the\n // single-`row` shape trace/follow sessions use ever carries one — so a\n // batch pads with `undefined` to stay index-aligned with `rows`.\n const incomingClickyRows: (ClickyRow | undefined)[] = event.rows\n ? event.rows.map(() => undefined)\n : [event.clickyRow];\n\n const merged = buffer.rows.concat(incoming);\n const mergedClickyRows = buffer.clickyRows.concat(incomingClickyRows);\n const overflow = Math.max(0, merged.length - Math.max(1, maxRows));\n return {\n rows: overflow > 0 ? merged.slice(overflow) : merged,\n clickyRows: overflow > 0 ? mergedClickyRows.slice(overflow) : mergedClickyRows,\n lastSequence: event.sequence,\n dropped: buffer.dropped + overflow,\n };\n}\n\nexport interface UseLogTailOptions {\n /** Query profile to follow, e.g. \"k8s-pod-logs\". */\n profile: string;\n /** Profile parameters, sent as the session request's query string. */\n params?: Record<string, unknown> | undefined;\n /** Base path of the session API, e.g. \"/api/v1\". */\n basePath?: string | undefined;\n /** Start/stop is the caller's: flipping this to false closes the stream; the server reaps the unsubscribed session. */\n following?: boolean | undefined;\n /** Rows kept in memory; older ones are evicted and counted in `droppedRows`. */\n maxRows?: number | undefined;\n /**\n * Called once per applied `event` frame, in sequence order, before the frame\n * reaches the buffer — so a reducer sees every row even after `maxRows` starts\n * evicting. A replayed sequence is not delivered again, and `done` frames are\n * never delivered. The latest callback is always used; changing its identity\n * does not restart the session. A throw is reported as a `stream` error.\n */\n onEvent?: ((event: LogTailEvent) => void) | undefined;\n}\n\nexport interface UseLogTailResult {\n /** Accumulated rows, oldest first. */\n rows: Record<string, unknown>[];\n /** Each row's presented ClickyRow, index-aligned with `rows`; `undefined`\n * where the event carried none (a plain log tail, or a top-session\n * snapshot event). */\n clickyRows: (ClickyRow | undefined)[];\n status: LogTailStatus;\n error: LogTailError | null;\n sessionId: string | null;\n /** True while the caller wants rows and the session has not reached a terminal state. */\n following: boolean;\n /** Rows the cap evicted; non-zero means the tail no longer holds the whole run. */\n droppedRows: number;\n /** Highest sequence applied — the resume point a reconnect would ask for. */\n lastSequence: number | null;\n /** Latest SessionInfo the server sent, for eventCount / startedAt / state. */\n session: LogSessionInfo | null;\n}\n\nconst DEFAULT_BASE = \"/api/v1\";\nconst DEFAULT_MAX_ROWS = 5_000;\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nexport function useLogTail(options: UseLogTailOptions): UseLogTailResult {\n const {\n profile,\n params,\n basePath = DEFAULT_BASE,\n following: wantFollow = false,\n maxRows = DEFAULT_MAX_ROWS,\n onEvent,\n } = options;\n\n const [buffer, setBuffer] = useState<LogTailBuffer>(emptyLogTailBuffer);\n const [status, setStatus] = useState<LogTailStatus>(\"idle\");\n const [error, setError] = useState<LogTailError | null>(null);\n const [session, setSession] = useState<LogSessionInfo | null>(null);\n\n // The cap is read when a frame lands rather than closed over, so changing how\n // much history to keep never tears down a live session to do it.\n const maxRowsRef = useRef(maxRows);\n maxRowsRef.current = maxRows;\n const onEventRef = useRef(onEvent);\n onEventRef.current = onEvent;\n\n // Profile parameters travel in the query string because that is where the\n // session endpoint reads them from: it builds its params by walking\n // r.URL.Query() and never looks at the body, so a posted body is a filter the\n // server silently ignores — the tail would run the profile's defaults while\n // the reader believed they were following what they had filtered to.\n //\n // Serialized here rather than in the effect so it is also the effect's\n // dependency: two renders with equal parameters must not restart the session.\n const paramsQuery = encodeTailParams(params);\n\n useEffect(() => {\n if (!wantFollow) return;\n\n // Each start is a distinct server session replaying from its own first\n // sequence, so the previous run's rows cannot be continued — carrying them\n // over would interleave two unrelated sequence spaces in one list.\n setBuffer(emptyLogTailBuffer);\n setError(null);\n setSession(null);\n\n // Checked before the session exists: a session nobody can subscribe to\n // would hold a server slot until the reaper noticed.\n if (!hasEventSource()) {\n setStatus(\"failed\");\n setError({\n scope: \"start\",\n message: `cannot follow ${profile}: this browser has no EventSource, and session events are served only as SSE`,\n });\n return;\n }\n setStatus(\"starting\");\n\n let cancelled = false;\n const aborter = new AbortController();\n let source: EventSource | undefined;\n let ended = false;\n // onEvent's own watermark, tracked here rather than read from the buffer:\n // the buffer only dedupes inside a state updater, which React may run twice,\n // so a callback there would double-count. Same rule as appendTailEvent.\n let delivered: number | null = null;\n\n const deliver = (event: LogTailEvent) => {\n if (delivered !== null && event.sequence <= delivered) return;\n delivered = event.sequence;\n try {\n onEventRef.current?.(event);\n } catch (cause) {\n setError({ scope: \"stream\", message: `onEvent threw on sequence ${event.sequence}: ${String(cause)}` });\n }\n };\n\n const apply = (event: LogTailEvent) => {\n // An event-level error does not end the session, so it is reported beside\n // the rows rather than in place of them. Set before delivery so a reducer\n // that throws on the same frame is the error left standing.\n if (event.error) setError({ scope: \"stream\", message: event.error });\n deliver(event);\n setBuffer((prev) => appendTailEvent(prev, event, maxRowsRef.current));\n };\n\n // Unlike the task stream this does not skip what it cannot parse: a tail\n // that silently discards frames is indistinguishable from a quiet log\n // source, which is the one thing a reader must never be left guessing at.\n const readFrame = (label: string, e: MessageEvent): unknown => {\n try {\n return JSON.parse(e.data as string);\n } catch (cause) {\n setError({ scope: \"stream\", message: `unparseable ${label} frame: ${String(cause)}` });\n return undefined;\n }\n };\n\n const settle = (info: LogSessionInfo) => {\n setSession(info);\n if (!isTerminalSessionState(info.state)) return;\n ended = true;\n setStatus(info.state as LogTailStatus);\n if (info.state === \"failed\") {\n setError({\n scope: \"session\",\n message: info.error || `session ${info.id} failed without reporting a reason`,\n });\n }\n };\n\n const subscribe = (id: string) => {\n const es = new EventSource(`${basePath}/sessions/${encodeURIComponent(id)}/events`);\n source = es;\n setStatus(\"streaming\");\n es.addEventListener(\"event\", (e) => {\n const frame = readFrame(\"event\", e as MessageEvent);\n if (frame) apply(frame as LogTailEvent);\n });\n es.addEventListener(\"done\", (e) => {\n const info = readFrame(\"done\", e as MessageEvent);\n if (info) settle(info as LogSessionInfo);\n es.close();\n });\n // A dropped connection is not a failure: EventSource reconnects with\n // Last-Event-ID and the server replays. It is reported through `status`\n // only, so a blip never leaves a sticky error the reader has to dismiss —\n // and never after the session ended, where it would overwrite the reason\n // it ended with a reconnect that is not going to happen.\n es.onerror = () => {\n if (!ended) setStatus(\"connection lost — retrying\");\n };\n };\n\n const start = async () => {\n let info: LogSessionInfo;\n try {\n const res = await fetch(\n `${basePath}/profile/${encodeURIComponent(profile)}/sessions?follow=true${paramsQuery}`,\n { method: \"POST\", headers: { Accept: \"application/json\" }, signal: aborter.signal },\n );\n if (cancelled) return;\n if (!res.ok) {\n const detail = (await res.text()).trim() || \"no detail\";\n setStatus(\"failed\");\n setError({\n scope: \"start\",\n httpStatus: res.status,\n message:\n res.status === 409\n ? `the server is already running as many follow sessions as it allows (HTTP 409): ${detail}`\n : `could not start a follow session for ${profile} (HTTP ${res.status}): ${detail}`,\n });\n return;\n }\n info = (await res.json()) as LogSessionInfo;\n } catch (cause) {\n // Our own abort on unmount or a parameter change: nothing is left to report to.\n if (cancelled) return;\n setStatus(\"failed\");\n setError({\n scope: \"start\",\n message: `could not reach ${basePath} to start a follow session for ${profile}: ${String(cause)}`,\n });\n return;\n }\n\n // The caller stopped following while the POST was in flight. If the\n // session was created, nothing ever subscribes, so the server ends it.\n if (cancelled) return;\n setSession(info);\n subscribe(info.id);\n };\n\n void start();\n // Aborting the start and closing the stream is the whole release: a view\n // session without a subscriber ends server-side.\n return () => {\n cancelled = true;\n aborter.abort();\n source?.close();\n };\n }, [profile, paramsQuery, basePath, wantFollow]);\n\n // Stopping returns the surface to idle, but a session that ended on its own\n // keeps its terminal status so the reason it ended stays on screen.\n useEffect(() => {\n if (!wantFollow) setStatus((prev) => (isTerminalTailStatus(prev) ? prev : \"idle\"));\n }, [wantFollow]);\n\n return {\n rows: buffer.rows,\n clickyRows: buffer.clickyRows,\n status,\n error,\n sessionId: session?.id ?? null,\n following: wantFollow && !isTerminalTailStatus(status),\n droppedRows: buffer.dropped,\n lastSequence: buffer.lastSequence,\n session,\n };\n}\n"],"names":[],"mappings":";;AA4FO,SAAS,uBAAuB,OAA6C;AAClF,SAAO,UAAU,UAAa,kBAAkB,KAAK;AACvD;AAEA,SAAS,qBAAqB,QAAgC;AAC5D,SAAO,eAAe,MAAM,KAAK,kBAAkB,MAAM;AAC3D;AAkBO,MAAM,qBAAoC;AAAA,EAC/C,MAAM,CAAA;AAAA,EACN,YAAY,CAAA;AAAA,EACZ,cAAc;AAAA,EACd,SAAS;AACX;AAgBO,SAAS,iBAAiB,QAAqD;AACpF,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,QAAQ,IAAI,gBAAA;AAClB,aAAW,OAAO,OAAO,KAAK,MAAM,EAAE,QAAQ;AAC5C,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,UAAU,UAAa,UAAU,KAAM;AAC3C,UAAM,OAAO,KAAK,OAAO,KAAK,CAAC;AAAA,EACjC;AACA,QAAM,UAAU,MAAM,SAAA;AACtB,SAAO,YAAY,KAAK,KAAK,IAAI,OAAO;AAC1C;AAYO,SAAS,gBACd,QACA,OACA,SACe;AACf,MAAI,OAAO,iBAAiB,QAAQ,MAAM,YAAY,OAAO,aAAc,QAAO;AAClF,QAAM,WAAW,MAAM,SAAS,MAAM,MAAM,CAAC,MAAM,GAAG,IAAI;AAC1D,MAAI,SAAS,WAAW,EAAG,QAAO,EAAE,GAAG,QAAQ,cAAc,MAAM,SAAA;AAKnE,QAAM,qBAAgD,MAAM,OACxD,MAAM,KAAK,IAAI,MAAM,MAAS,IAC9B,CAAC,MAAM,SAAS;AAEpB,QAAM,SAAS,OAAO,KAAK,OAAO,QAAQ;AAC1C,QAAM,mBAAmB,OAAO,WAAW,OAAO,kBAAkB;AACpE,QAAM,WAAW,KAAK,IAAI,GAAG,OAAO,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC;AACjE,SAAO;AAAA,IACL,MAAM,WAAW,IAAI,OAAO,MAAM,QAAQ,IAAI;AAAA,IAC9C,YAAY,WAAW,IAAI,iBAAiB,MAAM,QAAQ,IAAI;AAAA,IAC9D,cAAc,MAAM;AAAA,IACpB,SAAS,OAAO,UAAU;AAAA,EAAA;AAE9B;AA2CA,MAAM,eAAe;AACrB,MAAM,mBAAmB;AAEzB,SAAS,iBAA0B;AACjC,SAAO,OAAO,eAAe,eAAe,OAAO,WAAW,gBAAgB;AAChF;AAEO,SAAS,WAAW,SAA8C;AACvE,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,WAAW,aAAa;AAAA,IACxB,UAAU;AAAA,IACV;AAAA,EAAA,IACE;AAEJ,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAwB,kBAAkB;AACtE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAwB,MAAM;AAC1D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA8B,IAAI;AAC5D,QAAM,CAAC,SAAS,UAAU,IAAI,SAAgC,IAAI;AAIlE,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AACrB,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AAUrB,QAAM,cAAc,iBAAiB,MAAM;AAE3C,YAAU,MAAM;AACd,QAAI,CAAC,WAAY;AAKjB,cAAU,kBAAkB;AAC5B,aAAS,IAAI;AACb,eAAW,IAAI;AAIf,QAAI,CAAC,kBAAkB;AACrB,gBAAU,QAAQ;AAClB,eAAS;AAAA,QACP,OAAO;AAAA,QACP,SAAS,iBAAiB,OAAO;AAAA,MAAA,CAClC;AACD;AAAA,IACF;AACA,cAAU,UAAU;AAEpB,QAAI,YAAY;AAChB,UAAM,UAAU,IAAI,gBAAA;AACpB,QAAI;AACJ,QAAI,QAAQ;AAIZ,QAAI,YAA2B;AAE/B,UAAM,UAAU,CAAC,UAAwB;;AACvC,UAAI,cAAc,QAAQ,MAAM,YAAY,UAAW;AACvD,kBAAY,MAAM;AAClB,UAAI;AACF,yBAAW,YAAX,oCAAqB;AAAA,MACvB,SAAS,OAAO;AACd,iBAAS,EAAE,OAAO,UAAU,SAAS,6BAA6B,MAAM,QAAQ,KAAK,OAAO,KAAK,CAAC,GAAA,CAAI;AAAA,MACxG;AAAA,IACF;AAEA,UAAM,QAAQ,CAAC,UAAwB;AAIrC,UAAI,MAAM,MAAO,UAAS,EAAE,OAAO,UAAU,SAAS,MAAM,OAAO;AACnE,cAAQ,KAAK;AACb,gBAAU,CAAC,SAAS,gBAAgB,MAAM,OAAO,WAAW,OAAO,CAAC;AAAA,IACtE;AAKA,UAAM,YAAY,CAAC,OAAe,MAA6B;AAC7D,UAAI;AACF,eAAO,KAAK,MAAM,EAAE,IAAc;AAAA,MACpC,SAAS,OAAO;AACd,iBAAS,EAAE,OAAO,UAAU,SAAS,eAAe,KAAK,WAAW,OAAO,KAAK,CAAC,GAAA,CAAI;AACrF,eAAO;AAAA,MACT;AAAA,IACF;AAEA,UAAM,SAAS,CAAC,SAAyB;AACvC,iBAAW,IAAI;AACf,UAAI,CAAC,uBAAuB,KAAK,KAAK,EAAG;AACzC,cAAQ;AACR,gBAAU,KAAK,KAAsB;AACrC,UAAI,KAAK,UAAU,UAAU;AAC3B,iBAAS;AAAA,UACP,OAAO;AAAA,UACP,SAAS,KAAK,SAAS,WAAW,KAAK,EAAE;AAAA,QAAA,CAC1C;AAAA,MACH;AAAA,IACF;AAEA,UAAM,YAAY,CAAC,OAAe;AAChC,YAAM,KAAK,IAAI,YAAY,GAAG,QAAQ,aAAa,mBAAmB,EAAE,CAAC,SAAS;AAClF,eAAS;AACT,gBAAU,WAAW;AACrB,SAAG,iBAAiB,SAAS,CAAC,MAAM;AAClC,cAAM,QAAQ,UAAU,SAAS,CAAiB;AAClD,YAAI,aAAa,KAAqB;AAAA,MACxC,CAAC;AACD,SAAG,iBAAiB,QAAQ,CAAC,MAAM;AACjC,cAAM,OAAO,UAAU,QAAQ,CAAiB;AAChD,YAAI,aAAa,IAAsB;AACvC,WAAG,MAAA;AAAA,MACL,CAAC;AAMD,SAAG,UAAU,MAAM;AACjB,YAAI,CAAC,MAAO,WAAU,4BAA4B;AAAA,MACpD;AAAA,IACF;AAEA,UAAM,QAAQ,YAAY;AACxB,UAAI;AACJ,UAAI;AACF,cAAM,MAAM,MAAM;AAAA,UAChB,GAAG,QAAQ,YAAY,mBAAmB,OAAO,CAAC,wBAAwB,WAAW;AAAA,UACrF,EAAE,QAAQ,QAAQ,SAAS,EAAE,QAAQ,mBAAA,GAAsB,QAAQ,QAAQ,OAAA;AAAA,QAAO;AAEpF,YAAI,UAAW;AACf,YAAI,CAAC,IAAI,IAAI;AACX,gBAAM,UAAU,MAAM,IAAI,KAAA,GAAQ,UAAU;AAC5C,oBAAU,QAAQ;AAClB,mBAAS;AAAA,YACP,OAAO;AAAA,YACP,YAAY,IAAI;AAAA,YAChB,SACE,IAAI,WAAW,MACX,kFAAkF,MAAM,KACxF,wCAAwC,OAAO,UAAU,IAAI,MAAM,MAAM,MAAM;AAAA,UAAA,CACtF;AACD;AAAA,QACF;AACA,eAAQ,MAAM,IAAI,KAAA;AAAA,MACpB,SAAS,OAAO;AAEd,YAAI,UAAW;AACf,kBAAU,QAAQ;AAClB,iBAAS;AAAA,UACP,OAAO;AAAA,UACP,SAAS,mBAAmB,QAAQ,kCAAkC,OAAO,KAAK,OAAO,KAAK,CAAC;AAAA,QAAA,CAChG;AACD;AAAA,MACF;AAIA,UAAI,UAAW;AACf,iBAAW,IAAI;AACf,gBAAU,KAAK,EAAE;AAAA,IACnB;AAEA,SAAK,MAAA;AAGL,WAAO,MAAM;AACX,kBAAY;AACZ,cAAQ,MAAA;AACR,uCAAQ;AAAA,IACV;AAAA,EACF,GAAG,CAAC,SAAS,aAAa,UAAU,UAAU,CAAC;AAI/C,YAAU,MAAM;AACd,QAAI,CAAC,WAAY,WAAU,CAAC,SAAU,qBAAqB,IAAI,IAAI,OAAO,MAAO;AAAA,EACnF,GAAG,CAAC,UAAU,CAAC;AAEf,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,YAAY,OAAO;AAAA,IACnB;AAAA,IACA;AAAA,IACA,YAAW,mCAAS,OAAM;AAAA,IAC1B,WAAW,cAAc,CAAC,qBAAqB,MAAM;AAAA,IACrD,aAAa,OAAO;AAAA,IACpB,cAAc,OAAO;AAAA,IACrB;AAAA,EAAA;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"use-log-tail.js","sources":["../../src/hooks/use-log-tail.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ClickyRow } from \"../data/Clicky\";\nimport { isSessionState, isSessionTerminal, type SessionState } from \"../rpc/sessionTypes\";\nimport { useEventSourceFactory, type EventSourceLike } from \"./event-source\";\n\n// use-log-tail is the clicky-ui client for a *follow* session: the server opens\n// a live source for a query profile and streams rows out of it until someone\n// closes it. The resource underneath is unforgiving. A follow session holds a\n// websocket to Loki, or a log stream to a kubelet, for as long as it exists,\n// and the server caps how many may run at once (ErrMaxSessions, surfaced as\n// HTTP 409). The server owns that slot's release: a view session ends shortly\n// after its last subscriber disconnects, so every exit path — unmount and a\n// parameter change included — only has to abort the start request and close\n// the EventSource.\n//\n// Events travel over SSE only. The events route serves SSE (or ndjson on\n// request) and resumes through Last-Event-ID; there is no JSON-array polling\n// shape to fall back to, so a browser without EventSource fails loudly.\n//\n// Each SSE frame carries an `id:`, so a reconnecting EventSource sends\n// Last-Event-ID and the server resumes from that sequence — re-delivering\n// frames the client may already hold. Sequences are the server's, not a client\n// index: they do not start at 1 and they do not reset, so the accumulator\n// dedupes against the highest sequence it has applied rather than against a\n// position in an array.\n\n/** The sessions API's state, under the name this hook has always exported. */\nexport type LogSessionState = SessionState;\n\n/** SessionInfo as returned by POST /profile/{name}/sessions and the `done` frame. */\nexport interface LogSessionInfo {\n id: string;\n profile: string;\n kind: string;\n state: LogSessionState;\n params?: Record<string, unknown>;\n error?: string;\n eventCount: number;\n startedAt: string;\n stoppedAt?: string;\n}\n\n/**\n * One `event:` frame. It carries either a single `row` or a batch under `rows`\n * — never both — plus an `error` when the source failed mid-stream without the\n * session itself ending.\n *\n * `clickyRow` is the presented row — `{cells, detail?}`, built by the exact\n * function that fills in `node.rows[n]` of the list's own\n * `application/json+clicky` document — riding next to the raw `row` on a\n * trace/follow session's per-row events. A plain log tail (or a top-session\n * snapshot event) never sets it; only a reader that reconciles rows against a\n * presented table, i.e. OperationCatalog's follow mode, depends on it.\n */\nexport interface LogTailEvent {\n sessionId: string;\n sequence: number;\n time?: string;\n row?: Record<string, unknown>;\n rows?: Record<string, unknown>[];\n /** The presented ClickyRow for `row`, when the profile sends one. */\n clickyRow?: ClickyRow;\n error?: string;\n}\n\n/**\n * Where a failure came from, because the three are not interchangeable to a\n * reader: `start` means the session never existed (409 is the cap), `stream`\n * means the session is alive but something in it went wrong, and `session`\n * means the server ended it.\n */\nexport interface LogTailError {\n scope: \"start\" | \"stream\" | \"session\";\n message: string;\n /** HTTP status when a request produced the failure. 409 is the session cap. */\n httpStatus?: number;\n}\n\n/** Renderable transport/session state. The terminal values are the server's own. */\nexport type LogTailStatus =\n | \"idle\"\n | \"starting\"\n | \"streaming\"\n | \"connection lost — retrying\"\n | \"completed\"\n | \"failed\"\n | \"stopped\"\n | \"interrupted\";\n\n/**\n * `stopping` is a stop in flight: the source may still flush rows, so the tail\n * keeps reading until the done frame.\n */\nexport function isTerminalSessionState(state: LogSessionState | undefined): boolean {\n return state !== undefined && isSessionTerminal(state);\n}\n\nfunction isTerminalTailStatus(status: LogTailStatus): boolean {\n return isSessionState(status) && isSessionTerminal(status);\n}\n\nexport interface LogTailBuffer {\n /** Rows in arrival order, oldest first — the order this surface tails in. */\n rows: Record<string, unknown>[];\n /**\n * Each entry's presented ClickyRow, index-aligned with `rows` (same length,\n * same order) — `undefined` where the event carried none. Additive: a plain\n * log tail never reads this and `rows` itself is unchanged, so existing\n * `useLogTail` consumers keep working exactly as before.\n */\n clickyRows: (ClickyRow | undefined)[];\n /** Highest sequence applied, or null before the first frame. Never assumed to start at 1. */\n lastSequence: number | null;\n /** Rows evicted by the cap. Kept so the UI can say so instead of quietly shortening history. */\n dropped: number;\n}\n\nexport const emptyLogTailBuffer: LogTailBuffer = {\n rows: [],\n clickyRows: [],\n lastSequence: null,\n dropped: 0,\n};\n\n/**\n * Renders profile parameters as the tail of a session request's query string,\n * `\"\"` when there are none, and each entry already `&`-prefixed so it appends\n * to a query that always begins with `?follow=true`.\n *\n * Keys are sorted so two callers passing the same parameters in a different\n * order produce the same string — the effect that opens the session depends on\n * it, and an unstable spelling would tear down a live tail and start a new one\n * on a render that changed nothing.\n *\n * A null or undefined value is omitted rather than sent as the text \"null\": the\n * server reads every query key as a supplied parameter, so spelling an absent\n * one out states a filter the caller did not ask for.\n */\nexport function encodeTailParams(params: Record<string, unknown> | undefined): string {\n if (!params) return \"\";\n const query = new URLSearchParams();\n for (const key of Object.keys(params).sort()) {\n const value = params[key];\n if (value === undefined || value === null) continue;\n query.append(key, String(value));\n }\n const encoded = query.toString();\n return encoded === \"\" ? \"\" : `&${encoded}`;\n}\n\n/**\n * Applies one frame to the buffer, dropping anything at or below the highest\n * sequence already seen. A watermark is enough — and cheaper than remembering\n * every id — because SSE is ordered within a connection and the only source of\n * duplicates is a reconnect, whose replay starts at Last-Event-ID and so can\n * only re-deliver frames the buffer has already passed.\n *\n * Returns the buffer unchanged when the frame adds nothing, so React can bail\n * out of the render.\n */\nexport function appendTailEvent(\n buffer: LogTailBuffer,\n event: LogTailEvent,\n maxRows: number,\n): LogTailBuffer {\n if (buffer.lastSequence !== null && event.sequence <= buffer.lastSequence) return buffer;\n const incoming = event.rows ?? (event.row ? [event.row] : []);\n if (incoming.length === 0) return { ...buffer, lastSequence: event.sequence };\n\n // A batched `rows` frame has no per-item clickyRow equivalent — only the\n // single-`row` shape trace/follow sessions use ever carries one — so a\n // batch pads with `undefined` to stay index-aligned with `rows`.\n const incomingClickyRows: (ClickyRow | undefined)[] = event.rows\n ? event.rows.map(() => undefined)\n : [event.clickyRow];\n\n const merged = buffer.rows.concat(incoming);\n const mergedClickyRows = buffer.clickyRows.concat(incomingClickyRows);\n const overflow = Math.max(0, merged.length - Math.max(1, maxRows));\n return {\n rows: overflow > 0 ? merged.slice(overflow) : merged,\n clickyRows: overflow > 0 ? mergedClickyRows.slice(overflow) : mergedClickyRows,\n lastSequence: event.sequence,\n dropped: buffer.dropped + overflow,\n };\n}\n\nexport interface UseLogTailOptions {\n /** Query profile to follow, e.g. \"k8s-pod-logs\". */\n profile: string;\n /** Profile parameters, sent as the session request's query string. */\n params?: Record<string, unknown> | undefined;\n /** Base path of the session API, e.g. \"/api/v1\". */\n basePath?: string | undefined;\n /** Start/stop is the caller's: flipping this to false closes the stream; the server reaps the unsubscribed session. */\n following?: boolean | undefined;\n /** Rows kept in memory; older ones are evicted and counted in `droppedRows`. */\n maxRows?: number | undefined;\n /**\n * Called once per applied `event` frame, in sequence order, before the frame\n * reaches the buffer — so a reducer sees every row even after `maxRows` starts\n * evicting. A replayed sequence is not delivered again, and `done` frames are\n * never delivered. The latest callback is always used; changing its identity\n * does not restart the session. A throw is reported as a `stream` error.\n */\n onEvent?: ((event: LogTailEvent) => void) | undefined;\n}\n\nexport interface UseLogTailResult {\n /** Accumulated rows, oldest first. */\n rows: Record<string, unknown>[];\n /** Each row's presented ClickyRow, index-aligned with `rows`; `undefined`\n * where the event carried none (a plain log tail, or a top-session\n * snapshot event). */\n clickyRows: (ClickyRow | undefined)[];\n status: LogTailStatus;\n error: LogTailError | null;\n sessionId: string | null;\n /** True while the caller wants rows and the session has not reached a terminal state. */\n following: boolean;\n /** Rows the cap evicted; non-zero means the tail no longer holds the whole run. */\n droppedRows: number;\n /** Highest sequence applied — the resume point a reconnect would ask for. */\n lastSequence: number | null;\n /** Latest SessionInfo the server sent, for eventCount / startedAt / state. */\n session: LogSessionInfo | null;\n}\n\nconst DEFAULT_BASE = \"/api/v1\";\nconst DEFAULT_MAX_ROWS = 5_000;\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nexport function useLogTail(options: UseLogTailOptions): UseLogTailResult {\n const {\n profile,\n params,\n basePath = DEFAULT_BASE,\n following: wantFollow = false,\n maxRows = DEFAULT_MAX_ROWS,\n onEvent,\n } = options;\n\n const [buffer, setBuffer] = useState<LogTailBuffer>(emptyLogTailBuffer);\n const [status, setStatus] = useState<LogTailStatus>(\"idle\");\n const [error, setError] = useState<LogTailError | null>(null);\n const [session, setSession] = useState<LogSessionInfo | null>(null);\n\n // The cap is read when a frame lands rather than closed over, so changing how\n // much history to keep never tears down a live session to do it.\n const maxRowsRef = useRef(maxRows);\n maxRowsRef.current = maxRows;\n const onEventRef = useRef(onEvent);\n onEventRef.current = onEvent;\n\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n // Profile parameters travel in the query string because that is where the\n // session endpoint reads them from: it builds its params by walking\n // r.URL.Query() and never looks at the body, so a posted body is a filter the\n // server silently ignores — the tail would run the profile's defaults while\n // the reader believed they were following what they had filtered to.\n //\n // Serialized here rather than in the effect so it is also the effect's\n // dependency: two renders with equal parameters must not restart the session.\n const paramsQuery = encodeTailParams(params);\n\n useEffect(() => {\n if (!wantFollow) return;\n\n // Each start is a distinct server session replaying from its own first\n // sequence, so the previous run's rows cannot be continued — carrying them\n // over would interleave two unrelated sequence spaces in one list.\n setBuffer(emptyLogTailBuffer);\n setError(null);\n setSession(null);\n\n // Checked before the session exists: a session nobody can subscribe to\n // would hold a server slot until the reaper noticed.\n if (!hasEventSource()) {\n setStatus(\"failed\");\n setError({\n scope: \"start\",\n message: `cannot follow ${profile}: this browser has no EventSource, and session events are served only as SSE`,\n });\n return;\n }\n setStatus(\"starting\");\n\n let cancelled = false;\n const aborter = new AbortController();\n let source: EventSourceLike | undefined;\n let ended = false;\n // onEvent's own watermark, tracked here rather than read from the buffer:\n // the buffer only dedupes inside a state updater, which React may run twice,\n // so a callback there would double-count. Same rule as appendTailEvent.\n let delivered: number | null = null;\n\n const deliver = (event: LogTailEvent) => {\n if (delivered !== null && event.sequence <= delivered) return;\n delivered = event.sequence;\n try {\n onEventRef.current?.(event);\n } catch (cause) {\n setError({ scope: \"stream\", message: `onEvent threw on sequence ${event.sequence}: ${String(cause)}` });\n }\n };\n\n const apply = (event: LogTailEvent) => {\n // An event-level error does not end the session, so it is reported beside\n // the rows rather than in place of them. Set before delivery so a reducer\n // that throws on the same frame is the error left standing.\n if (event.error) setError({ scope: \"stream\", message: event.error });\n deliver(event);\n setBuffer((prev) => appendTailEvent(prev, event, maxRowsRef.current));\n };\n\n // Unlike the task stream this does not skip what it cannot parse: a tail\n // that silently discards frames is indistinguishable from a quiet log\n // source, which is the one thing a reader must never be left guessing at.\n const readFrame = (label: string, e: MessageEvent): unknown => {\n try {\n return JSON.parse(e.data as string);\n } catch (cause) {\n setError({ scope: \"stream\", message: `unparseable ${label} frame: ${String(cause)}` });\n return undefined;\n }\n };\n\n const settle = (info: LogSessionInfo) => {\n setSession(info);\n if (!isTerminalSessionState(info.state)) return;\n ended = true;\n setStatus(info.state as LogTailStatus);\n if (info.state === \"failed\") {\n setError({\n scope: \"session\",\n message: info.error || `session ${info.id} failed without reporting a reason`,\n });\n }\n };\n\n const subscribe = (id: string) => {\n const es = eventSourceFactoryRef.current(`${basePath}/sessions/${encodeURIComponent(id)}/events`);\n source = es;\n setStatus(\"streaming\");\n es.addEventListener(\"event\", (e) => {\n const frame = readFrame(\"event\", e as MessageEvent);\n if (frame) apply(frame as LogTailEvent);\n });\n es.addEventListener(\"done\", (e) => {\n const info = readFrame(\"done\", e as MessageEvent);\n if (info) settle(info as LogSessionInfo);\n es.close();\n });\n // A dropped connection is not a failure: EventSource reconnects with\n // Last-Event-ID and the server replays. It is reported through `status`\n // only, so a blip never leaves a sticky error the reader has to dismiss —\n // and never after the session ended, where it would overwrite the reason\n // it ended with a reconnect that is not going to happen.\n es.onerror = () => {\n if (!ended) setStatus(\"connection lost — retrying\");\n };\n };\n\n const start = async () => {\n let info: LogSessionInfo;\n try {\n const res = await fetch(\n `${basePath}/profile/${encodeURIComponent(profile)}/sessions?follow=true${paramsQuery}`,\n { method: \"POST\", headers: { Accept: \"application/json\" }, signal: aborter.signal },\n );\n if (cancelled) return;\n if (!res.ok) {\n const detail = (await res.text()).trim() || \"no detail\";\n setStatus(\"failed\");\n setError({\n scope: \"start\",\n httpStatus: res.status,\n message:\n res.status === 409\n ? `the server is already running as many follow sessions as it allows (HTTP 409): ${detail}`\n : `could not start a follow session for ${profile} (HTTP ${res.status}): ${detail}`,\n });\n return;\n }\n info = (await res.json()) as LogSessionInfo;\n } catch (cause) {\n // Our own abort on unmount or a parameter change: nothing is left to report to.\n if (cancelled) return;\n setStatus(\"failed\");\n setError({\n scope: \"start\",\n message: `could not reach ${basePath} to start a follow session for ${profile}: ${String(cause)}`,\n });\n return;\n }\n\n // The caller stopped following while the POST was in flight. If the\n // session was created, nothing ever subscribes, so the server ends it.\n if (cancelled) return;\n setSession(info);\n subscribe(info.id);\n };\n\n void start();\n // Aborting the start and closing the stream is the whole release: a view\n // session without a subscriber ends server-side.\n return () => {\n cancelled = true;\n aborter.abort();\n source?.close();\n };\n }, [profile, paramsQuery, basePath, wantFollow]);\n\n // Stopping returns the surface to idle, but a session that ended on its own\n // keeps its terminal status so the reason it ended stays on screen.\n useEffect(() => {\n if (!wantFollow) setStatus((prev) => (isTerminalTailStatus(prev) ? prev : \"idle\"));\n }, [wantFollow]);\n\n return {\n rows: buffer.rows,\n clickyRows: buffer.clickyRows,\n status,\n error,\n sessionId: session?.id ?? null,\n following: wantFollow && !isTerminalTailStatus(status),\n droppedRows: buffer.dropped,\n lastSequence: buffer.lastSequence,\n session,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const react = require("react");
|
|
4
|
+
const eventSource = require("./event-source.cjs");
|
|
4
5
|
const DEFAULT_BASE = "/api/todos";
|
|
5
6
|
const DEFAULT_POLL_MS = 2e3;
|
|
6
7
|
function hasEventSource() {
|
|
@@ -37,6 +38,9 @@ function usePrompts(options = {}) {
|
|
|
37
38
|
} = options;
|
|
38
39
|
const [prompts, setPrompts] = react.useState([]);
|
|
39
40
|
const [status, setStatus] = react.useState("idle");
|
|
41
|
+
const eventSourceFactory = eventSource.useEventSourceFactory();
|
|
42
|
+
const eventSourceFactoryRef = react.useRef(eventSourceFactory);
|
|
43
|
+
eventSourceFactoryRef.current = eventSourceFactory;
|
|
40
44
|
const labelsKey = JSON.stringify(labels ?? {});
|
|
41
45
|
react.useEffect(() => {
|
|
42
46
|
if (!enabled) return;
|
|
@@ -64,7 +68,7 @@ function usePrompts(options = {}) {
|
|
|
64
68
|
if (timer) clearTimeout(timer);
|
|
65
69
|
};
|
|
66
70
|
}
|
|
67
|
-
const es =
|
|
71
|
+
const es = eventSourceFactoryRef.current(`${basePath}/prompts/stream${query ? `?${query}` : ""}`);
|
|
68
72
|
setStatus("connected");
|
|
69
73
|
es.addEventListener("prompts", (e) => {
|
|
70
74
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-prompts.cjs","sources":["../../src/hooks/use-prompts.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { JsonSchemaObject } from \"../components/json-schema-form-types\";\n\n// use-prompts is the generic clicky-ui client for the clicky `prompt` manager\n// (the Go-side question/elicitation broker). Like use-task-runs it is SSE-first —\n// it subscribes to the manager's SSEHandler (event: prompts carrying the full\n// filtered listing) and falls back to polling the JSON endpoint. It speaks only\n// PromptSnapshot, no app concepts; producers (commit checks, tool approvals, AI\n// elicitation) are indistinguishable here.\n\n/** PromptSnapshot mirrors the Go prompt.PromptSnapshot wire shape. */\nexport interface PromptSnapshot {\n id: string;\n kind?: string;\n title: string;\n description?: string;\n schema: JsonSchemaObject;\n /** pending | answered | cancelled | expired */\n state: string;\n value?: Record<string, unknown>;\n cancelled?: boolean;\n owner?: string;\n labels?: Record<string, string>;\n createdAt?: string;\n resolvedAt?: string;\n}\n\nexport interface PromptFilter {\n owner?: string | undefined;\n kind?: string | undefined;\n state?: string | undefined;\n labels?: Record<string, string> | undefined;\n}\n\nexport interface UsePromptsOptions extends PromptFilter {\n /** Base path the prompt API is mounted under, e.g. \"/api/todos\". */\n basePath?: string | undefined;\n enabled?: boolean | undefined;\n pollMs?: number | undefined;\n forcePoll?: boolean | undefined;\n}\n\nexport interface UsePromptsResult {\n prompts: PromptSnapshot[];\n /** Pending prompts only (state === \"pending\"), newest first. */\n pending: PromptSnapshot[];\n status: string;\n}\n\nconst DEFAULT_BASE = \"/api/todos\";\nconst DEFAULT_POLL_MS = 2_000;\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nfunction filterQuery(filter: PromptFilter): string {\n const params = new URLSearchParams();\n if (filter.owner) params.set(\"owner\", filter.owner);\n if (filter.kind) params.set(\"kind\", filter.kind);\n if (filter.state) params.set(\"state\", filter.state);\n for (const [k, v] of Object.entries(filter.labels ?? {})) params.append(\"label\", `${k}=${v}`);\n return params.toString();\n}\n\n/** Resolve a prompt by id with an answer (or cancel it). */\nexport async function answerPrompt(\n basePath: string,\n id: string,\n answer: { values?: Record<string, unknown>; cancelled?: boolean },\n): Promise<void> {\n const res = await fetch(`${basePath}/prompts/${encodeURIComponent(id)}/answer`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(answer),\n });\n if (!res.ok) {\n throw new Error((await res.text()) || `answer failed: ${res.status}`);\n }\n}\n\n// usePrompts subscribes to the filtered prompt listing. The stream has no\n// completion terminal — a dashboard stays subscribed to observe new prompts.\nexport function usePrompts(options: UsePromptsOptions = {}): UsePromptsResult {\n const {\n owner,\n kind,\n state: stateFilter,\n labels,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n } = options;\n\n const [prompts, setPrompts] = useState<PromptSnapshot[]>([]);\n const [status, setStatus] = useState(\"idle\");\n\n const labelsKey = JSON.stringify(labels ?? {});\n useEffect(() => {\n if (!enabled) return;\n const query = filterQuery({ owner, kind, state: stateFilter, labels });\n\n if (forcePoll || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const res = await fetch(`${basePath}/prompts${query ? `?${query}` : \"\"}`, {\n headers: { Accept: \"application/json\" },\n });\n if (res.ok) {\n setPrompts((await res.json()) as PromptSnapshot[]);\n setStatus(\"polling\");\n }\n } catch {\n setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n const es =
|
|
1
|
+
{"version":3,"file":"use-prompts.cjs","sources":["../../src/hooks/use-prompts.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { JsonSchemaObject } from \"../components/json-schema-form-types\";\nimport { useEventSourceFactory } from \"./event-source\";\n\n// use-prompts is the generic clicky-ui client for the clicky `prompt` manager\n// (the Go-side question/elicitation broker). Like use-task-runs it is SSE-first —\n// it subscribes to the manager's SSEHandler (event: prompts carrying the full\n// filtered listing) and falls back to polling the JSON endpoint. It speaks only\n// PromptSnapshot, no app concepts; producers (commit checks, tool approvals, AI\n// elicitation) are indistinguishable here.\n\n/** PromptSnapshot mirrors the Go prompt.PromptSnapshot wire shape. */\nexport interface PromptSnapshot {\n id: string;\n kind?: string;\n title: string;\n description?: string;\n schema: JsonSchemaObject;\n /** pending | answered | cancelled | expired */\n state: string;\n value?: Record<string, unknown>;\n cancelled?: boolean;\n owner?: string;\n labels?: Record<string, string>;\n createdAt?: string;\n resolvedAt?: string;\n}\n\nexport interface PromptFilter {\n owner?: string | undefined;\n kind?: string | undefined;\n state?: string | undefined;\n labels?: Record<string, string> | undefined;\n}\n\nexport interface UsePromptsOptions extends PromptFilter {\n /** Base path the prompt API is mounted under, e.g. \"/api/todos\". */\n basePath?: string | undefined;\n enabled?: boolean | undefined;\n pollMs?: number | undefined;\n forcePoll?: boolean | undefined;\n}\n\nexport interface UsePromptsResult {\n prompts: PromptSnapshot[];\n /** Pending prompts only (state === \"pending\"), newest first. */\n pending: PromptSnapshot[];\n status: string;\n}\n\nconst DEFAULT_BASE = \"/api/todos\";\nconst DEFAULT_POLL_MS = 2_000;\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nfunction filterQuery(filter: PromptFilter): string {\n const params = new URLSearchParams();\n if (filter.owner) params.set(\"owner\", filter.owner);\n if (filter.kind) params.set(\"kind\", filter.kind);\n if (filter.state) params.set(\"state\", filter.state);\n for (const [k, v] of Object.entries(filter.labels ?? {})) params.append(\"label\", `${k}=${v}`);\n return params.toString();\n}\n\n/** Resolve a prompt by id with an answer (or cancel it). */\nexport async function answerPrompt(\n basePath: string,\n id: string,\n answer: { values?: Record<string, unknown>; cancelled?: boolean },\n): Promise<void> {\n const res = await fetch(`${basePath}/prompts/${encodeURIComponent(id)}/answer`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(answer),\n });\n if (!res.ok) {\n throw new Error((await res.text()) || `answer failed: ${res.status}`);\n }\n}\n\n// usePrompts subscribes to the filtered prompt listing. The stream has no\n// completion terminal — a dashboard stays subscribed to observe new prompts.\nexport function usePrompts(options: UsePromptsOptions = {}): UsePromptsResult {\n const {\n owner,\n kind,\n state: stateFilter,\n labels,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n } = options;\n\n const [prompts, setPrompts] = useState<PromptSnapshot[]>([]);\n const [status, setStatus] = useState(\"idle\");\n\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n const labelsKey = JSON.stringify(labels ?? {});\n useEffect(() => {\n if (!enabled) return;\n const query = filterQuery({ owner, kind, state: stateFilter, labels });\n\n if (forcePoll || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const res = await fetch(`${basePath}/prompts${query ? `?${query}` : \"\"}`, {\n headers: { Accept: \"application/json\" },\n });\n if (res.ok) {\n setPrompts((await res.json()) as PromptSnapshot[]);\n setStatus(\"polling\");\n }\n } catch {\n setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n const es = eventSourceFactoryRef.current(`${basePath}/prompts/stream${query ? `?${query}` : \"\"}`);\n setStatus(\"connected\");\n es.addEventListener(\"prompts\", (e) => {\n try {\n setPrompts(JSON.parse((e as MessageEvent).data) as PromptSnapshot[]);\n } catch {\n /* ignore malformed frame */\n }\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [owner, kind, stateFilter, labelsKey, basePath, enabled, pollMs, forcePoll]);\n\n const pending = prompts.filter((p) => p.state === \"pending\");\n return { prompts, pending, status };\n}\n"],"names":["useState","useEventSourceFactory","useRef","useEffect"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-prompts.d.ts","sourceRoot":"","sources":["../../src/hooks/use-prompts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"use-prompts.d.ts","sourceRoot":"","sources":["../../src/hooks/use-prompts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAU7E,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,gBAAgB,CAAC;IACzB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;CAC7C;AAED,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,gEAAgE;IAChE,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAkBD,4DAA4D;AAC5D,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAChE,OAAO,CAAC,IAAI,CAAC,CASf;AAID,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CA+D5E"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { useEventSourceFactory } from "./event-source.js";
|
|
2
3
|
const DEFAULT_BASE = "/api/todos";
|
|
3
4
|
const DEFAULT_POLL_MS = 2e3;
|
|
4
5
|
function hasEventSource() {
|
|
@@ -35,6 +36,9 @@ function usePrompts(options = {}) {
|
|
|
35
36
|
} = options;
|
|
36
37
|
const [prompts, setPrompts] = useState([]);
|
|
37
38
|
const [status, setStatus] = useState("idle");
|
|
39
|
+
const eventSourceFactory = useEventSourceFactory();
|
|
40
|
+
const eventSourceFactoryRef = useRef(eventSourceFactory);
|
|
41
|
+
eventSourceFactoryRef.current = eventSourceFactory;
|
|
38
42
|
const labelsKey = JSON.stringify(labels ?? {});
|
|
39
43
|
useEffect(() => {
|
|
40
44
|
if (!enabled) return;
|
|
@@ -62,7 +66,7 @@ function usePrompts(options = {}) {
|
|
|
62
66
|
if (timer) clearTimeout(timer);
|
|
63
67
|
};
|
|
64
68
|
}
|
|
65
|
-
const es =
|
|
69
|
+
const es = eventSourceFactoryRef.current(`${basePath}/prompts/stream${query ? `?${query}` : ""}`);
|
|
66
70
|
setStatus("connected");
|
|
67
71
|
es.addEventListener("prompts", (e) => {
|
|
68
72
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-prompts.js","sources":["../../src/hooks/use-prompts.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { JsonSchemaObject } from \"../components/json-schema-form-types\";\n\n// use-prompts is the generic clicky-ui client for the clicky `prompt` manager\n// (the Go-side question/elicitation broker). Like use-task-runs it is SSE-first —\n// it subscribes to the manager's SSEHandler (event: prompts carrying the full\n// filtered listing) and falls back to polling the JSON endpoint. It speaks only\n// PromptSnapshot, no app concepts; producers (commit checks, tool approvals, AI\n// elicitation) are indistinguishable here.\n\n/** PromptSnapshot mirrors the Go prompt.PromptSnapshot wire shape. */\nexport interface PromptSnapshot {\n id: string;\n kind?: string;\n title: string;\n description?: string;\n schema: JsonSchemaObject;\n /** pending | answered | cancelled | expired */\n state: string;\n value?: Record<string, unknown>;\n cancelled?: boolean;\n owner?: string;\n labels?: Record<string, string>;\n createdAt?: string;\n resolvedAt?: string;\n}\n\nexport interface PromptFilter {\n owner?: string | undefined;\n kind?: string | undefined;\n state?: string | undefined;\n labels?: Record<string, string> | undefined;\n}\n\nexport interface UsePromptsOptions extends PromptFilter {\n /** Base path the prompt API is mounted under, e.g. \"/api/todos\". */\n basePath?: string | undefined;\n enabled?: boolean | undefined;\n pollMs?: number | undefined;\n forcePoll?: boolean | undefined;\n}\n\nexport interface UsePromptsResult {\n prompts: PromptSnapshot[];\n /** Pending prompts only (state === \"pending\"), newest first. */\n pending: PromptSnapshot[];\n status: string;\n}\n\nconst DEFAULT_BASE = \"/api/todos\";\nconst DEFAULT_POLL_MS = 2_000;\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nfunction filterQuery(filter: PromptFilter): string {\n const params = new URLSearchParams();\n if (filter.owner) params.set(\"owner\", filter.owner);\n if (filter.kind) params.set(\"kind\", filter.kind);\n if (filter.state) params.set(\"state\", filter.state);\n for (const [k, v] of Object.entries(filter.labels ?? {})) params.append(\"label\", `${k}=${v}`);\n return params.toString();\n}\n\n/** Resolve a prompt by id with an answer (or cancel it). */\nexport async function answerPrompt(\n basePath: string,\n id: string,\n answer: { values?: Record<string, unknown>; cancelled?: boolean },\n): Promise<void> {\n const res = await fetch(`${basePath}/prompts/${encodeURIComponent(id)}/answer`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(answer),\n });\n if (!res.ok) {\n throw new Error((await res.text()) || `answer failed: ${res.status}`);\n }\n}\n\n// usePrompts subscribes to the filtered prompt listing. The stream has no\n// completion terminal — a dashboard stays subscribed to observe new prompts.\nexport function usePrompts(options: UsePromptsOptions = {}): UsePromptsResult {\n const {\n owner,\n kind,\n state: stateFilter,\n labels,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n } = options;\n\n const [prompts, setPrompts] = useState<PromptSnapshot[]>([]);\n const [status, setStatus] = useState(\"idle\");\n\n const labelsKey = JSON.stringify(labels ?? {});\n useEffect(() => {\n if (!enabled) return;\n const query = filterQuery({ owner, kind, state: stateFilter, labels });\n\n if (forcePoll || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const res = await fetch(`${basePath}/prompts${query ? `?${query}` : \"\"}`, {\n headers: { Accept: \"application/json\" },\n });\n if (res.ok) {\n setPrompts((await res.json()) as PromptSnapshot[]);\n setStatus(\"polling\");\n }\n } catch {\n setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n const es =
|
|
1
|
+
{"version":3,"file":"use-prompts.js","sources":["../../src/hooks/use-prompts.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { JsonSchemaObject } from \"../components/json-schema-form-types\";\nimport { useEventSourceFactory } from \"./event-source\";\n\n// use-prompts is the generic clicky-ui client for the clicky `prompt` manager\n// (the Go-side question/elicitation broker). Like use-task-runs it is SSE-first —\n// it subscribes to the manager's SSEHandler (event: prompts carrying the full\n// filtered listing) and falls back to polling the JSON endpoint. It speaks only\n// PromptSnapshot, no app concepts; producers (commit checks, tool approvals, AI\n// elicitation) are indistinguishable here.\n\n/** PromptSnapshot mirrors the Go prompt.PromptSnapshot wire shape. */\nexport interface PromptSnapshot {\n id: string;\n kind?: string;\n title: string;\n description?: string;\n schema: JsonSchemaObject;\n /** pending | answered | cancelled | expired */\n state: string;\n value?: Record<string, unknown>;\n cancelled?: boolean;\n owner?: string;\n labels?: Record<string, string>;\n createdAt?: string;\n resolvedAt?: string;\n}\n\nexport interface PromptFilter {\n owner?: string | undefined;\n kind?: string | undefined;\n state?: string | undefined;\n labels?: Record<string, string> | undefined;\n}\n\nexport interface UsePromptsOptions extends PromptFilter {\n /** Base path the prompt API is mounted under, e.g. \"/api/todos\". */\n basePath?: string | undefined;\n enabled?: boolean | undefined;\n pollMs?: number | undefined;\n forcePoll?: boolean | undefined;\n}\n\nexport interface UsePromptsResult {\n prompts: PromptSnapshot[];\n /** Pending prompts only (state === \"pending\"), newest first. */\n pending: PromptSnapshot[];\n status: string;\n}\n\nconst DEFAULT_BASE = \"/api/todos\";\nconst DEFAULT_POLL_MS = 2_000;\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nfunction filterQuery(filter: PromptFilter): string {\n const params = new URLSearchParams();\n if (filter.owner) params.set(\"owner\", filter.owner);\n if (filter.kind) params.set(\"kind\", filter.kind);\n if (filter.state) params.set(\"state\", filter.state);\n for (const [k, v] of Object.entries(filter.labels ?? {})) params.append(\"label\", `${k}=${v}`);\n return params.toString();\n}\n\n/** Resolve a prompt by id with an answer (or cancel it). */\nexport async function answerPrompt(\n basePath: string,\n id: string,\n answer: { values?: Record<string, unknown>; cancelled?: boolean },\n): Promise<void> {\n const res = await fetch(`${basePath}/prompts/${encodeURIComponent(id)}/answer`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(answer),\n });\n if (!res.ok) {\n throw new Error((await res.text()) || `answer failed: ${res.status}`);\n }\n}\n\n// usePrompts subscribes to the filtered prompt listing. The stream has no\n// completion terminal — a dashboard stays subscribed to observe new prompts.\nexport function usePrompts(options: UsePromptsOptions = {}): UsePromptsResult {\n const {\n owner,\n kind,\n state: stateFilter,\n labels,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n } = options;\n\n const [prompts, setPrompts] = useState<PromptSnapshot[]>([]);\n const [status, setStatus] = useState(\"idle\");\n\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n const labelsKey = JSON.stringify(labels ?? {});\n useEffect(() => {\n if (!enabled) return;\n const query = filterQuery({ owner, kind, state: stateFilter, labels });\n\n if (forcePoll || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const res = await fetch(`${basePath}/prompts${query ? `?${query}` : \"\"}`, {\n headers: { Accept: \"application/json\" },\n });\n if (res.ok) {\n setPrompts((await res.json()) as PromptSnapshot[]);\n setStatus(\"polling\");\n }\n } catch {\n setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n const es = eventSourceFactoryRef.current(`${basePath}/prompts/stream${query ? `?${query}` : \"\"}`);\n setStatus(\"connected\");\n es.addEventListener(\"prompts\", (e) => {\n try {\n setPrompts(JSON.parse((e as MessageEvent).data) as PromptSnapshot[]);\n } catch {\n /* ignore malformed frame */\n }\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [owner, kind, stateFilter, labelsKey, basePath, enabled, pollMs, forcePoll]);\n\n const pending = prompts.filter((p) => p.state === \"pending\");\n return { prompts, pending, status };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ const react = require("react");
|
|
|
4
4
|
const TaskSnapshot = require("../data/TaskSnapshot.cjs");
|
|
5
5
|
const taskStreams = require("../data/task-streams.cjs");
|
|
6
6
|
const taskQueryKeys = require("../data/task-query-keys.cjs");
|
|
7
|
+
const eventSource = require("./event-source.cjs");
|
|
7
8
|
const DEFAULT_BASE = "/api/v1";
|
|
8
9
|
const DEFAULT_POLL_MS = 2e3;
|
|
9
10
|
function streamUrl(base, query) {
|
|
@@ -29,6 +30,9 @@ function useTaskRun(options = {}) {
|
|
|
29
30
|
const [streamsById, setStreamsById] = react.useState({});
|
|
30
31
|
const [status, setStatus] = react.useState("idle");
|
|
31
32
|
const [isComplete, setIsComplete] = react.useState(false);
|
|
33
|
+
const eventSourceFactory = eventSource.useEventSourceFactory();
|
|
34
|
+
const eventSourceFactoryRef = react.useRef(eventSourceFactory);
|
|
35
|
+
eventSourceFactoryRef.current = eventSourceFactory;
|
|
32
36
|
react.useEffect(() => {
|
|
33
37
|
const runIds = runIdsKey ? runIdsKey.split(",") : [];
|
|
34
38
|
setStateKey(subscriptionKey);
|
|
@@ -92,7 +96,7 @@ function useTaskRun(options = {}) {
|
|
|
92
96
|
if (timer) clearTimeout(timer);
|
|
93
97
|
};
|
|
94
98
|
}
|
|
95
|
-
const es =
|
|
99
|
+
const es = eventSourceFactoryRef.current(streamUrl(basePath, query));
|
|
96
100
|
setStatus("connecting");
|
|
97
101
|
es.onopen = () => setStatus("connected");
|
|
98
102
|
es.addEventListener("task", (e) => {
|
|
@@ -137,6 +141,9 @@ function useTaskRuns(options = {}) {
|
|
|
137
141
|
} = options;
|
|
138
142
|
const [received, setReceived] = react.useState({ key: null, runs: [] });
|
|
139
143
|
const [status, setStatus] = react.useState("idle");
|
|
144
|
+
const eventSourceFactory = eventSource.useEventSourceFactory();
|
|
145
|
+
const eventSourceFactoryRef = react.useRef(eventSourceFactory);
|
|
146
|
+
eventSourceFactoryRef.current = eventSourceFactory;
|
|
140
147
|
react.useEffect(() => {
|
|
141
148
|
if (!enabled) return;
|
|
142
149
|
const params = new URLSearchParams();
|
|
@@ -172,7 +179,7 @@ function useTaskRuns(options = {}) {
|
|
|
172
179
|
if (timer) clearTimeout(timer);
|
|
173
180
|
};
|
|
174
181
|
}
|
|
175
|
-
const es =
|
|
182
|
+
const es = eventSourceFactoryRef.current(`${basePath}/tasks/runs/stream${query ? `?${query}` : ""}`);
|
|
176
183
|
setStatus("connecting");
|
|
177
184
|
es.onopen = () => setStatus("connected");
|
|
178
185
|
es.addEventListener("runs", (e) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-task-run.cjs","sources":["../../src/hooks/use-task-run.ts"],"sourcesContent":["import { useEffect, useMemo, useState } from \"react\";\nimport {\n allGroupsTerminal,\n type TaskRunMeta,\n type TaskSnapshot,\n} from \"../data/TaskSnapshot\";\nimport {\n applyTaskOutputDelta,\n emptyTaskStreams,\n type TaskOutputDelta,\n type TaskStreams,\n withTaskStreams,\n} from \"../data/task-streams\";\nimport { taskQueryKeys } from \"../data/task-query-keys\";\n\n// use-task-run / use-task-runs are the generic clicky-ui task clients. They are\n// SSE-first: they subscribe to clicky's SSEHandler (event: task / event: done)\n// and fall back to polling the JSON endpoints only when EventSource is\n// unavailable. They speak only TaskSnapshot / TaskRunMeta — no app concepts.\n\nexport interface TaskTransportOptions {\n /** Base path of the clicky task API, e.g. \"/api/v1\". */\n basePath?: string | undefined;\n /** Disable the subscription entirely (e.g. before an id is known). */\n enabled?: boolean | undefined;\n /** Poll interval (ms) for the fallback / runs listing. */\n pollMs?: number | undefined;\n /** Force the polling transport even when EventSource exists (mainly tests). */\n forcePoll?: boolean | undefined;\n}\n\nconst DEFAULT_BASE = \"/api/v1\";\nconst DEFAULT_POLL_MS = 2_000;\n\nfunction streamUrl(base: string, query: string): string {\n return `${base}/tasks/stream${query ? `?${query}` : \"\"}`;\n}\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nexport interface UseTaskRunResult {\n snapshots: TaskSnapshot[];\n status: string;\n isComplete: boolean;\n}\n\nexport interface UseTaskRunOptions extends TaskTransportOptions {\n /** Stable run id to follow (drill-down). Mutually exclusive with ids/kind. */\n id?: string | undefined;\n /** Stable run ids to follow together. Mutually exclusive with id/kind. */\n ids?: string[] | undefined;\n /** Follow every run of a kind. */\n kind?: string | undefined;\n}\n\n// useTaskRun follows one or more runs (by id) or a kind, SSE-first. It accumulates the\n// latest snapshot per task/group id and reports completion once every group is\n// terminal.\nexport function useTaskRun(options: UseTaskRunOptions = {}): UseTaskRunResult {\n const {\n id,\n ids,\n kind,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n } = options;\n const runIdsKey = (ids?.filter(Boolean) ?? (id ? [id] : [])).join(\",\");\n // State is reset in an effect, so the first render after the subscription\n // changes still holds the previous run's snapshots and completion; stateKey\n // hides them until the reset lands.\n const subscriptionKey = JSON.stringify([runIdsKey, kind ?? \"\", basePath, enabled]);\n const [stateKey, setStateKey] = useState(subscriptionKey);\n\n const [byId, setById] = useState<Record<string, TaskSnapshot>>({});\n // Streams are held beside the snapshots, never inside them: a task frame\n // replaces its snapshot wholesale and would take the accumulated output with\n // it. See ../data/task-streams.\n const [streamsById, setStreamsById] = useState<Record<string, TaskStreams>>({});\n const [status, setStatus] = useState(\"idle\");\n const [isComplete, setIsComplete] = useState(false);\n\n useEffect(() => {\n const runIds = runIdsKey ? runIdsKey.split(\",\") : [];\n setStateKey(subscriptionKey);\n if (!enabled || (runIds.length === 0 && !kind)) {\n setById({});\n setStreamsById({});\n setStatus(\"idle\");\n setIsComplete(false);\n return;\n }\n setById({});\n setStreamsById({});\n setIsComplete(false);\n\n const params = new URLSearchParams();\n if (runIds.length > 0) params.set(\"tasks\", runIdsKey);\n if (kind) params.set(\"kind\", kind);\n const query = params.toString();\n\n const merge = (incoming: TaskSnapshot[]) => {\n setById((prev) => {\n const next = { ...prev };\n for (const snap of incoming) next[snap.type === \"group\" ? `group:${snap.groupId ?? snap.id}` : snap.id] = snap;\n return next;\n });\n };\n\n // Deltas are applied whether or not the task frame has landed yet: the\n // stream is keyed by task id on its own, so output that arrives first is\n // held rather than dropped.\n const mergeOutput = (delta: TaskOutputDelta) => {\n setStreamsById((prev) => ({\n ...prev,\n [delta.id]: applyTaskOutputDelta(prev[delta.id] ?? emptyTaskStreams, delta),\n }));\n };\n\n // Polling fallback transport.\n if (forcePoll || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const responses = await Promise.all(runIds.map((runId) => fetch(\n `${basePath}/tasks/${encodeURIComponent(runId)}`,\n { headers: { Accept: \"application/json\" } },\n )));\n if (responses.some((response) => !response.ok)) throw new Error(\"Task request failed\");\n if (responses.every((response) => response.ok)) {\n const snaps = (await Promise.all(responses.map(\n async (response) => (await response.json()) as TaskSnapshot[],\n ))).flat();\n if (stopped) return;\n merge(snaps);\n setStatus(\"polling\");\n if (allGroupsTerminal(snaps)) {\n setIsComplete(true);\n return; // stop polling\n }\n }\n } catch {\n if (!stopped) setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n // SSE transport (default).\n const es = new EventSource(streamUrl(basePath, query));\n setStatus(\"connecting\");\n es.onopen = () => setStatus(\"connected\");\n es.addEventListener(\"task\", (e) => {\n try {\n merge([JSON.parse((e as MessageEvent).data) as TaskSnapshot]);\n setStatus(\"connected\");\n } catch {\n setStatus(\"invalid task stream data\");\n }\n });\n es.addEventListener(\"output\", (e) => {\n try {\n mergeOutput(JSON.parse((e as MessageEvent).data) as TaskOutputDelta);\n } catch {\n setStatus(\"invalid task output data\");\n }\n });\n es.addEventListener(\"done\", () => {\n setStatus(\"complete\");\n setIsComplete(true);\n es.close();\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [subscriptionKey, runIdsKey, kind, basePath, enabled, pollMs, forcePoll]);\n\n const current = stateKey === subscriptionKey;\n // The streams are merged back in only here, at the boundary. The polling\n // fallback never populates them — its JSON snapshots already carry the full\n // output — so this is a no-op on that transport.\n const snapshots = useMemo(\n () => current\n ? Object.values(byId).map((snapshot) => withTaskStreams(snapshot, streamsById[snapshot.id]))\n : [],\n [current, byId, streamsById],\n );\n\n return { snapshots, status: current ? status : \"idle\", isComplete: current && isComplete };\n}\n\nexport interface UseTaskRunsOptions extends TaskTransportOptions {\n kind?: string | undefined;\n status?: string | undefined;\n /** Label equality filters (k=v). */\n labels?: Record<string, string> | undefined;\n}\n\n/** The identity of the listing request that produced a given `runs` array. */\nexport type TaskRunsKey = ReturnType<typeof taskQueryKeys.runs>;\n\nexport interface UseTaskRunsResult {\n runs: TaskRunMeta[];\n status: string;\n /**\n * Query key of the request that actually produced `runs`, or null before the\n * first frame arrives. Changing `kind`/`status`/`labels` re-subscribes but\n * keeps the previous listing on screen until the new transport delivers, so\n * this key lags `taskQueryKeys.runs(options)` during that window. A consumer\n * mirroring `runs` into a cache must compare the two and skip the write while\n * they differ, or it publishes rows that do not match the requested filters.\n */\n runsKey: TaskRunsKey | null;\n}\n\n// useTaskRuns lists all runs (TaskRunMeta) for the manager view. It is SSE-first:\n// it subscribes to clicky's runs stream (event: runs carrying the full listing)\n// and falls back to polling the JSON runs endpoint only when EventSource is\n// unavailable or forced off. The listing has no completion terminal of its own —\n// a manager view stays subscribed to observe new and changing runs.\nexport function useTaskRuns(options: UseTaskRunsOptions = {}): UseTaskRunsResult {\n const {\n kind,\n status: statusFilter,\n labels,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n } = options;\n\n // Runs are stored together with the key of the request that delivered them, so\n // a stale listing can never be mistaken for the current filters' result.\n const [received, setReceived] = useState<{\n key: TaskRunsKey | null;\n runs: TaskRunMeta[];\n }>({ key: null, runs: [] });\n const [status, setStatus] = useState(\"idle\");\n\n useEffect(() => {\n if (!enabled) return;\n\n const params = new URLSearchParams();\n if (kind) params.set(\"kind\", kind);\n if (statusFilter) params.set(\"status\", statusFilter);\n for (const [k, v] of Object.entries(labels ?? {})) params.append(\"label\", `${k}=${v}`);\n const query = params.toString();\n const requestKey = taskQueryKeys.runs({ basePath, kind, status: statusFilter, labels });\n const receive = (next: TaskRunMeta[]) => setReceived({ key: requestKey, runs: next });\n\n // Polling fallback transport.\n if (forcePoll || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const res = await fetch(`${basePath}/tasks${query ? `?${query}` : \"\"}`, {\n headers: { Accept: \"application/json\" },\n });\n if (!res.ok) throw new Error(`Task listing failed with ${res.status}`);\n if (res.ok) {\n const next = (await res.json()) as TaskRunMeta[];\n if (stopped) return;\n receive(next);\n setStatus(\"polling\");\n }\n } catch {\n if (!stopped) setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n // SSE transport (default).\n const es = new EventSource(`${basePath}/tasks/runs/stream${query ? `?${query}` : \"\"}`);\n setStatus(\"connecting\");\n es.onopen = () => setStatus(\"connected\");\n es.addEventListener(\"runs\", (e) => {\n try {\n receive(JSON.parse((e as MessageEvent).data) as TaskRunMeta[]);\n setStatus(\"connected\");\n } catch {\n setStatus(\"invalid task listing data\");\n }\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [kind, statusFilter, JSON.stringify(labels ?? {}), basePath, enabled, pollMs, forcePoll]);\n\n return { runs: received.runs, status, runsKey: received.key };\n}\n"],"names":["useState","useEffect","applyTaskOutputDelta","emptyTaskStreams","allGroupsTerminal","useMemo","withTaskStreams","taskQueryKeys"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-task-run.cjs","sources":["../../src/hooks/use-task-run.ts"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n allGroupsTerminal,\n type TaskRunMeta,\n type TaskSnapshot,\n} from \"../data/TaskSnapshot\";\nimport {\n applyTaskOutputDelta,\n emptyTaskStreams,\n type TaskOutputDelta,\n type TaskStreams,\n withTaskStreams,\n} from \"../data/task-streams\";\nimport { taskQueryKeys } from \"../data/task-query-keys\";\nimport { useEventSourceFactory } from \"./event-source\";\n\n// use-task-run / use-task-runs are the generic clicky-ui task clients. They are\n// SSE-first: they subscribe to clicky's SSEHandler (event: task / event: done)\n// and fall back to polling the JSON endpoints only when EventSource is\n// unavailable. They speak only TaskSnapshot / TaskRunMeta — no app concepts.\n\nexport interface TaskTransportOptions {\n /** Base path of the clicky task API, e.g. \"/api/v1\". */\n basePath?: string | undefined;\n /** Disable the subscription entirely (e.g. before an id is known). */\n enabled?: boolean | undefined;\n /** Poll interval (ms) for the fallback / runs listing. */\n pollMs?: number | undefined;\n /** Force the polling transport even when EventSource exists (mainly tests). */\n forcePoll?: boolean | undefined;\n}\n\nconst DEFAULT_BASE = \"/api/v1\";\nconst DEFAULT_POLL_MS = 2_000;\n\nfunction streamUrl(base: string, query: string): string {\n return `${base}/tasks/stream${query ? `?${query}` : \"\"}`;\n}\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nexport interface UseTaskRunResult {\n snapshots: TaskSnapshot[];\n status: string;\n isComplete: boolean;\n}\n\nexport interface UseTaskRunOptions extends TaskTransportOptions {\n /** Stable run id to follow (drill-down). Mutually exclusive with ids/kind. */\n id?: string | undefined;\n /** Stable run ids to follow together. Mutually exclusive with id/kind. */\n ids?: string[] | undefined;\n /** Follow every run of a kind. */\n kind?: string | undefined;\n}\n\n// useTaskRun follows one or more runs (by id) or a kind, SSE-first. It accumulates the\n// latest snapshot per task/group id and reports completion once every group is\n// terminal.\nexport function useTaskRun(options: UseTaskRunOptions = {}): UseTaskRunResult {\n const {\n id,\n ids,\n kind,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n } = options;\n const runIdsKey = (ids?.filter(Boolean) ?? (id ? [id] : [])).join(\",\");\n // State is reset in an effect, so the first render after the subscription\n // changes still holds the previous run's snapshots and completion; stateKey\n // hides them until the reset lands.\n const subscriptionKey = JSON.stringify([runIdsKey, kind ?? \"\", basePath, enabled]);\n const [stateKey, setStateKey] = useState(subscriptionKey);\n\n const [byId, setById] = useState<Record<string, TaskSnapshot>>({});\n // Streams are held beside the snapshots, never inside them: a task frame\n // replaces its snapshot wholesale and would take the accumulated output with\n // it. See ../data/task-streams.\n const [streamsById, setStreamsById] = useState<Record<string, TaskStreams>>({});\n const [status, setStatus] = useState(\"idle\");\n const [isComplete, setIsComplete] = useState(false);\n\n // Read through a ref so a host's inline `value={(url) => ...}` factory prop\n // does not restart the subscription every render — only the effect's own\n // deps below do that.\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n useEffect(() => {\n const runIds = runIdsKey ? runIdsKey.split(\",\") : [];\n setStateKey(subscriptionKey);\n if (!enabled || (runIds.length === 0 && !kind)) {\n setById({});\n setStreamsById({});\n setStatus(\"idle\");\n setIsComplete(false);\n return;\n }\n setById({});\n setStreamsById({});\n setIsComplete(false);\n\n const params = new URLSearchParams();\n if (runIds.length > 0) params.set(\"tasks\", runIdsKey);\n if (kind) params.set(\"kind\", kind);\n const query = params.toString();\n\n const merge = (incoming: TaskSnapshot[]) => {\n setById((prev) => {\n const next = { ...prev };\n for (const snap of incoming) next[snap.type === \"group\" ? `group:${snap.groupId ?? snap.id}` : snap.id] = snap;\n return next;\n });\n };\n\n // Deltas are applied whether or not the task frame has landed yet: the\n // stream is keyed by task id on its own, so output that arrives first is\n // held rather than dropped.\n const mergeOutput = (delta: TaskOutputDelta) => {\n setStreamsById((prev) => ({\n ...prev,\n [delta.id]: applyTaskOutputDelta(prev[delta.id] ?? emptyTaskStreams, delta),\n }));\n };\n\n // Polling fallback transport.\n if (forcePoll || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const responses = await Promise.all(runIds.map((runId) => fetch(\n `${basePath}/tasks/${encodeURIComponent(runId)}`,\n { headers: { Accept: \"application/json\" } },\n )));\n if (responses.some((response) => !response.ok)) throw new Error(\"Task request failed\");\n if (responses.every((response) => response.ok)) {\n const snaps = (await Promise.all(responses.map(\n async (response) => (await response.json()) as TaskSnapshot[],\n ))).flat();\n if (stopped) return;\n merge(snaps);\n setStatus(\"polling\");\n if (allGroupsTerminal(snaps)) {\n setIsComplete(true);\n return; // stop polling\n }\n }\n } catch {\n if (!stopped) setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n // SSE transport (default).\n const es = eventSourceFactoryRef.current(streamUrl(basePath, query));\n setStatus(\"connecting\");\n es.onopen = () => setStatus(\"connected\");\n es.addEventListener(\"task\", (e) => {\n try {\n merge([JSON.parse((e as MessageEvent).data) as TaskSnapshot]);\n setStatus(\"connected\");\n } catch {\n setStatus(\"invalid task stream data\");\n }\n });\n es.addEventListener(\"output\", (e) => {\n try {\n mergeOutput(JSON.parse((e as MessageEvent).data) as TaskOutputDelta);\n } catch {\n setStatus(\"invalid task output data\");\n }\n });\n es.addEventListener(\"done\", () => {\n setStatus(\"complete\");\n setIsComplete(true);\n es.close();\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [subscriptionKey, runIdsKey, kind, basePath, enabled, pollMs, forcePoll]);\n\n const current = stateKey === subscriptionKey;\n // The streams are merged back in only here, at the boundary. The polling\n // fallback never populates them — its JSON snapshots already carry the full\n // output — so this is a no-op on that transport.\n const snapshots = useMemo(\n () => current\n ? Object.values(byId).map((snapshot) => withTaskStreams(snapshot, streamsById[snapshot.id]))\n : [],\n [current, byId, streamsById],\n );\n\n return { snapshots, status: current ? status : \"idle\", isComplete: current && isComplete };\n}\n\nexport interface UseTaskRunsOptions extends TaskTransportOptions {\n kind?: string | undefined;\n status?: string | undefined;\n /** Label equality filters (k=v). */\n labels?: Record<string, string> | undefined;\n}\n\n/** The identity of the listing request that produced a given `runs` array. */\nexport type TaskRunsKey = ReturnType<typeof taskQueryKeys.runs>;\n\nexport interface UseTaskRunsResult {\n runs: TaskRunMeta[];\n status: string;\n /**\n * Query key of the request that actually produced `runs`, or null before the\n * first frame arrives. Changing `kind`/`status`/`labels` re-subscribes but\n * keeps the previous listing on screen until the new transport delivers, so\n * this key lags `taskQueryKeys.runs(options)` during that window. A consumer\n * mirroring `runs` into a cache must compare the two and skip the write while\n * they differ, or it publishes rows that do not match the requested filters.\n */\n runsKey: TaskRunsKey | null;\n}\n\n// useTaskRuns lists all runs (TaskRunMeta) for the manager view. It is SSE-first:\n// it subscribes to clicky's runs stream (event: runs carrying the full listing)\n// and falls back to polling the JSON runs endpoint only when EventSource is\n// unavailable or forced off. The listing has no completion terminal of its own —\n// a manager view stays subscribed to observe new and changing runs.\nexport function useTaskRuns(options: UseTaskRunsOptions = {}): UseTaskRunsResult {\n const {\n kind,\n status: statusFilter,\n labels,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n } = options;\n\n // Runs are stored together with the key of the request that delivered them, so\n // a stale listing can never be mistaken for the current filters' result.\n const [received, setReceived] = useState<{\n key: TaskRunsKey | null;\n runs: TaskRunMeta[];\n }>({ key: null, runs: [] });\n const [status, setStatus] = useState(\"idle\");\n\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n useEffect(() => {\n if (!enabled) return;\n\n const params = new URLSearchParams();\n if (kind) params.set(\"kind\", kind);\n if (statusFilter) params.set(\"status\", statusFilter);\n for (const [k, v] of Object.entries(labels ?? {})) params.append(\"label\", `${k}=${v}`);\n const query = params.toString();\n const requestKey = taskQueryKeys.runs({ basePath, kind, status: statusFilter, labels });\n const receive = (next: TaskRunMeta[]) => setReceived({ key: requestKey, runs: next });\n\n // Polling fallback transport.\n if (forcePoll || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const res = await fetch(`${basePath}/tasks${query ? `?${query}` : \"\"}`, {\n headers: { Accept: \"application/json\" },\n });\n if (!res.ok) throw new Error(`Task listing failed with ${res.status}`);\n if (res.ok) {\n const next = (await res.json()) as TaskRunMeta[];\n if (stopped) return;\n receive(next);\n setStatus(\"polling\");\n }\n } catch {\n if (!stopped) setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n // SSE transport (default).\n const es = eventSourceFactoryRef.current(`${basePath}/tasks/runs/stream${query ? `?${query}` : \"\"}`);\n setStatus(\"connecting\");\n es.onopen = () => setStatus(\"connected\");\n es.addEventListener(\"runs\", (e) => {\n try {\n receive(JSON.parse((e as MessageEvent).data) as TaskRunMeta[]);\n setStatus(\"connected\");\n } catch {\n setStatus(\"invalid task listing data\");\n }\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [kind, statusFilter, JSON.stringify(labels ?? {}), basePath, enabled, pollMs, forcePoll]);\n\n return { runs: received.runs, status, runsKey: received.key };\n}\n"],"names":["useState","useEventSourceFactory","useRef","useEffect","applyTaskOutputDelta","emptyTaskStreams","allGroupsTerminal","useMemo","withTaskStreams","taskQueryKeys"],"mappings":"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"}
|