@appsolutely-io/waypoint 0.0.0-stage → 0.1.1-next.1
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/LICENSE +21 -0
- package/README.md +297 -2
- package/dist/block-reply-r8u-DNmZ.d.cts +181 -0
- package/dist/block-reply-z8El20nD.d.ts +181 -0
- package/dist/blocks-2ABWECKG.js +1 -0
- package/dist/blocks-7453YTYJ.cjs +1 -0
- package/dist/chunk-3KV7TDDR.cjs +3 -0
- package/dist/chunk-4BWV3SEL.cjs +1 -0
- package/dist/chunk-5Q3INHKA.js +1 -0
- package/dist/chunk-5TBO732O.js +1 -0
- package/dist/chunk-6OZSKCPT.cjs +1 -0
- package/dist/chunk-A3YOSFLE.cjs +1 -0
- package/dist/chunk-BRUQ75G3.cjs +1 -0
- package/dist/chunk-C3VS33WJ.js +1 -0
- package/dist/chunk-D2EGPBCX.cjs +1 -0
- package/dist/chunk-D4Q46KWA.js +1 -0
- package/dist/chunk-FBADWFRR.cjs +1 -0
- package/dist/chunk-FQSWQLRW.js +1 -0
- package/dist/chunk-GT6PI4CT.cjs +1 -0
- package/dist/chunk-II26UWZM.cjs +1 -0
- package/dist/chunk-IPI4O4LP.js +1 -0
- package/dist/chunk-JFMVVGGB.js +1 -0
- package/dist/chunk-K73VV7KA.js +1 -0
- package/dist/chunk-L4PFFRDE.js +3 -0
- package/dist/chunk-LLPGVQUT.cjs +1 -0
- package/dist/chunk-M63QOANE.js +1 -0
- package/dist/chunk-MEKDQDSV.js +1 -0
- package/dist/chunk-OEPKPDLD.js +1 -0
- package/dist/chunk-OWT55N6J.js +3 -0
- package/dist/chunk-PFR2J5BV.js +1 -0
- package/dist/chunk-Q7BULGQG.cjs +1 -0
- package/dist/chunk-QFBAOEPD.js +1 -0
- package/dist/chunk-QS3MKZOY.cjs +3 -0
- package/dist/chunk-RZ3AGK3U.cjs +1 -0
- package/dist/chunk-RZZDQWC2.js +1 -0
- package/dist/chunk-S73WZWDP.js +1 -0
- package/dist/chunk-T5XF2R74.js +1 -0
- package/dist/chunk-TCFCSGQ3.js +1 -0
- package/dist/chunk-TODYQ3SG.cjs +1 -0
- package/dist/chunk-TVSQHTK2.cjs +1 -0
- package/dist/chunk-VGGJGRNK.cjs +1 -0
- package/dist/chunk-XSHOLIC7.cjs +1 -0
- package/dist/chunk-XTKUDSDK.cjs +1 -0
- package/dist/chunk-XWDB2LS3.cjs +1 -0
- package/dist/cli.js +3 -0
- package/dist/confirmation-view-5BZ2PP7J.cjs +1 -0
- package/dist/confirmation-view-Z5JHY2F5.js +1 -0
- package/dist/conformance.cjs +1 -0
- package/dist/conformance.d.cts +10 -0
- package/dist/conformance.d.ts +10 -0
- package/dist/conformance.js +1 -0
- package/dist/flow-parser.cjs +1 -0
- package/dist/flow-parser.d.cts +3 -0
- package/dist/flow-parser.d.ts +3 -0
- package/dist/flow-parser.js +1 -0
- package/dist/flows-panel-2SCPYZ3X.js +1 -0
- package/dist/flows-panel-HQYJMOP4.cjs +1 -0
- package/dist/form-extras-RNMBCYV7.cjs +1 -0
- package/dist/form-extras-X3B3JCMH.js +1 -0
- package/dist/form-fill-C32AHxSr.d.cts +653 -0
- package/dist/form-fill-Dp3ut1_Y.d.ts +653 -0
- package/dist/form-session.cjs +1 -0
- package/dist/form-session.d.cts +7 -0
- package/dist/form-session.d.ts +7 -0
- package/dist/form-session.js +1 -0
- package/dist/frame-decoder.cjs +3 -0
- package/dist/frame-decoder.d.cts +5 -0
- package/dist/frame-decoder.d.ts +5 -0
- package/dist/frame-decoder.js +3 -0
- package/dist/generated-C9tbjwK0.d.cts +797 -0
- package/dist/generated-C9tbjwK0.d.ts +797 -0
- package/dist/hooks/index.cjs +1 -0
- package/dist/hooks/index.d.cts +48 -0
- package/dist/hooks/index.d.ts +48 -0
- package/dist/hooks/index.js +1 -0
- package/dist/i18n-WC9f-h_q.d.cts +151 -0
- package/dist/i18n-WC9f-h_q.d.ts +151 -0
- package/dist/index-B5idIFTx.d.cts +34 -0
- package/dist/index-DPzoKXCK.d.ts +34 -0
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +38 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +3 -0
- package/dist/intent-BQPPows1.d.cts +154 -0
- package/dist/intent-BuvqEU3Q.d.ts +154 -0
- package/dist/intent.cjs +1 -0
- package/dist/intent.d.cts +2 -0
- package/dist/intent.d.ts +2 -0
- package/dist/intent.js +1 -0
- package/dist/markdown-5NUOCOGY.js +23 -0
- package/dist/markdown-CG7NXI7X.cjs +23 -0
- package/dist/message-actions-ORBVYGNE.cjs +1 -0
- package/dist/message-actions-XNBBFHGW.js +1 -0
- package/dist/messages/en/index.cjs +1 -0
- package/dist/messages/en/index.d.cts +3 -0
- package/dist/messages/en/index.d.ts +3 -0
- package/dist/messages/en/index.js +1 -0
- package/dist/messages/index.cjs +1 -0
- package/dist/messages/index.d.cts +14 -0
- package/dist/messages/index.d.ts +14 -0
- package/dist/messages/index.js +1 -0
- package/dist/messages/zh-Hans/index.cjs +1 -0
- package/dist/messages/zh-Hans/index.d.cts +3 -0
- package/dist/messages/zh-Hans/index.d.ts +3 -0
- package/dist/messages/zh-Hans/index.js +1 -0
- package/dist/on-demand.cjs +1 -0
- package/dist/on-demand.d.cts +56 -0
- package/dist/on-demand.d.ts +56 -0
- package/dist/on-demand.js +1 -0
- package/dist/problem-messages-THONMXFZ.cjs +1 -0
- package/dist/problem-messages-Z5QGO6RE.js +1 -0
- package/dist/react/index.cjs +1 -0
- package/dist/react/index.d.cts +421 -0
- package/dist/react/index.d.ts +421 -0
- package/dist/react/index.js +1 -0
- package/dist/reads-Bk84x_rt.d.ts +38 -0
- package/dist/reads-DlpCt8dE.d.cts +38 -0
- package/dist/recovery-panel-6EBUMANX.js +3 -0
- package/dist/recovery-panel-EQIIV7TT.cjs +3 -0
- package/dist/reply-controls-HUYHUO3W.js +1 -0
- package/dist/reply-controls-LGY2RKRJ.cjs +1 -0
- package/dist/run-XBABOEXK.js +1 -0
- package/dist/schema.json +2900 -0
- package/dist/skin-antd/index.cjs +1 -0
- package/dist/skin-antd/index.d.cts +30 -0
- package/dist/skin-antd/index.d.ts +30 -0
- package/dist/skin-antd/index.js +1 -0
- package/dist/skin-default/index.cjs +1 -0
- package/dist/skin-default/index.d.cts +42 -0
- package/dist/skin-default/index.d.ts +42 -0
- package/dist/skin-default/index.js +1 -0
- package/dist/step-name-BlgVvshf.d.ts +19 -0
- package/dist/step-name-CB3d3kkP.d.cts +19 -0
- package/dist/storage-commit.cjs +1 -0
- package/dist/storage-commit.d.cts +17 -0
- package/dist/storage-commit.d.ts +17 -0
- package/dist/storage-commit.js +1 -0
- package/dist/stream-reads.cjs +1 -0
- package/dist/stream-reads.d.cts +22 -0
- package/dist/stream-reads.d.ts +22 -0
- package/dist/stream-reads.js +1 -0
- package/dist/styles.css +811 -0
- package/dist/testing/index.cjs +1 -0
- package/dist/testing/index.d.cts +57 -0
- package/dist/testing/index.d.ts +57 -0
- package/dist/testing/index.js +1 -0
- package/docs/examples/message-versions.tsx +74 -0
- package/docs/form-fill-protocol.md +83 -0
- package/docs/message-versions.md +64 -0
- package/docs/plan-protocol.md +83 -0
- package/docs/reply-protocol.md +75 -0
- package/etc/hooks.api.md +2394 -0
- package/etc/index.api.md +2159 -0
- package/etc/messages-en.api.md +13 -0
- package/etc/messages-zh-Hans.api.md +13 -0
- package/etc/messages.api.md +186 -0
- package/etc/react.api.md +2977 -0
- package/etc/skin-antd.api.md +2998 -0
- package/etc/skin-default.api.md +61 -0
- package/etc/testing.api.md +3043 -0
- package/package.json +169 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _chunkBRUQ75G3cjs = require('../chunk-BRUQ75G3.cjs');var _chunkA3YOSFLEcjs = require('../chunk-A3YOSFLE.cjs');require('../chunk-FBADWFRR.cjs');require('../chunk-QS3MKZOY.cjs');require('../chunk-RZ3AGK3U.cjs');var _chunk4BWV3SELcjs = require('../chunk-4BWV3SEL.cjs');require('../chunk-GT6PI4CT.cjs');var _chunkLLPGVQUTcjs = require('../chunk-LLPGVQUT.cjs');var _chunk3KV7TDDRcjs = require('../chunk-3KV7TDDR.cjs');var _chunkQ7BULGQGcjs = require('../chunk-Q7BULGQG.cjs');require('../chunk-XSHOLIC7.cjs');require('../chunk-II26UWZM.cjs');var W=(e,r)=>e.id===r||(_nullishCoalesce(_optionalChain([e, 'access', _ => _.body, 'optionalAccess', _2 => _2.blocks]), () => ([]))).some(s=>W(s,r));function v(e=Date.now){let r=new Map,s=new Map,t=new Map,n=[],a=0,c=o=>{let i=t.get(o);return i||(i={protocol:"1.0",id:o,title:"Example",messages:[]},t.set(o,i)),i},u=(o,i)=>{for(let l of _nullishCoalesce(r.get(o), () => ([])))l(i)};return{transport:{negotiate:async()=>({protocol:"1.0",capabilities:["plans","flows","widgets"]}),read:async o=>structuredClone(c(o)),send:async(o,i)=>{let l=JSON.stringify([i,o.clientReplyId]),b=JSON.stringify({...o,protocol:void 0}),h=s.get(l),I={id:o.clientReplyId,code:"refused"};if(h){if(h.body!==b)throw I;return h.result}let S=c(i),R="blockId"in o?S.messages.flatMap(p=>p.blocks).find(p=>W(p,o.blockId)):void 0;if(_optionalChain([R, 'optionalAccess', _3 => _3.answered]))throw I;if(o.kind==="stop"){let p=S.messages.find(A=>A.id===o.messageId&&A.role==="assistant");if(!p)throw I;p.status==="writing"&&(o.versionIndex===void 0||o.versionIndex===(_nullishCoalesce(p.versionIndex, () => (1))))&&(p.status="stopped");let P={id:`reply-${++a}`};return s.set(l,{body:b,result:P}),P}let m=n.shift();if(_optionalChain([m, 'optionalAccess', _4 => _4.problem]))throw m.problem;if(R){let p={...o};delete p.protocol,R.answered=!0,R.answer=p}o.kind==="message"&&S.messages.push({id:`message-${++a}`,clientMessageId:o.clientMessageId,role:"user",content:o.content,blocks:[],createdAt:e()}),m&&(m.content!==void 0||m.blocks!==void 0)&&S.messages.push({id:`message-${++a}`,role:"assistant",content:_nullishCoalesce(m.content, () => ("")),blocks:_nullishCoalesce(m.blocks, () => ([])),createdAt:e(),...m.writing&&{status:"writing"}});for(let p of _nullishCoalesce(_optionalChain([m, 'optionalAccess', _5 => _5.events]), () => ([])))u(i,p);let q={id:`reply-${++a}`};return s.set(l,{body:b,result:q}),q},subscribe:(o,i)=>{let l=r.get(o);return l||(l=new Set,r.set(o,l)),l.add(i),()=>{l.delete(i)}}},enqueue:o=>{n.push(o)},emit:u,activeStreams:()=>[...r.values()].reduce((o,i)=>o+i.size,0)}}function w(){let e=0;return{clock:{now:()=>Date.now(),setTimeout:(r,s)=>setTimeout(r,s),clearTimeout:r=>clearTimeout(r)},online:()=>!0,visibility:()=>!0,ids:()=>`test-${++e}`}}var _react = require('react');var _react3 = require('@testing-library/react');function K(e,r){return _react3.render.call(void 0, _react.createElement.call(void 0, _chunkA3YOSFLEcjs.w,{client:r.client,...r.skin},e))}function te(e,r={}){e.test("composer, keyboard and accessibility contract",async()=>{let s=v(),t=_chunk3KV7TDDRcjs.i.call(void 0, {accountId:"test-account",conversationId:"test-conversation",platform:w(),transport:s.transport});await t.ready;try{let n=_react3.render.call(void 0, _react.createElement.call(void 0, _chunkA3YOSFLEcjs.O,{client:t,...r})),a=n.getByRole("textbox",{name:"Message"});await _react3.act.call(void 0, async()=>{_react3.fireEvent.change(a,{target:{value:"Keep"}})}),_react3.fireEvent.keyDown(a,{key:"Enter",shiftKey:!0}),e.assert.equal(t.getSnapshot().draft,"Keep"),await _react3.act.call(void 0, async()=>{_react3.fireEvent.keyDown(a,{key:"Enter"})}),e.assert.equal(t.getSnapshot().draft,""),e.assert.ok(n.getByRole("alert"))}finally{_react3.cleanup.call(void 0, ),t.dispose()}})}function oe(e,r={}){e.test("slot, provider and widget isolation contract",async()=>{let s=v();s.enqueue({content:"Fixture",blocks:[{type:"markdown",id:"widget-a",fallback:"Readable",body:{text:"Widget"}}]});let t=_chunk3KV7TDDRcjs.i.call(void 0, {accountId:"test-account",conversationId:"test-conversation",platform:w(),transport:s.transport});await t.ready,await t.setDraft("Start"),await t.send();try{let n=_react3.render.call(void 0, _react.createElement.call(void 0, _chunkA3YOSFLEcjs.O,{client:t,...r}));await _react3.waitFor.call(void 0, ()=>{if(!n.container.querySelector('[data-part="widgetFrame"] .wp-markdown'))throw new Error("Markdown is not ready")});let a=()=>{let u=new Map;for(let f of n.container.querySelectorAll("[data-part]")){let o=f.getAttribute("data-part");if(["composer","widgetFrame","messageActions","extraPanels"].includes(o))continue;let i=f.cloneNode(!0);for(let b of i.querySelectorAll("[data-part]"))b.replaceChildren();let l=_nullishCoalesce(u.get(o), () => ([]));l.push(i.outerHTML),u.set(o,l)}return[...u]},c=a();n.rerender(_react.createElement.call(void 0, _chunkA3YOSFLEcjs.O,{client:t,...r,slots:{composer:()=>_react.createElement.call(void 0, "span",null,"Custom composer"),messageActions:()=>_react.createElement.call(void 0, "span",null,"Custom actions"),extraPanels:()=>_react.createElement.call(void 0, "span",null,"Custom panel")}})),e.assert.equal(a(),c),e.assert.ok(n.queryAllByText("Custom actions").length),n.rerender(_react.createElement.call(void 0, _chunkA3YOSFLEcjs.O,{client:t,...r,parts:{composer:()=>_react.createElement.call(void 0, "span",null,"Custom composer")}})),e.assert.equal(a(),c),n.rerender(_react.createElement.call(void 0, _chunkA3YOSFLEcjs.O,{client:t,...r,widgets:[{type:"markdown",render:()=>_react.createElement.call(void 0, "span",null,"Custom markdown")}]})),e.assert.ok(n.queryByText("Custom markdown")),e.assert.equal(a(),c)}finally{_react3.cleanup.call(void 0, ),t.dispose()}})}function re(e,r={}){let s={markdown:{text:"**Readable**"},"form.fill":{requestId:"request-a",values:{title:{value:"Draft",validation:{status:"valid",messages:[]}}}},card:{title:"Card",blocks:[]},list:{items:["Item"]},table:{columns:["Title"],rows:[["Value"]]},form:{fields:[{name:"title",label:"Title",required:!0}]},choice:{mode:"single",options:["One","Two"]},buttons:{actions:[{id:"accept",label:"Accept"}]},diff:{before:"Before",after:"After"},plan:{planId:"plan-a"},"route-link":{label:"View",route:"/items/a"},notice:{text:"Notice"}};for(let[t,n]of Object.entries(s))e.test(`widget contract: ${t}`,async()=>{let a=v(),c=_chunk3KV7TDDRcjs.i.call(void 0, {accountId:"test-account",conversationId:"test-conversation",platform:w(),transport:a.transport});await c.ready;try{let f=K(_react.createElement.call(void 0, _chunkA3YOSFLEcjs.M,{block:{type:t,id:"widget-a",fallback:"Readable",body:n},disabled:!1,onReply:()=>{}}),{client:c,skin:r});e.assert.ok(f.container.textContent)}finally{_react3.cleanup.call(void 0, ),c.dispose()}})}function ne(e,r={}){e.test("failure announcement and direction contract",async()=>{let s=v(),t=_chunk3KV7TDDRcjs.i.call(void 0, {accountId:"test-account",conversationId:"test-conversation",platform:w(),transport:s.transport});await t.ready;try{let n=_react3.render.call(void 0, _react.createElement.call(void 0, _chunkA3YOSFLEcjs.O,{client:t,...r,dir:"rtl"}));await _react3.act.call(void 0, async()=>{t.report({id:"failure-a",code:"refused"})}),e.assert.ok(n.getByRole("alert").textContent),e.assert.equal(_optionalChain([n, 'access', _6 => _6.container, 'access', _7 => _7.querySelector, 'call', _8 => _8(".waypoint"), 'optionalAccess', _9 => _9.getAttribute, 'call', _10 => _10("dir")]),"rtl");let a=t.getSnapshot().announcement;await _react3.act.call(void 0, async()=>{t.report({id:"failure-a",code:"refused"})}),e.assert.equal(t.getSnapshot().announcement,a+1)}finally{_react3.cleanup.call(void 0, ),t.dispose()}})}function ae(e,r={}){e.test("form drafts and field adornment contract",async()=>{let s=v(),t=_chunk3KV7TDDRcjs.i.call(void 0, {accountId:"form-account",conversationId:"form-chat",platform:w(),transport:s.transport});await t.ready;let n=_chunkLLPGVQUTcjs.a.call(void 0, {fields:[{name:"title",label:"Title",tier:"low",value:"Draft"}],ids:()=>"form-contract"});try{let a=K(_react.createElement.call(void 0, _chunkBRUQ75G3cjs.c,{session:n,onRegenerate:()=>{}}),{client:t,skin:r}),c=a.getByRole("textbox",{name:"Title"});_react3.act.call(void 0, ()=>{c.focus()}),e.assert.ok(a.getByRole("button",{name:"Regenerate Title"})),e.assert.ok(a.getAllByRole("status").some(f=>f.textContent==="Regenerate Title")),_react3.fireEvent.change(c,{target:{value:"Edited"}}),e.assert.equal(_optionalChain([n, 'access', _11 => _11.getSnapshot, 'call', _12 => _12(), 'access', _13 => _13.fields, 'access', _14 => _14.title, 'optionalAccess', _15 => _15.value]),"Edited");let u=n.begin({text:"Fill"});_react3.act.call(void 0, ()=>{n.receive({requestId:u.requestId,values:{title:{value:"Proposal",validation:{status:"valid",messages:[]}}}})}),e.assert.equal(c.value,"Edited"),_react3.fireEvent.click(await a.findByRole("button",{name:"Replace"})),e.assert.equal(c.value,"Proposal")}finally{_react3.cleanup.call(void 0, ),n.dispose(),t.dispose()}})}exports.CAPABILITIES = _chunkQ7BULGQGcjs.b; exports.addMessages = _chunkA3YOSFLEcjs.q; exports.catalogueFor = _chunkA3YOSFLEcjs.s; exports.createFormatter = _chunkA3YOSFLEcjs.t; exports.createScriptedTransport = v; exports.createTestPlatform = w; exports.darkTheme = _chunk4BWV3SELcjs.b; exports.defineAccessibilityContract = ne; exports.defineFormFillContract = ae; exports.defineOverrideContract = oe; exports.defineSkinContract = te; exports.defineWidgetContract = re; exports.english = _chunkA3YOSFLEcjs.p; exports.lightTheme = _chunk4BWV3SELcjs.a; exports.renderWithWaypoint = K; exports.simplifiedChinese = _chunkA3YOSFLEcjs.r; exports.themeProperties = _chunk4BWV3SELcjs.c;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { V as TransportEvent, X as TransportPort, I as PlatformPorts, W as WaypointClient } from '../form-fill-C32AHxSr.cjs';
|
|
2
|
+
export { A as AttachmentPort, B as BuiltinFlowTab, h as CAPABILITIES, i as Capability, C as ClientConfig, b as ClientSnapshot, j as ComposerContribution, k as ConversationReadError, d as ConversationReadResult, l as ConversationSource, m as ConversationSummary, D as DEFAULT_FLOW_TABS, n as FORM_SESSION_UNAVAILABLE, o as FeedbackInput, p as FeedbackPort, q as FlowSource, r as FlowSummary, s as FlowTab, t as FormDraft, u as FormField, v as FormMaterial, a as FormSession, F as FormSessionConfig, w as FormSnapshot, K as KeepChangesReview, x as KeepChangesReviewResult, y as KeepDisabledReason, M as MaybePromise, z as MessageVersionInput, E as MessageVersionPort, N as NegotiationOffer, P as PendingSend, G as PlanReadRefused, H as PlanRunner, f as ReadPorts, J as ReaderRequestContext, e as ReplyDelivery, R as ReplyInput, L as ReplyOutcome, g as ReplyState, O as RestoredMessage, Q as RunnerPorts, S as StepContext, T as StoragePort, U as ToolStep, c as WaypointEvent, Y as asProblem, Z as canTransition, _ as confirmationOf, $ as createMemoryStorage, a0 as createPlanRunner, a1 as createWaypointClient, a2 as flowTabId, a3 as loadFormSession, a4 as readProblem, a5 as replyRow, a6 as versionWalk } from '../form-fill-C32AHxSr.cjs';
|
|
3
|
+
import { B as Block, P as Problem } from '../generated-C9tbjwK0.cjs';
|
|
4
|
+
export { b as BlockButtons, c as BlockCard, d as BlockChoice, e as BlockDetails, f as BlockDiff, g as BlockForm, h as BlockFormFill, i as BlockList, j as BlockMarkdown, k as BlockNotice, l as BlockPlan, m as BlockRouteLink, n as BlockTable, C as CanonicalRequest, o as CardAnswer, p as ConfirmationItem, q as ConfirmationRecord, r as ConfirmationSchedule, s as Conversation, t as CreateConversation, D as DecisionVersions, E as ExecutionAcceptance, u as ExtensionBlock, v as FinalHistory, a as Flow, w as FlowParameter, x as FormFillField, y as FormFillRequest, z as FormFillValue, A as FormSubject, G as FormValidation, F as Frame, H as FrameBlock, I as FrameMessageEnd, J as FrameMessageStart, K as FramePlanDraft, L as FramePlanFinal, N as FramePlanStep, O as FramePlanningRead, Q as FrameProblem, S as FrameStale, U as FrameTextDelta, V as FrameToolCall, W as ImmediateDraft, X as ImmediatePlan, Y as IntentCause, Z as IntentKind, _ as IntentRequest, $ as IntentTarget, a0 as IntentWinner, a1 as KeptFact, a2 as LeaveAcknowledgment, M as ManifestEntry, a3 as Message, a4 as Negotiated, a5 as Negotiation, a6 as NegotiationOfferWire, a7 as Outcome, a8 as PatchFlow, a9 as Plan, aa as PlanCommand, ab as PlanIntentReply, ac as PlanRecord, ad as PlanState, R as ReadTemplate, ae as RecoveryPhase, af as RecoveryView, ag as Reply, ah as ReplyAction, ai as ReplyConfirm, aj as ReplyDecline, ak as ReplyForm, al as ReplyFormFill, am as ReplyMessage, an as ReplyPlanValues, ao as ReplyStop, ap as ReplyToolResult, aq as RunFlow, ar as Source, as as Step, at as TimedPlan, T as ToolCall, au as WaypointProtocol, av as WinnerKind } from '../generated-C9tbjwK0.cjs';
|
|
5
|
+
import { ReactElement } from 'react';
|
|
6
|
+
import { RenderResult } from '@testing-library/react';
|
|
7
|
+
import { WaypointProviderProps } from '../react/index.cjs';
|
|
8
|
+
export { ButtonProps, CheckboxProps, ConfirmSheet, ConversationRail, ConversationRailProps, DrawerProps, FieldAdornment, FieldAdornmentProps, FormFillFields, FormFillFieldsProps, FormFillPopup, FormFillPopupProps, InputProps, MarkdownImagePolicy, PartData, PartDataMap, PartMessage, PartName, PartPending, PartProps, PartStep, PlanDiagram, PlanDiagramProps, PrimitiveComponents, PrivateAttachmentPort, RecoveryPanel, Slots, ThemeTokensHook, WaypointChat, WaypointChatProps, WaypointProvider, Widget, WidgetProps, WidgetRegistration, defaultProblemText, isSafeUrl, partNames, standardWidgets, useComposer, useConversation, useFlows, useFormFill, usePlan, useWaypointClient } from '../react/index.cjs';
|
|
9
|
+
export { A as ActionEntry, a as ActionId, B as BlockAnswer, b as BlockReply, m as BlockSummary, K as KeepSheet, c as KeepSheetView, P as PlanView, d as PlanViewAlert, e as PlanViewInput, f as PlanViewStatus, S as StepStatus, T as Tone, V as ViewMessage, g as cardReplyId, k as keepSheet, n as noticeMessages, p as planView, u as useBlockReply, h as usePlanView, i as useWaypointMessage, j as useWaypointSelector, l as useWaypointSnapshot } from '../block-reply-r8u-DNmZ.cjs';
|
|
10
|
+
export { P as PlanLabels, s as stepName } from '../step-name-CB3d3kkP.cjs';
|
|
11
|
+
export { B as BlockCatalogue, a as BlockMessageKey, C as ConfirmationCatalogue, b as ConfirmationMessageKey, F as FormCatalogue, c as FormMessageKey, d as FormatMessage, M as MessageKey, e as MessageValues, P as ProblemCatalogue, f as ProblemMessageKey, R as RecoveryCatalogue, g as RecoveryMessageKey, h as ReplyCatalogue, i as ReplyMessageKey, k as addMessages, l as catalogueFor, m as createFormatter, j as english, s as simplifiedChinese } from '../i18n-WC9f-h_q.cjs';
|
|
12
|
+
export { RequiredThemeToken, ResolvedTheme, Theme, ThemeToken, darkTheme, lightTheme, themeProperties } from '../skin-default/index.cjs';
|
|
13
|
+
export { I as IntentAlert, b as IntentFailure, c as IntentNotice, d as IntentOverlay, a as ScopeSignal } from '../intent-BQPPows1.cjs';
|
|
14
|
+
|
|
15
|
+
interface ScriptedTurn {
|
|
16
|
+
content?: string;
|
|
17
|
+
blocks?: Block[];
|
|
18
|
+
problem?: Problem;
|
|
19
|
+
events?: TransportEvent[];
|
|
20
|
+
/** The reply is left being written, as a reply still streaming in. */
|
|
21
|
+
writing?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A scripted host. A `stop` ends the reply being written as `stopped` and
|
|
25
|
+
* keeps its text; a stop that names another version changes nothing. Like a real one it deduplicates by reply id, refuses the
|
|
26
|
+
* same id with another body, and holds one answer per card: the first
|
|
27
|
+
* answer it applies marks the card answered and names it in the card's
|
|
28
|
+
* `answer`, and any other answer to that card is refused (`refused`).
|
|
29
|
+
*/
|
|
30
|
+
declare function createScriptedTransport(now?: () => number): {
|
|
31
|
+
transport: TransportPort;
|
|
32
|
+
enqueue: (turn: ScriptedTurn) => void;
|
|
33
|
+
emit: (id: string, event: TransportEvent) => void;
|
|
34
|
+
activeStreams: () => number;
|
|
35
|
+
};
|
|
36
|
+
declare function createTestPlatform(): PlatformPorts;
|
|
37
|
+
|
|
38
|
+
type Skin = Partial<Omit<WaypointProviderProps, "client" | "children">>;
|
|
39
|
+
declare function renderWithWaypoint(element: ReactElement, options: {
|
|
40
|
+
client: WaypointClient;
|
|
41
|
+
skin?: Skin;
|
|
42
|
+
}): RenderResult;
|
|
43
|
+
interface ContractAssertions {
|
|
44
|
+
equal(actual: unknown, expected: unknown): void;
|
|
45
|
+
ok(value: unknown): void;
|
|
46
|
+
}
|
|
47
|
+
interface ContractSuite {
|
|
48
|
+
test(name: string, run: () => Promise<void>): void;
|
|
49
|
+
assert: ContractAssertions;
|
|
50
|
+
}
|
|
51
|
+
declare function defineSkinContract(suite: ContractSuite, skin?: Skin): void;
|
|
52
|
+
declare function defineOverrideContract(suite: ContractSuite, skin?: Skin): void;
|
|
53
|
+
declare function defineWidgetContract(suite: ContractSuite, skin?: Skin): void;
|
|
54
|
+
declare function defineAccessibilityContract(suite: ContractSuite, skin?: Skin): void;
|
|
55
|
+
declare function defineFormFillContract(suite: ContractSuite, skin?: Skin): void;
|
|
56
|
+
|
|
57
|
+
export { Block, type ContractAssertions, type ContractSuite, PlatformPorts, Problem, type ScriptedTurn, type Skin, TransportEvent, TransportPort, WaypointClient, WaypointProviderProps, createScriptedTransport, createTestPlatform, defineAccessibilityContract, defineFormFillContract, defineOverrideContract, defineSkinContract, defineWidgetContract, renderWithWaypoint };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { V as TransportEvent, X as TransportPort, I as PlatformPorts, W as WaypointClient } from '../form-fill-Dp3ut1_Y.js';
|
|
2
|
+
export { A as AttachmentPort, B as BuiltinFlowTab, h as CAPABILITIES, i as Capability, C as ClientConfig, b as ClientSnapshot, j as ComposerContribution, k as ConversationReadError, d as ConversationReadResult, l as ConversationSource, m as ConversationSummary, D as DEFAULT_FLOW_TABS, n as FORM_SESSION_UNAVAILABLE, o as FeedbackInput, p as FeedbackPort, q as FlowSource, r as FlowSummary, s as FlowTab, t as FormDraft, u as FormField, v as FormMaterial, a as FormSession, F as FormSessionConfig, w as FormSnapshot, K as KeepChangesReview, x as KeepChangesReviewResult, y as KeepDisabledReason, M as MaybePromise, z as MessageVersionInput, E as MessageVersionPort, N as NegotiationOffer, P as PendingSend, G as PlanReadRefused, H as PlanRunner, f as ReadPorts, J as ReaderRequestContext, e as ReplyDelivery, R as ReplyInput, L as ReplyOutcome, g as ReplyState, O as RestoredMessage, Q as RunnerPorts, S as StepContext, T as StoragePort, U as ToolStep, c as WaypointEvent, Y as asProblem, Z as canTransition, _ as confirmationOf, $ as createMemoryStorage, a0 as createPlanRunner, a1 as createWaypointClient, a2 as flowTabId, a3 as loadFormSession, a4 as readProblem, a5 as replyRow, a6 as versionWalk } from '../form-fill-Dp3ut1_Y.js';
|
|
3
|
+
import { B as Block, P as Problem } from '../generated-C9tbjwK0.js';
|
|
4
|
+
export { b as BlockButtons, c as BlockCard, d as BlockChoice, e as BlockDetails, f as BlockDiff, g as BlockForm, h as BlockFormFill, i as BlockList, j as BlockMarkdown, k as BlockNotice, l as BlockPlan, m as BlockRouteLink, n as BlockTable, C as CanonicalRequest, o as CardAnswer, p as ConfirmationItem, q as ConfirmationRecord, r as ConfirmationSchedule, s as Conversation, t as CreateConversation, D as DecisionVersions, E as ExecutionAcceptance, u as ExtensionBlock, v as FinalHistory, a as Flow, w as FlowParameter, x as FormFillField, y as FormFillRequest, z as FormFillValue, A as FormSubject, G as FormValidation, F as Frame, H as FrameBlock, I as FrameMessageEnd, J as FrameMessageStart, K as FramePlanDraft, L as FramePlanFinal, N as FramePlanStep, O as FramePlanningRead, Q as FrameProblem, S as FrameStale, U as FrameTextDelta, V as FrameToolCall, W as ImmediateDraft, X as ImmediatePlan, Y as IntentCause, Z as IntentKind, _ as IntentRequest, $ as IntentTarget, a0 as IntentWinner, a1 as KeptFact, a2 as LeaveAcknowledgment, M as ManifestEntry, a3 as Message, a4 as Negotiated, a5 as Negotiation, a6 as NegotiationOfferWire, a7 as Outcome, a8 as PatchFlow, a9 as Plan, aa as PlanCommand, ab as PlanIntentReply, ac as PlanRecord, ad as PlanState, R as ReadTemplate, ae as RecoveryPhase, af as RecoveryView, ag as Reply, ah as ReplyAction, ai as ReplyConfirm, aj as ReplyDecline, ak as ReplyForm, al as ReplyFormFill, am as ReplyMessage, an as ReplyPlanValues, ao as ReplyStop, ap as ReplyToolResult, aq as RunFlow, ar as Source, as as Step, at as TimedPlan, T as ToolCall, au as WaypointProtocol, av as WinnerKind } from '../generated-C9tbjwK0.js';
|
|
5
|
+
import { ReactElement } from 'react';
|
|
6
|
+
import { RenderResult } from '@testing-library/react';
|
|
7
|
+
import { WaypointProviderProps } from '../react/index.js';
|
|
8
|
+
export { ButtonProps, CheckboxProps, ConfirmSheet, ConversationRail, ConversationRailProps, DrawerProps, FieldAdornment, FieldAdornmentProps, FormFillFields, FormFillFieldsProps, FormFillPopup, FormFillPopupProps, InputProps, MarkdownImagePolicy, PartData, PartDataMap, PartMessage, PartName, PartPending, PartProps, PartStep, PlanDiagram, PlanDiagramProps, PrimitiveComponents, PrivateAttachmentPort, RecoveryPanel, Slots, ThemeTokensHook, WaypointChat, WaypointChatProps, WaypointProvider, Widget, WidgetProps, WidgetRegistration, defaultProblemText, isSafeUrl, partNames, standardWidgets, useComposer, useConversation, useFlows, useFormFill, usePlan, useWaypointClient } from '../react/index.js';
|
|
9
|
+
export { A as ActionEntry, a as ActionId, B as BlockAnswer, b as BlockReply, m as BlockSummary, K as KeepSheet, c as KeepSheetView, P as PlanView, d as PlanViewAlert, e as PlanViewInput, f as PlanViewStatus, S as StepStatus, T as Tone, V as ViewMessage, g as cardReplyId, k as keepSheet, n as noticeMessages, p as planView, u as useBlockReply, h as usePlanView, i as useWaypointMessage, j as useWaypointSelector, l as useWaypointSnapshot } from '../block-reply-z8El20nD.js';
|
|
10
|
+
export { P as PlanLabels, s as stepName } from '../step-name-BlgVvshf.js';
|
|
11
|
+
export { B as BlockCatalogue, a as BlockMessageKey, C as ConfirmationCatalogue, b as ConfirmationMessageKey, F as FormCatalogue, c as FormMessageKey, d as FormatMessage, M as MessageKey, e as MessageValues, P as ProblemCatalogue, f as ProblemMessageKey, R as RecoveryCatalogue, g as RecoveryMessageKey, h as ReplyCatalogue, i as ReplyMessageKey, k as addMessages, l as catalogueFor, m as createFormatter, j as english, s as simplifiedChinese } from '../i18n-WC9f-h_q.js';
|
|
12
|
+
export { RequiredThemeToken, ResolvedTheme, Theme, ThemeToken, darkTheme, lightTheme, themeProperties } from '../skin-default/index.js';
|
|
13
|
+
export { I as IntentAlert, b as IntentFailure, c as IntentNotice, d as IntentOverlay, a as ScopeSignal } from '../intent-BuvqEU3Q.js';
|
|
14
|
+
|
|
15
|
+
interface ScriptedTurn {
|
|
16
|
+
content?: string;
|
|
17
|
+
blocks?: Block[];
|
|
18
|
+
problem?: Problem;
|
|
19
|
+
events?: TransportEvent[];
|
|
20
|
+
/** The reply is left being written, as a reply still streaming in. */
|
|
21
|
+
writing?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A scripted host. A `stop` ends the reply being written as `stopped` and
|
|
25
|
+
* keeps its text; a stop that names another version changes nothing. Like a real one it deduplicates by reply id, refuses the
|
|
26
|
+
* same id with another body, and holds one answer per card: the first
|
|
27
|
+
* answer it applies marks the card answered and names it in the card's
|
|
28
|
+
* `answer`, and any other answer to that card is refused (`refused`).
|
|
29
|
+
*/
|
|
30
|
+
declare function createScriptedTransport(now?: () => number): {
|
|
31
|
+
transport: TransportPort;
|
|
32
|
+
enqueue: (turn: ScriptedTurn) => void;
|
|
33
|
+
emit: (id: string, event: TransportEvent) => void;
|
|
34
|
+
activeStreams: () => number;
|
|
35
|
+
};
|
|
36
|
+
declare function createTestPlatform(): PlatformPorts;
|
|
37
|
+
|
|
38
|
+
type Skin = Partial<Omit<WaypointProviderProps, "client" | "children">>;
|
|
39
|
+
declare function renderWithWaypoint(element: ReactElement, options: {
|
|
40
|
+
client: WaypointClient;
|
|
41
|
+
skin?: Skin;
|
|
42
|
+
}): RenderResult;
|
|
43
|
+
interface ContractAssertions {
|
|
44
|
+
equal(actual: unknown, expected: unknown): void;
|
|
45
|
+
ok(value: unknown): void;
|
|
46
|
+
}
|
|
47
|
+
interface ContractSuite {
|
|
48
|
+
test(name: string, run: () => Promise<void>): void;
|
|
49
|
+
assert: ContractAssertions;
|
|
50
|
+
}
|
|
51
|
+
declare function defineSkinContract(suite: ContractSuite, skin?: Skin): void;
|
|
52
|
+
declare function defineOverrideContract(suite: ContractSuite, skin?: Skin): void;
|
|
53
|
+
declare function defineWidgetContract(suite: ContractSuite, skin?: Skin): void;
|
|
54
|
+
declare function defineAccessibilityContract(suite: ContractSuite, skin?: Skin): void;
|
|
55
|
+
declare function defineFormFillContract(suite: ContractSuite, skin?: Skin): void;
|
|
56
|
+
|
|
57
|
+
export { Block, type ContractAssertions, type ContractSuite, PlatformPorts, Problem, type ScriptedTurn, type Skin, TransportEvent, TransportPort, WaypointClient, WaypointProviderProps, createScriptedTransport, createTestPlatform, defineAccessibilityContract, defineFormFillContract, defineOverrideContract, defineSkinContract, defineWidgetContract, renderWithWaypoint };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{c as F}from"../chunk-5Q3INHKA.js";import{M,O as y,p as H,q as J,r as V,s as j,t as z,w as E}from"../chunk-IPI4O4LP.js";import"../chunk-FQSWQLRW.js";import"../chunk-OWT55N6J.js";import"../chunk-QFBAOEPD.js";import{a as $,b as L,c as N}from"../chunk-RZZDQWC2.js";import"../chunk-M63QOANE.js";import{a as B}from"../chunk-TCFCSGQ3.js";import{i as g}from"../chunk-L4PFFRDE.js";import{b as D}from"../chunk-T5XF2R74.js";import"../chunk-K73VV7KA.js";import"../chunk-5TBO732O.js";var W=(e,r)=>e.id===r||(e.body?.blocks??[]).some(s=>W(s,r));function v(e=Date.now){let r=new Map,s=new Map,t=new Map,n=[],a=0,c=o=>{let i=t.get(o);return i||(i={protocol:"1.0",id:o,title:"Example",messages:[]},t.set(o,i)),i},u=(o,i)=>{for(let l of r.get(o)??[])l(i)};return{transport:{negotiate:async()=>({protocol:"1.0",capabilities:["plans","flows","widgets"]}),read:async o=>structuredClone(c(o)),send:async(o,i)=>{let l=JSON.stringify([i,o.clientReplyId]),b=JSON.stringify({...o,protocol:void 0}),h=s.get(l),I={id:o.clientReplyId,code:"refused"};if(h){if(h.body!==b)throw I;return h.result}let S=c(i),R="blockId"in o?S.messages.flatMap(p=>p.blocks).find(p=>W(p,o.blockId)):void 0;if(R?.answered)throw I;if(o.kind==="stop"){let p=S.messages.find(A=>A.id===o.messageId&&A.role==="assistant");if(!p)throw I;p.status==="writing"&&(o.versionIndex===void 0||o.versionIndex===(p.versionIndex??1))&&(p.status="stopped");let P={id:`reply-${++a}`};return s.set(l,{body:b,result:P}),P}let m=n.shift();if(m?.problem)throw m.problem;if(R){let p={...o};delete p.protocol,R.answered=!0,R.answer=p}o.kind==="message"&&S.messages.push({id:`message-${++a}`,clientMessageId:o.clientMessageId,role:"user",content:o.content,blocks:[],createdAt:e()}),m&&(m.content!==void 0||m.blocks!==void 0)&&S.messages.push({id:`message-${++a}`,role:"assistant",content:m.content??"",blocks:m.blocks??[],createdAt:e(),...m.writing&&{status:"writing"}});for(let p of m?.events??[])u(i,p);let q={id:`reply-${++a}`};return s.set(l,{body:b,result:q}),q},subscribe:(o,i)=>{let l=r.get(o);return l||(l=new Set,r.set(o,l)),l.add(i),()=>{l.delete(i)}}},enqueue:o=>{n.push(o)},emit:u,activeStreams:()=>[...r.values()].reduce((o,i)=>o+i.size,0)}}function w(){let e=0;return{clock:{now:()=>Date.now(),setTimeout:(r,s)=>setTimeout(r,s),clearTimeout:r=>clearTimeout(r)},online:()=>!0,visibility:()=>!0,ids:()=>`test-${++e}`}}import{createElement as d}from"react";import{render as T,fireEvent as k,act as x,cleanup as C,waitFor as O}from"@testing-library/react";function K(e,r){return T(d(E,{client:r.client,...r.skin},e))}function te(e,r={}){e.test("composer, keyboard and accessibility contract",async()=>{let s=v(),t=g({accountId:"test-account",conversationId:"test-conversation",platform:w(),transport:s.transport});await t.ready;try{let n=T(d(y,{client:t,...r})),a=n.getByRole("textbox",{name:"Message"});await x(async()=>{k.change(a,{target:{value:"Keep"}})}),k.keyDown(a,{key:"Enter",shiftKey:!0}),e.assert.equal(t.getSnapshot().draft,"Keep"),await x(async()=>{k.keyDown(a,{key:"Enter"})}),e.assert.equal(t.getSnapshot().draft,""),e.assert.ok(n.getByRole("alert"))}finally{C(),t.dispose()}})}function oe(e,r={}){e.test("slot, provider and widget isolation contract",async()=>{let s=v();s.enqueue({content:"Fixture",blocks:[{type:"markdown",id:"widget-a",fallback:"Readable",body:{text:"Widget"}}]});let t=g({accountId:"test-account",conversationId:"test-conversation",platform:w(),transport:s.transport});await t.ready,await t.setDraft("Start"),await t.send();try{let n=T(d(y,{client:t,...r}));await O(()=>{if(!n.container.querySelector('[data-part="widgetFrame"] .wp-markdown'))throw new Error("Markdown is not ready")});let a=()=>{let u=new Map;for(let f of n.container.querySelectorAll("[data-part]")){let o=f.getAttribute("data-part");if(["composer","widgetFrame","messageActions","extraPanels"].includes(o))continue;let i=f.cloneNode(!0);for(let b of i.querySelectorAll("[data-part]"))b.replaceChildren();let l=u.get(o)??[];l.push(i.outerHTML),u.set(o,l)}return[...u]},c=a();n.rerender(d(y,{client:t,...r,slots:{composer:()=>d("span",null,"Custom composer"),messageActions:()=>d("span",null,"Custom actions"),extraPanels:()=>d("span",null,"Custom panel")}})),e.assert.equal(a(),c),e.assert.ok(n.queryAllByText("Custom actions").length),n.rerender(d(y,{client:t,...r,parts:{composer:()=>d("span",null,"Custom composer")}})),e.assert.equal(a(),c),n.rerender(d(y,{client:t,...r,widgets:[{type:"markdown",render:()=>d("span",null,"Custom markdown")}]})),e.assert.ok(n.queryByText("Custom markdown")),e.assert.equal(a(),c)}finally{C(),t.dispose()}})}function re(e,r={}){let s={markdown:{text:"**Readable**"},"form.fill":{requestId:"request-a",values:{title:{value:"Draft",validation:{status:"valid",messages:[]}}}},card:{title:"Card",blocks:[]},list:{items:["Item"]},table:{columns:["Title"],rows:[["Value"]]},form:{fields:[{name:"title",label:"Title",required:!0}]},choice:{mode:"single",options:["One","Two"]},buttons:{actions:[{id:"accept",label:"Accept"}]},diff:{before:"Before",after:"After"},plan:{planId:"plan-a"},"route-link":{label:"View",route:"/items/a"},notice:{text:"Notice"}};for(let[t,n]of Object.entries(s))e.test(`widget contract: ${t}`,async()=>{let a=v(),c=g({accountId:"test-account",conversationId:"test-conversation",platform:w(),transport:a.transport});await c.ready;try{let f=K(d(M,{block:{type:t,id:"widget-a",fallback:"Readable",body:n},disabled:!1,onReply:()=>{}}),{client:c,skin:r});e.assert.ok(f.container.textContent)}finally{C(),c.dispose()}})}function ne(e,r={}){e.test("failure announcement and direction contract",async()=>{let s=v(),t=g({accountId:"test-account",conversationId:"test-conversation",platform:w(),transport:s.transport});await t.ready;try{let n=T(d(y,{client:t,...r,dir:"rtl"}));await x(async()=>{t.report({id:"failure-a",code:"refused"})}),e.assert.ok(n.getByRole("alert").textContent),e.assert.equal(n.container.querySelector(".waypoint")?.getAttribute("dir"),"rtl");let a=t.getSnapshot().announcement;await x(async()=>{t.report({id:"failure-a",code:"refused"})}),e.assert.equal(t.getSnapshot().announcement,a+1)}finally{C(),t.dispose()}})}function ae(e,r={}){e.test("form drafts and field adornment contract",async()=>{let s=v(),t=g({accountId:"form-account",conversationId:"form-chat",platform:w(),transport:s.transport});await t.ready;let n=B({fields:[{name:"title",label:"Title",tier:"low",value:"Draft"}],ids:()=>"form-contract"});try{let a=K(d(F,{session:n,onRegenerate:()=>{}}),{client:t,skin:r}),c=a.getByRole("textbox",{name:"Title"});x(()=>{c.focus()}),e.assert.ok(a.getByRole("button",{name:"Regenerate Title"})),e.assert.ok(a.getAllByRole("status").some(f=>f.textContent==="Regenerate Title")),k.change(c,{target:{value:"Edited"}}),e.assert.equal(n.getSnapshot().fields.title?.value,"Edited");let u=n.begin({text:"Fill"});x(()=>{n.receive({requestId:u.requestId,values:{title:{value:"Proposal",validation:{status:"valid",messages:[]}}}})}),e.assert.equal(c.value,"Edited"),k.click(await a.findByRole("button",{name:"Replace"})),e.assert.equal(c.value,"Proposal")}finally{C(),n.dispose(),t.dispose()}})}export{D as CAPABILITIES,J as addMessages,j as catalogueFor,z as createFormatter,v as createScriptedTransport,w as createTestPlatform,L as darkTheme,ne as defineAccessibilityContract,ae as defineFormFillContract,oe as defineOverrideContract,te as defineSkinContract,re as defineWidgetContract,H as english,$ as lightTheme,K as renderWithWaypoint,V as simplifiedChinese,N as themeProperties};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// A host's message version controls, as `docs/message-versions.md`
|
|
2
|
+
// describes. A host imports from "@appsolutely-io/waypoint" and
|
|
3
|
+
// "@appsolutely-io/waypoint/react"; this copy imports the sources so the
|
|
4
|
+
// repository's tests run it as written.
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { versionWalk } from "../../src/core/client";
|
|
7
|
+
import { useWaypointClient, type PartProps } from "../../src/react/ui";
|
|
8
|
+
|
|
9
|
+
interface ShownMessage {
|
|
10
|
+
id: string;
|
|
11
|
+
role: "user" | "assistant";
|
|
12
|
+
status?: string;
|
|
13
|
+
versionIndex?: number | null;
|
|
14
|
+
versionCount?: number | null;
|
|
15
|
+
}
|
|
16
|
+
/** The `messageActions` part: Previous / Next version, Regenerate, Edit. */
|
|
17
|
+
export function MessageVersionActions({ data }: PartProps) {
|
|
18
|
+
const client = useWaypointClient();
|
|
19
|
+
const message = (data as { message?: ShownMessage } | undefined)?.message;
|
|
20
|
+
// One call at a time from these buttons. The client also sends one
|
|
21
|
+
// request (one stable clientReplyId) for identical calls in flight.
|
|
22
|
+
const [busy, setBusy] = useState(false);
|
|
23
|
+
if (!message) return null;
|
|
24
|
+
const walk = versionWalk(message);
|
|
25
|
+
// A reply still being written has nothing to act on yet.
|
|
26
|
+
const unavailable = busy || message.status === "writing";
|
|
27
|
+
const act = (call: () => Promise<void>) => {
|
|
28
|
+
if (unavailable) return;
|
|
29
|
+
setBusy(true);
|
|
30
|
+
// The client reports a failed call itself; this only ends the wait.
|
|
31
|
+
void call()
|
|
32
|
+
.catch(() => {})
|
|
33
|
+
.finally(() => setBusy(false));
|
|
34
|
+
};
|
|
35
|
+
// Unavailable buttons stay focusable, so a press never drops focus.
|
|
36
|
+
const action = (label: string, off: boolean, run: () => Promise<void>) => (
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
aria-disabled={off || undefined}
|
|
40
|
+
onClick={() => {
|
|
41
|
+
if (!off) act(run);
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{label}
|
|
45
|
+
</button>
|
|
46
|
+
);
|
|
47
|
+
return (
|
|
48
|
+
<div role="group" aria-label="Message versions">
|
|
49
|
+
{walk ? (
|
|
50
|
+
<>
|
|
51
|
+
{action(
|
|
52
|
+
"Previous version",
|
|
53
|
+
unavailable || walk.previous === null,
|
|
54
|
+
() => client.selectVersion(message.id, walk.previous!),
|
|
55
|
+
)}
|
|
56
|
+
<span>
|
|
57
|
+
{walk.index} / {walk.count}
|
|
58
|
+
</span>
|
|
59
|
+
{action("Next version", unavailable || walk.next === null, () =>
|
|
60
|
+
client.selectVersion(message.id, walk.next!),
|
|
61
|
+
)}
|
|
62
|
+
</>
|
|
63
|
+
) : null}
|
|
64
|
+
{message.role === "assistant"
|
|
65
|
+
? action("Regenerate", unavailable, () => client.regenerate(message.id))
|
|
66
|
+
: // Only a sent message is edited (a queued send is a pending row,
|
|
67
|
+
// which gets no actions, and is withdrawn instead).
|
|
68
|
+
action("Edit", unavailable, async () => {
|
|
69
|
+
const content = window.prompt("Edit message");
|
|
70
|
+
if (content) await client.editMessage(message.id, content);
|
|
71
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Form-bound conversations
|
|
2
|
+
|
|
3
|
+
Form fill is an additive protocol 1.1 capability named `form-fill`. Negotiate with `supported: ["1.1", "1.0"]`; the peer selects a mutually supported version and capabilities. A 1.0 peer retains ordinary conversation behavior and does not enable form fill. The JSON Schema export defines the wire types; generated TypeScript is derived from it.
|
|
4
|
+
|
|
5
|
+
## Creating and reopening
|
|
6
|
+
|
|
7
|
+
`POST /conversations` accepts the existing title and protocol plus `mode: "form-fill"`, `clientCreationId` and `subject`. Retry the same creation with the same identity; the peer returns the same conversation. A subject has `key`, `resource`, `label`, `route` and `state`. State is `pending`, `bound` or `unlinked`; bound subjects additionally have `record`, an opaque record reference. Each unsaved form instance has its own pending key. Labels and routes come from a host. Clients do not infer record routes or bind subjects themselves.
|
|
8
|
+
|
|
9
|
+
A child adds `parentConversationId` and `field`. The peer verifies the parent belongs to the same authorized subject, inherits its subject and form mode, and refuses a mismatched subject. Each regeneration creates a child under the root conversation; retrying a failed creation reuses its identity. Creation attempts are scoped to the client session and parent/field, so closing and conditionally unmounting the popup does not lose that identity. A definitive not-found or gone selection result for that child evicts the cached attempt and permits a fresh creation identity. A successful read applied during selection takes precedence over a transient failed refetch. A newer matching 404/410 clears the loaded conversation and resolves missing. Each client targets at most 100 retained attempts, pruning the oldest settled entries first. Entries with creation, selection or send operations in flight are never pruned. If all entries are active, the cache may temporarily exceed 100 until operations settle; pruning runs again after each settlement. This capacity limit is the exception to retaining abandoned, settled creation identities. Network failures, superseded reads and other uncertain outcomes retain the attempt; Generate retries the same child read without creating a duplicate. If that child is already authoritatively loaded and its stream is open with no current conversation-read problem, Generate uses it without selecting or subscribing again. Selection resets stream status to unsubscribed until the new subscription reports a status. An unsubscribed, unavailable, connecting or reconnecting stream takes the full selection path to resubscribe. `GET /conversations/:id` retains mode, subject, parent and field metadata. `GET /conversations?subject=<encoded-key>` returns only conversations the viewer may see for that subject. Without the filter it returns the viewer's conversation summaries. Authorization and pagination policy belong to a host; the optional `ConversationSource` can adapt a different transport or pagination scheme to the array interface.
|
|
10
|
+
|
|
11
|
+
A form conversation supports form-fill requests only. It never grants general read tools, plan execution, flow execution or writes. The core refuses these operations in form mode, including on reopening. Until the first successful conversation read, mode is unknown: tool and plan frames are held without executing, and direct plan/flow execution and plan installation are refused. A failed read surfaces an error and keeps frames held. Reaching 128 held frames stops the local conversation: the client unsubscribes, discards all held frames and reports one `unavailable` problem with id `conversation-unavailable`. It retains no dropped call ids and sends no tool replies. The snapshot exposes `conversationUnavailable`. While it is true, the UI disables the composer, send and retry controls, explains that local unavailability does not stop a server turn and offers Reload conversation even if a later problem replaces the original notice. Late frames and reads cannot restart the stopped session. An explicit `refresh()` or `switchConversation(id)` starts a new subscription with sequence tracking reset; normal read-confirmed mode rules apply again. Reloading the same id preserves the in-memory draft and pending rows, including without storage or when storage fails. Interrupted sends become in-doubt with their original identities so an unacknowledged row remains retryable; an authoritative read removes acknowledged rows. A failed recovery read keeps the unavailable controls disabled; a successful read clears the restriction. Before the bound is reached, a later successful ordinary read starts the held frames in order once, without waiting for earlier host operations to finish; a form read reports each held frame as refused and discards it. Conversation switches and disposal discard held frames. Stopping a plan remains available in every mode. Concurrent plan frame reads use the latest frame result, so a slow draft cannot overwrite a newer final plan. Every `switchConversation(id)` starts with unknown mode and waits for read confirmation; caller metadata does not authorize frame execution or refusal. A host must enforce the same restrictions on its server. Subject metadata is descriptive and grants no authority.
|
|
12
|
+
|
|
13
|
+
`refresh()` and `switchConversation(id)` return a `ConversationReadResult` for the requested id: `loaded` includes the authoritative conversation, `missing` includes a definitive 404/410, and `unavailable` or `superseded` retains uncertainty. When another read supersedes one in the same scope and conversation, callers follow the newest read through its settled result. If an authoritative success was applied during that caller’s operation, a transient failed overlapping refetch returns that loaded outcome instead; a previously cached conversation alone does not override a fresh failure. A newer matching 404/410 takes precedence over earlier successes, clears the conversation and sets conversationUnavailable. Send, retry and reply are disabled until a successful read clears it; the UI shows the reason and Reload conversation. Missing reads recover through a normal refresh; stopped streams additionally require a new subscription. If an older read returns 404/410 after a newer read has started, the older failure is superseded even when the newer read fails transiently. Any previously loaded snapshot is retained in that ordering; the conversation-read problem forces Generate through switchConversation to read the child again before sending. Switching scope or disposing still yields `superseded`. The first successful read may establish previously unknown mode while a newer read is pending. The popup uses this settled outcome instead of inferring disappearance from a null snapshot. The HTTP transport preserves failed read status/id in `ConversationReadError`; custom transports can use the same error type. Only a matching requested id and current 404/410 result proves absence. A stale response or an unrelated id cannot evict the attempt. Existing callers may await these methods and ignore their result.
|
|
14
|
+
|
|
15
|
+
## Request and result
|
|
16
|
+
|
|
17
|
+
Send to `POST /conversations/:id/replies`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"protocol": "1.1",
|
|
22
|
+
"kind": "form-fill",
|
|
23
|
+
"clientReplyId": "fill-request-a",
|
|
24
|
+
"request": {
|
|
25
|
+
"requestId": "fill-request-a",
|
|
26
|
+
"text": "Draft a title from this material",
|
|
27
|
+
"references": ["https://example.test/reference"],
|
|
28
|
+
"attachments": ["private-attachment-a"],
|
|
29
|
+
"fields": [
|
|
30
|
+
{ "name": "title", "label": "Title", "tier": "low", "value": "" }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The client projects only configured low-tier fields on every initial fill and child request. A child request also includes `field` and only that field's descriptor. A host independently checks the fields against its own allowlist before sending material to a model; caller-supplied tiers are not authority. Reference strings are material, never fetched by the library. Attachments are opaque private references returned by an injected adapter. Image bytes do not enter the JSON message. Retry the same request with the same `clientReplyId`; the peer deduplicates it.
|
|
37
|
+
|
|
38
|
+
The reply is an ordinary assistant message containing this block, delivered by the existing `block` frame or authoritative conversation read:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"type": "form.fill",
|
|
43
|
+
"id": "fill-result-a",
|
|
44
|
+
"fallback": "Draft title available",
|
|
45
|
+
"body": {
|
|
46
|
+
"requestId": "fill-request-a",
|
|
47
|
+
"values": {
|
|
48
|
+
"title": {
|
|
49
|
+
"value": "A complete proposed title",
|
|
50
|
+
"validation": {
|
|
51
|
+
"status": "invalid",
|
|
52
|
+
"messages": ["Review this title before saving"],
|
|
53
|
+
"guidance": "Prefer a shorter title"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`values` is a map: exactly one entry per requested field, with no extra or missing fields. Each value may include a display `label` copied from the requested field descriptor; peers should include it so transcript widgets show a human-readable field name. Older results without a label fall back to the key. Each value has a validation status and messages; guidance is optional and nonblocking. A host validates structured model output before emitting it. The form session checks field membership, completeness and validation shape again. Invalid values remain editable drafts and guidance never truncates text. An unknown extension block still uses the existing readable fallback; malformed `form.fill` is not an extension.
|
|
62
|
+
|
|
63
|
+
## Local drafts and replacement
|
|
64
|
+
|
|
65
|
+
`await loadFormSession({ fields, ids })` creates the session; its code is loaded on first use, outside the core bundle, and a failed load rejects with `FORM_SESSION_UNAVAILABLE` (`retryable`, id `form-session-load`) without keeping anything, so calling again retries. The session owns local form state. It has no storage or save port. Supply JSON-compatible values and globally unique request ids. Undefined initial or edited field values are normalized to null. `begin(material)` snapshots field revisions and projects low-tier descriptors. `receive(body)` accepts only the latest matching request once. New fills supersede older requests. The snapshot exposes `pending` until a matching result, cancellation or disposal; regeneration controls are disabled during that interval. `edit` records user changes, not hydration, and `getSnapshot`/`subscribe` expose editable values and validation. Treat snapshots as read-only.
|
|
66
|
+
|
|
67
|
+
Untouched fields accept a generated value. Fields edited since initialization or the previous accepted generated value retain the edit and expose a replacement proposal (`replacement`, with the fill that made it). An entry that does not parse as the field's type yet (an emptied or partly typed number) is recorded with `markEdited(field, text)`: it counts as an edit, keeps the last valid `value`, and sets `localInvalid` with the entered `localText`, so a fill over it becomes a proposal and a host's own submit can refuse while it is set. `replace(field, revision)` succeeds only for the revision shown in the prompt; a newer edit requires another proposal. Cancel retains the edit. Explicit acceptance resets the edited marker so later regeneration can replace that accepted draft. Missing, extra, stale and duplicate results are ignored; a malformed result for the pending request does not end it by itself, and `failRequest(requestId)` ends it (the popup does so and offers a Retry that sends a new request). The session keeps the last twenty applied fills in `fills` (those that still own a field): `revert(fill)` restores, for each field that fill still owns (still at the revision it set), the value and validation from before it, with a new revision and `edited: false`, and withdraws the proposals that fill made; it is refused while a request is pending, and later edits, accepted proposals and newer fills are never undone. Keep one session per open form instance. Call `dispose` when leaving the form; it clears drafts. Closing the material popup cancels its pending local fill, including an accepted request whose result has not arrived. Late results do not change fields or raise an error; they remain visible in the conversation transcript. The server is not notified and may finish generation. Existing form drafts are retained. A pending-fill hint explains this before closing.
|
|
68
|
+
|
|
69
|
+
Pass the session as optional `formSession` to `createWaypointClient` to receive matching streamed/refetched blocks and make the ordinary composer send projected form requests. Otherwise use `FormFillPopup` with an explicit session. The popup listens to authoritative conversation results. Images may upload while a draft request is pending. Remove stays refused while the request is pending, including while a failed submission offers Retry; the draft arriving, closing the popup or ending the request releases it. Transport acceptance or submission failure alone does not release it. Generate is disabled while a fill is pending, including after transport acceptance; completion or local cancellation ends that state. A failed submission exposes a separate Retry action that resends the original cached material and request identity in the same conversation; it does not generate a replacement request. Do not reuse a session for unrelated form subjects. A host saves session values only through its own explicit normal form Save. Nothing in receiving, displaying or replacing a draft saves a record.
|
|
70
|
+
|
|
71
|
+
## React and skins
|
|
72
|
+
|
|
73
|
+
Render `FormFillPopup` within `WaypointProvider`, supplying a session, `open` and `onClose`. It expects an already-created form conversation; create that through `ConversationSource.create` or `createHttpConversationSource`, then select it on the client. For regeneration supply `field` and `source`; the popup creates and selects a child conversation before requesting the field. The popup's Message and Reference links fields are the skin's `textarea` primitive. A private `attachments` adapter implements `upload(File): Promise<string>` and `remove(reference): Promise<void>`. It owns authentication, private persistence and retention. Images come from paste, an "Attach images" picker or a drop on the popup's fields; each is tried in turn, listed by its local file name (never sent; the reference stays opaque, and a repeated name gets an ordinal) with its own Remove, and every outcome is said in the popup's status. Without that adapter the popup says images cannot be attached here, and a dropped file never reaches the page. No public preview is loaded and no media-library write is performed. The adapter and a host own limits, image validation, cancellation/cleanup failures and eventual retention cleanup.
|
|
74
|
+
|
|
75
|
+
`FormFillFields` exposes default editable controls (text, numbers as number inputs, booleans through the optional `checkbox` primitive; other values are shown read-only with a hint to edit them in the form) and a `renderField` adapter for host-specific controls and structured values. It renders no element of its own around the fields. A proposal shows its value in plain words and its own validation, with Replace and Keep mine; typing in the field keeps the typing and withdraws the proposal. Invalid validation is drawn in the danger tone after the word "Invalid". The popup says "Generating draft…" once the host accepted the request and one combined "Draft ready" line when that request was filled, and lists "Revert this fill" for each fill that still owns a field. Suggestions, reverts, the popup's status, generation and attachments load as one lazy chunk with the first form; until it loads, or after it failed (with a Retry), the fields stay editable and Generate says why it is unavailable. Forward its `invalid` and `describedBy` properties to the input. The default uses skin input/button primitives and associates validation and guidance with each field. `onRegenerate(field)` opens the field popup. The shared `FieldAdornment` wrapper accepts arbitrary field children and the `fieldAdornment` slot/part; with `reason` set, its action stays focusable, is described by that text and does nothing when pressed (the slot receives `aria-disabled` and `describedBy`; forward them to the control while keeping native `disabled` for its separate disabled state). Pointer entry shows the control; exit hides after 400 ms, re-entry cancels hiding, and focus within keeps it visible. The action has an accessible name, announced politely when focus enters the field and never on pointer hover. Neutral and antd skins share behavior; antd supplies its button presentation.
|
|
76
|
+
|
|
77
|
+
`WaypointChat` accepts the existing `conversations` array with optional mode, subject and parent metadata, or optional `conversationSource`. The rail groups and filters topics, marks forms, folds child threads and renders safe subject routes. Unlinked subjects display “Saved, not linked.” All links use the shared URL policy; the library builds no host routes. `defineFormFillContract` from `/testing` checks draft replacement and focus behavior for consumer skins.
|
|
78
|
+
|
|
79
|
+
## Conformance boundary
|
|
80
|
+
|
|
81
|
+
`runFormFillConformance` and the normal `waypoint-conformance` runner exercise creation replay, reopened mode, subject filters, per-field validation, reply replay, non-low rejection, inherited child mode, wrong-subject rejection and refusal of general replies. The normal runner enables these cases only for a peer advertising protocol 1.1 with form fill. Isolated test peers provide the existing test session endpoint; no production model credential is required. Faulted fixture peers prove the kit detects broken mode, values, subject checks, filtering and creation deduplication.
|
|
82
|
+
|
|
83
|
+
A host owns authorization, allowlisting, private attachment retention, binding pending subjects after its normal Save, saved-but-unlinked reporting, media selection/upload and audit attribution. These server behaviors are not implemented by this package. A host must also refuse plan commands and tool requests in form mode independently of the browser guards.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Message versions
|
|
2
|
+
|
|
3
|
+
The chat's default `messageActions` part offers Copy, and Regenerate, Edit and the "‹ 2/3 ›" version walk when the client has a `versions` port. A host that wants other controls, or another look, passes its own part, which replaces the defaults. The library supplies the pieces they need and keeps them safe to call.
|
|
4
|
+
|
|
5
|
+
## The port
|
|
6
|
+
|
|
7
|
+
A host that lets users edit, regenerate or switch message versions passes a `MessageVersionPort` as `versions`:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
createWaypointClient,
|
|
12
|
+
type MessageVersionPort,
|
|
13
|
+
} from "@appsolutely-io/waypoint";
|
|
14
|
+
|
|
15
|
+
const versions: MessageVersionPort = {
|
|
16
|
+
edit: ({ conversationId, messageId, clientReplyId, content }) =>
|
|
17
|
+
api.editMessage(conversationId, messageId, content, clientReplyId),
|
|
18
|
+
regenerate: ({ conversationId, messageId, clientReplyId }) =>
|
|
19
|
+
api.regenerate(conversationId, messageId, clientReplyId),
|
|
20
|
+
select: ({ conversationId, messageId, clientReplyId, index }) =>
|
|
21
|
+
api.selectVersion(conversationId, messageId, index, clientReplyId),
|
|
22
|
+
};
|
|
23
|
+
const client = createWaypointClient({ ...config, versions });
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The client calls it through `client.editMessage(id, content)`, `client.regenerate(id)` and `client.selectVersion(id, index)`. Each refuses what does not fit (edit on a user message only, regenerate on an assistant message only, an index within `versionCount`) with the `refused` / `message-version` problem, sends one request with one stable `clientReplyId` for identical calls while one is in flight (the host deduplicates retries by that id), and reads the conversation again after the host answered, so the transcript always shows the host's versions.
|
|
27
|
+
|
|
28
|
+
## Feedback
|
|
29
|
+
|
|
30
|
+
A host that takes ratings of replies passes a `FeedbackPort` as `feedback`. The default actions then show Good response and Bad response on each finished reply, and the pressed one stays marked. A reply the host ended as failed or unavailable is not rated, copied or regenerated from its actions; Retry, in the reply row, asks for it again (with a `versions` port; without one the row offers no Retry):
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const client = createWaypointClient({
|
|
34
|
+
...config,
|
|
35
|
+
feedback: {
|
|
36
|
+
send: ({ conversationId, messageId, clientReplyId, rating }) =>
|
|
37
|
+
api.rateReply(conversationId, messageId, rating, clientReplyId),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`client.sendFeedback(id, rating)` refuses a user message or a reply still being written (`refused` / `message-feedback`), sends one request with one `clientReplyId` for identical ratings in flight, reports a failure, and resolves with whether the port took it.
|
|
43
|
+
|
|
44
|
+
## Your own controls
|
|
45
|
+
|
|
46
|
+
The `messageActions` part is rendered for every message with its data, `{ message: { id, role, status, versionIndex, versionCount } }`, so a host part knows which message it is on. `versionWalk(message)` gives the index, the count and the previous and next index, or `null` when there is only one version.
|
|
47
|
+
|
|
48
|
+
A complete part is in [`examples/message-versions.tsx`](examples/message-versions.tsx); the repository's tests render it as written. It shows Previous version and Next version when there are several, Regenerate on assistant messages and Edit on user messages, and it:
|
|
49
|
+
|
|
50
|
+
- makes its buttons unavailable (`aria-disabled`, so they keep focus) while its own call is in flight, and while the message's `status` is `writing` (a new version is still streaming);
|
|
51
|
+
- edits only sent user messages: a send still queued in the outbox is withdrawn from the chat's own controls, never edited;
|
|
52
|
+
- leaves every authority with the host: it calls the client, and the transcript shows what the next read says.
|
|
53
|
+
|
|
54
|
+
While a new version streams, `versionIndex` and `versionCount` keep the previous version's values until the next read.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { WaypointChat } from "@appsolutely-io/waypoint/react";
|
|
58
|
+
import { MessageVersionActions } from "./examples/message-versions";
|
|
59
|
+
|
|
60
|
+
<WaypointChat
|
|
61
|
+
client={client}
|
|
62
|
+
parts={{ messageActions: MessageVersionActions }}
|
|
63
|
+
/>;
|
|
64
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Plan protocol and host integration
|
|
2
|
+
|
|
3
|
+
This release supports protected immediate plans under `plan-recovery-v1`: proposal, initial-review, forward-active, readback, completed, cancelled, left-known, left-unconfirmed and left-reopened. Recovery is an additional contract on the existing wire `state`; `canTransition(from, to)` remains a legacy wire-edge helper, including `running` → `undoing`, and is never authorization to execute or a recovery-profile validator.
|
|
4
|
+
|
|
5
|
+
## Negotiation and reads
|
|
6
|
+
|
|
7
|
+
A client offers `plan-recovery` only with `plans.intent`. Leave and Keep additionally require `plan-leave` and `plan-keep`; each depends on `plan-recovery`. The client enables only capabilities it both offered and received. Forward the `NegotiationOffer` to the host `negotiate(supported, capabilities, offer)` helper. Its two-argument form remains the legacy path.
|
|
8
|
+
|
|
9
|
+
Each client owns its agreement. Adapters forward the optional `ReaderRequestContext` on conversation reads, sends, subscriptions and plan reads; they never retain a shared mutable agreement. HTTP uses `Waypoint-Agreement`, and EventSource uses `waypointAgreement` in the query. This identifier grants no permission. Authenticate and authorize independently, refuse malformed ids with 400, and refuse an unknown agreement with 409 and `{"code":"agreement-expired"}` before admitting work. The safety-control route authenticates and checks CSRF and scoped permission independently of agreement availability.
|
|
10
|
+
|
|
11
|
+
A protected record includes its conversation, generation and monotone revision. A lower revision is ignored; equal revisions must describe identical records. Missing or malformed identity/revision data, a changed generation or conversation, or stripping an adopted recovery view is refused without replacing the current record. Unknown profile or phase strings remain readable but cannot be controlled. Controls also remain unavailable until the fresh authoritative read after an agreement change succeeds. `RecoveryPhase` in intent replies remains a closed type.
|
|
12
|
+
|
|
13
|
+
Subscriptions under an agreement have one reconnect owner: the client closes a failed subscription, probes an authenticated read and reopens after 250/500/1000/2000/4000 ms. A successful probe does not reset the episode; a healthy open for ten seconds does. Offline attempts wait. Exhaustion reports `streamUnavailable`; explicit refresh or connectivity recovery retries. Adapter-native Last-Event-ID resume is not retained for these subscriptions. Signal events are invalidation hints, coalesced into authoritative reads, never content or evidence.
|
|
14
|
+
|
|
15
|
+
## Confirmation and execution
|
|
16
|
+
|
|
17
|
+
The host owns the proposal, exact canonical bytes, digest, review, execution id and fence. Confirmation is an ordinary reply bound to the reviewed digest and stable `clientReplyId`; the first accepted protected confirmation returns `ReplyDelivery` with its private `acceptance`. The client passes it only to the runner. `client.reply()` continues to return a boolean. An exact host-side replay returns its acknowledgment without minting new execution authority; a changed replay is refused. Never infer authority from caller-supplied properties or use client account ids as authentication.
|
|
18
|
+
|
|
19
|
+
The runner sends the confirmed canonical request unchanged with its step identity, preserves `If-Match`, and passes ephemeral step-up material separately. The host checks the current accepted execution, fence and exact step operation at admission and commit. A commit requires the `Admission` produced for that operation. `Admission` and `parseIntentRequest` are reference code in this repository (`src/host/plan-intents.ts`), not exports of the installed package. A terminal intent permanently fences the affected authority, so a held old write cannot commit after it. Run the resolver and all associated storage changes inside the host's own atomic per-plan transaction.
|
|
20
|
+
|
|
21
|
+
Stop inhibits local dispatch synchronously, including a Stop before acceptance. Interruption never automatically invokes the deprecated stop port or requests Undo. It submits the applicable Stop intent and reads step facts. Outcomes are `done`, `not-done` and `unknown`; communication loss alone proves neither success nor failure. Legacy plans without the profile inhibit locally and read back, with no terminal-intent traffic. Existing legacy resume/retry adapters can still be used by their client paths; they do not implement protected forward continuation or selected Undo.
|
|
22
|
+
|
|
23
|
+
A step's `Outcome` (in the plan record's `outcomes` and on a `plan.step` frame) may carry `reason`, the host's plain words for why a failed step failed (at most 500 characters, shown as plain text), and `retryable`, whether it can be tried again. The chat shows both under the failed step, after "Failed". Both are optional and only describe a failed step; a host never puts in `reason` anything the user may not see.
|
|
24
|
+
|
|
25
|
+
## Bounded terminal intents
|
|
26
|
+
|
|
27
|
+
There are five fixed slots, one each for `stop`, `cancel-initial`, `cancel-review`, `leave` and `keep`. A slot is bounded durable semantic state, not an unbounded request log. The resolver orders Stop above Cancel, Keep and Leave; safety-only replacement and compatible same-round Keep/Leave joining preserve actual effects and the original disposition. An old captured request cannot affect an independent later execution. Both evidence/winner and decision-resolution floors matter when joining rounds.
|
|
28
|
+
|
|
29
|
+
The client captures the target, round, expected slot version and correlation before awaiting delivery. An illustrative Stop body is:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"kind": "stop",
|
|
34
|
+
"planId": "example-plan",
|
|
35
|
+
"target": { "type": "execution", "id": "example-execution" },
|
|
36
|
+
"capturedRound": 0,
|
|
37
|
+
"expectedSlotVersion": 0,
|
|
38
|
+
"correlation": "example-correlation",
|
|
39
|
+
"cause": "user"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
All values must come from the adopted record and captured action. This example grants nothing. `parseIntentRequest` rejects unknown fields, malformed tokens, invalid integer bounds and a `payload` larger than 4096 UTF-8 bytes after NFC normalization. The separate streaming HTTP body limit is `MAX_INTENT_BODY_BYTES` = 8192 bytes: enforce it before parsing, including when Content-Length is absent or false. The body includes the payload and its surrounding JSON fields. Resolve the route's plan in the server's own store, verify its conversation and the authenticated caller's scope, then compare the parsed request against stored state. Do not select the authorized target from request-body echoes.
|
|
44
|
+
|
|
45
|
+
Send through `plans.intent(request)` to `/plans/:id/safety-control`. Return `PlanIntentReply` for that exact plan, kind, correlation and captured round, including its slot version, effective round, result, known phase and notice `keys`. The reply distinguishes fencing (`safetyApplied`) from a saved decision (`dispositionApplied`); a successful HTTP response alone is not proof that the requested disposition won.
|
|
46
|
+
|
|
47
|
+
- Initial Cancel targets the generation and follows its first acceptance if it races confirmation. It is distinct from a normal decline reply.
|
|
48
|
+
- Review Cancel closes only its captured review; it does not revoke an unrelated execution. Subsequent protected review features are not included in this release.
|
|
49
|
+
- Leave during an active run applies safety fencing but asks for a fresh review (`leaveSafetyNeedsReview`); it does not pretend the user's final disposition was saved. From readback, Leave acknowledges the current decision key, evidence digest/count and evidence/winner/resolution versions. An informed Leave can close as left-known or left-unconfirmed; identical concurrent Leave requests can join its round. Later evidence can produce left-reopened with actual facts preserved.
|
|
50
|
+
- Keep is explicitly reviewed as `forward-only` or `end-retention`. It refuses active or unknown work. Successful Keep closes remaining forward work or own retention, stores a permanent receipt and seals the plan. Later intents read `finalHistory` without reopening it. This release has no selected Undo choice, so Keep must not claim to waive one.
|
|
51
|
+
|
|
52
|
+
## Delivery uncertainty
|
|
53
|
+
|
|
54
|
+
`IntentOverlay` holds the complete unchanged request, producer, local inhibition fact and delivery state (`sending`, `uncertain`, `not-sent`), with optional retry time and problem. One pending intent per plan prevents new confirmation. Status and Retry redeliver the same semantic request; never create a new correlation to discover whether an old request won.
|
|
55
|
+
|
|
56
|
+
Reject an intent promise with a numeric HTTP `status` for a known host refusal, plus `retryAfter` for 429. An unclassified rejection is an unknown outcome. A 401 can be retried once after a successful authenticated call; 403, 429 and offline states wait for the user's eligible action. `checkIntentStatus`, `checkSavedChoice`, `checkKeepStatus` and `retryIntent` share the pending request. Snapshot notices express the returned facts, not an assumed choice.
|
|
57
|
+
|
|
58
|
+
The lazy intent module can fail to load. In that case local Stop remains inhibited and the action reports `retryable` / `intent-load`; a later use retries loading. Read handlers, client actions and storage commits also have lazy failure paths; a consumer must ship every referenced chunk. A load failure never authorizes fallback execution or automatic Undo.
|
|
59
|
+
|
|
60
|
+
## Host responsibilities
|
|
61
|
+
|
|
62
|
+
- Persist reviews, generations, executions, fences, five intent slots, rounds, receipts, final history and evidence atomically and durably. The reference fixture is in-memory and does not prove restart durability for a real service.
|
|
63
|
+
- Revalidate authentication, CSRF, current scope and stored target identity at the effect boundary. Reject the legacy `/stop` route for protected plans. Preserve exact confirmation/replay semantics and enforce body limits before parsing.
|
|
64
|
+
- Record actual per-step attribution, keep uncertainty explicit and retain permanent fenced authority and sealed receipts. The client cannot make remote writes atomic or manufacture proof from a timeout.
|
|
65
|
+
- Forward negotiation offers and reader contexts on every applicable adapter call. Validate records and intent replies against the shipped schema, registering `x-open-enum` as an annotation and the `date-time` format when using a strict schema validator.
|
|
66
|
+
- Keep the existing conformance session/script controls confined to an explicit disposable test deployment; the fixture returns 404 outside test mode. Audit actual deployment routing independently. Passing a test base URL does not certify production isolation.
|
|
67
|
+
|
|
68
|
+
Account quota, account deletion handling, original inverse continuation, background recovery/diagnostics, scheduled-work recovery, custom step kinds and their lifecycle conformance controls are **not included in this release**. Their host APIs, registry and scheduling policies are not supplied by the existing `RunnerPorts`. In particular, there is no lifecycle endpoint that can delete a principal and no SDK-wide deleted-reference registry.
|
|
69
|
+
|
|
70
|
+
For integrations with account lifecycle semantics, immutable principal subject references must never be reassigned to a recreated or different account. A receipt released before deletion has no later holding-deletion marker; reusing that identity can make its prior receipts readable. Identity non-reuse is a host responsibility, not a claim of retroactive SDK marking. Lifecycle test controls for such an integration must be isolated, 404 elsewhere, limited to the scenario's own created principal, and reject arbitrary principal targeting; those controls are not included here.
|
|
71
|
+
|
|
72
|
+
## Packaging acceptance
|
|
73
|
+
|
|
74
|
+
The eager core keeps its own budget in `.size-limit.cjs`, checked by `npm run size`. Intent, client actions, reads, storage, decoder and other lazy code keep separate entry budgets. The additional **core with lazy recovery** check rebundles all reachable static and dynamic recovery dependencies once with the size-limit plugin's esbuild minification and tree-shaking settings, then measures gzip level 9 minus the size plugin's 32-byte empty-project deduction. It excludes the independently measured YAML parser, form session and conformance runners. The regression limit in `.size-limit.cjs` allows 3% above the measured size, rounded down. This aggregate is not a startup-download claim. Existing entry caps remain independently enforced. A hidden reachable payload must make `npm run size` fail.
|
|
75
|
+
|
|
76
|
+
`npm run test:consumer -- <output-directory>` installs the packed prebuilt artifact with npm and pnpm without lifecycle scripts, compares installed distribution hashes, exercises ESM/CJS lazy public operations and imports all shipped lazy core entry modules discovered from top-level declaration/code pairs in the installed artifact. UI entry points load in both formats after optional peers are installed. The packed native graph resolves the framework-free core and its lazy modules using neutral/native conditions, refusing Node and UI runtime imports and missing chunks. This is device-free graph evidence; `./hooks` and native UI are not included in this release, and the smoke test proves no device-runtime behavior.
|
|
77
|
+
|
|
78
|
+
## Quality backlog
|
|
79
|
+
|
|
80
|
+
These known follow-through items do not add public guarantees:
|
|
81
|
+
|
|
82
|
+
- No release has been checked with a screen reader. The accessibility checks are DOM and browser assertions (roles, names, descriptions, focus, what a live region contains); whether speech reads each announcement once, and in what order, is not verified.
|
|
83
|
+
- The schema compiles under Ajv's default options once `x-open-enum` and the `date-time` format are registered; Ajv's `strict: true` refuses it: its `if` branches require a property they do not declare (`strictRequired`), and with default options Ajv already logs its `required` keywords without a `type` (`strictTypes`).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Reply protocol
|
|
2
|
+
|
|
3
|
+
What a host implements so the chat can show a reply as it is written and let the user act on it. Each feature here is optional and negotiated or announced by the host: a host that implements none of it stays valid.
|
|
4
|
+
|
|
5
|
+
The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT" and "MAY" in this document are to be interpreted as described in [BCP 14](https://www.rfc-editor.org/info/bcp14) ([RFC 2119](https://www.rfc-editor.org/rfc/rfc2119), [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174)) when, and only when, they appear in all capitals.
|
|
6
|
+
|
|
7
|
+
## Naming a step
|
|
8
|
+
|
|
9
|
+
A `tool.call` frame asks the client to read data for the reply it is writing. Its call MAY carry `label`, the host's plain words for the step, such as "Reading 3 orders…". The chat shows the latest step on one line above the reply row, and every step since the user's last message behind "Show all 2 steps"; each is running, done or failed. Without a label the step reads "Looking this up…".
|
|
10
|
+
|
|
11
|
+
- A host SHOULD give every call a `label`, in the conversation's language, at most 200 characters.
|
|
12
|
+
- A label is shown as plain text, never as markup. It MUST NOT carry data the user may not see.
|
|
13
|
+
|
|
14
|
+
## Citing sources
|
|
15
|
+
|
|
16
|
+
A message MAY carry `sources`, what the reply drew on: each has an `id`, a `title` and a `url`. `[n]` in the message's text cites the n-th source; the chat links it to the source, named "Source n: title", and lists the sources under the answer behind a "Sources (2)" disclosure.
|
|
17
|
+
|
|
18
|
+
- A host SHOULD cite each source it lists at least once.
|
|
19
|
+
- A source's `url` MUST be one the user may open. The chat links only addresses its URL policy allows (`isSafeUrl`, with `extraUrlSchemes`); any other source is named without a link.
|
|
20
|
+
|
|
21
|
+
## Collapsing supporting detail
|
|
22
|
+
|
|
23
|
+
A `details` block holds what supports the answer but is not the answer: how it was worked out, the rows it read, the caveats. Its body has a `summary`, the words on the disclosure (at most 200 characters), and `blocks`, shown inside it. The chat draws it closed, below the answer, as a native disclosure the user opens and closes.
|
|
24
|
+
|
|
25
|
+
- A host SHOULD put the answer in the message's text and the detail that supports it in a `details` block, so the answer comes first.
|
|
26
|
+
- A host MUST NOT put anything the user needs to act on only inside a `details` block, since it starts closed.
|
|
27
|
+
- Like every block, it carries a `fallback` for clients that do not know the type.
|
|
28
|
+
|
|
29
|
+
## Asking for what is missing
|
|
30
|
+
|
|
31
|
+
A `form` block asks the user for details inline. Each field MAY carry `value`, what the host already knows for it; the form starts with it and the user may change it before sending.
|
|
32
|
+
|
|
33
|
+
- A host SHOULD fill in every field it can, so the user types only what is missing.
|
|
34
|
+
- A field's `value` MUST NOT hold anything the user may not see.
|
|
35
|
+
|
|
36
|
+
## Stopping a reply
|
|
37
|
+
|
|
38
|
+
A client offers the `reply-stop` capability; a host that implements stopping echoes it in its negotiation answer. Only then does the chat show Stop while a reply is written.
|
|
39
|
+
|
|
40
|
+
Stop is an ordinary reply, `{ "kind": "stop", "clientReplyId": "…", "messageId": "…", "versionIndex": 1 }`, sent to the conversation's replies route (`ReplyStop` in the schema). The client marks the reply `stopped` at once and drops any text that still arrives for it: on the stream, and in a read that still says the host is writing it, which shows the text the reply had when it was stopped.
|
|
41
|
+
|
|
42
|
+
A stop's `clientReplyId` names one attempt at the reply: the version of the message being written when the user pressed Stop. The client repeats that id only when it repeats that request. Once a new version of the message is asked for (`regenerate`, see [message versions](message-versions.md)) or starts on the stream (`message.start`), a stop of it is a new request with a new `clientReplyId`, for the same `messageId`.
|
|
43
|
+
|
|
44
|
+
A stop names the version it is for in `versionIndex`, numbered as the message's `versionIndex` (1 for a message with one version). Stop and Regenerate are separate requests, so a stop can reach the host after the next version has started; the version number keeps it off that version. The field is optional: the client leaves it out when it cannot tell how the host numbers the version being written, which happens when a reply that had ended starts again on the stream and the client has no `versions` port.
|
|
45
|
+
|
|
46
|
+
A host that answered `reply-stop`:
|
|
47
|
+
|
|
48
|
+
- MUST issue agreements (an `agreementId` in its negotiation answer). A host that issues none MUST NOT answer `reply-stop`, because it cannot tell which clients negotiated it.
|
|
49
|
+
- MUST accept a stop of the assistant message it is writing, stop writing it, and end it with the status `stopped` (on the stream, `message.end` with `status: "stopped"`; in every later read, the message's `status`).
|
|
50
|
+
- MUST keep the text the message had when it stopped. It MUST NOT blank it or replace it with another version.
|
|
51
|
+
- MUST answer a repeated stop with the same `clientReplyId` with its first response, as for any reply.
|
|
52
|
+
- MUST ignore a stop whose `versionIndex` is not the version of the message it is writing: such a stop changes nothing, and the host SHOULD accept it, as it does a stop of a message that has ended. A stop without `versionIndex` is for the version being written when the stop arrives.
|
|
53
|
+
- MUST treat a stop with a new `clientReplyId` as a new request, even for a message it stopped before, and stop the version of that message it names, or, when it names none, the version it is writing then.
|
|
54
|
+
- SHOULD accept a stop of a message that has already ended, changing nothing.
|
|
55
|
+
- MUST NOT send `stopped` under an agreement that does not include `reply-stop`. In every read and every `message.end` it sends under such an agreement, it MUST give a stopped message the status `failed` instead, with the text it kept. Reads are shared by every client of the conversation, and `failed` is the status such a client knows that never shows a reply that is not whole as finished (see [When a reply fails](#when-a-reply-fails)). A request that presents no agreement cannot be told apart, and MAY be sent `stopped`.
|
|
56
|
+
|
|
57
|
+
A stopped reply is a finished reply: the chat shows its text with a "Stopped" mark and the reply row is empty.
|
|
58
|
+
|
|
59
|
+
`waypoint-conformance` checks these rules when the host answers `reply-stop`: the host must have issued an agreement, a stop naming another version must leave the reply being written, and the stopped reply is read under an agreement with `reply-stop` and under one without it. It needs the test script to support a reply left being written: a turn `{ "chunks": ["Partial answer"], "ending": "held" }` streams those pieces into an assistant message and leaves it `writing`.
|
|
60
|
+
|
|
61
|
+
## When a reply fails
|
|
62
|
+
|
|
63
|
+
A host that cannot finish a reply says so in one of two ways: it ends the message with `message.end` and `status: "failed"` or `"unavailable"` (and gives the message that `status` in every later read), or it leaves the message and sets the conversation's `replyStatus` to `failed` or `unavailable`. Either way the chat says "Reply failed" or "Reply unavailable" and never shows such a reply as finished. A message the host ended says it in the message, under whatever text it had, and a screen reader hears it once, from the chat's message announcements ("A reply could not be completed."); otherwise the reply row says it, and is what a screen reader hears. Below it, in the reply row, is Retry, which never sends the user's message twice. For a message the host ended, Retry asks for the reply again as a new version (`regenerate`, see [message versions](message-versions.md)) and keeps the failed one; the chat offers it only when it was given a `versions` port, since reading the conversation again would show the same ended message. Without one, the row offers no Retry and the user goes on from the composer, sending the question again if they wish. For a conversation's `replyStatus`, Retry reads the conversation again (or regenerates the last reply, for `failed`, when there is a `versions` port).
|
|
64
|
+
|
|
65
|
+
- A host SHOULD end a reply it gave up on with one of these statuses rather than leave it `writing`.
|
|
66
|
+
- A host MUST NOT mark a message `completed` when its text is not the whole reply.
|
|
67
|
+
|
|
68
|
+
## Where an error is shown
|
|
69
|
+
|
|
70
|
+
An error is shown next to what it is about, so the reader finds it where they are looking, at any width.
|
|
71
|
+
|
|
72
|
+
- **About one message.** A message the user sent that the host refused or did not receive stays in the transcript marked "Not sent", with its problem in plain words under it, its Retry, Withdraw and Dismiss, and the problem's "Details" (the error code and reference) after them. A reply the host ended as failed or unavailable says so in the reply, with Retry in the reply row right after it when the reply can be asked for again. Neither is repeated below the composer.
|
|
73
|
+
- **About the conversation.** Every other problem is about the conversation as a whole, not one message: a read of the conversation that failed, a stream that dropped, a plan, a saved request, the session or the agreement with the host. It is shown once, after the composer, with its one action and its "Details".
|
|
74
|
+
|
|
75
|
+
A problem is about a message when its `id` is the `clientMessageId` of a message the client could not send. A UI drawn from `/hooks` follows the same rule: `snapshot.pending` lists each message not sent with its `failure`, and `problemView(snapshot.problem)` words the problem.
|