@hitch42/applet 0.1.0-beta.10 → 0.1.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/applet.js +11 -11
  2. package/dist/{build-BlmLiKro.js → build-BRpYQ9Lk.js} +1 -1
  3. package/dist/build-CypJADhV.js +2 -0
  4. package/dist/{ci-0Q2mUh53.js → ci-C2CL-0sj.js} +2 -2
  5. package/dist/client.d.ts +2 -2
  6. package/dist/{contract-QUvEGiRI.d.ts → contract-CsUpTBe0.d.ts} +1 -1
  7. package/dist/{deploy-BbL3GObV.js → deploy-CdQQQ5HG.js} +131 -80
  8. package/dist/{generate-XB-72Fzj.js → generate-BDHb2kjw.js} +7 -4
  9. package/dist/generate-Cai2X9z1.js +2 -0
  10. package/dist/index.d.ts +3 -2
  11. package/dist/index.js +2 -1
  12. package/dist/{init-DwsXRUk-.js → init-CpVfJQbv.js} +4 -4
  13. package/dist/{load-config-OKZU3UKn.js → load-config-DI-e1Ool.js} +1 -1
  14. package/dist/{logs-XvK6Zs5Y.js → logs-CffdKMRd.js} +2 -2
  15. package/dist/{manifest-I-9s8R36.d.ts → manifest-C6_F4bL8.d.ts} +3 -1
  16. package/dist/{manifest-4ZAc6HUk.js → manifest-CJSL9GuC.js} +6 -1
  17. package/dist/{project-B0qNXnFu.js → project-D-hX59RS.js} +4 -1
  18. package/dist/react.d.ts +2 -1
  19. package/dist/react.js +57 -37
  20. package/dist/{schedules-BkWZsbSs.js → schedules-DIIowhD3.js} +1 -1
  21. package/dist/server.d.ts +2 -2
  22. package/dist/telephony.d.ts +24 -4
  23. package/dist/telephony.js +135 -16
  24. package/dist/{variables-0tjnAeut.js → variables-Bzl2ZLQN.js} +1 -1
  25. package/dist/vite.d.ts +1 -1
  26. package/dist/vite.js +17 -6
  27. package/package.json +3 -1
  28. package/scaffold/.agents/skills/hitch-applets/SKILL.md +1 -1
  29. package/scaffold/.agents/skills/hitch-applets/references/markdown.md +2 -0
  30. package/scaffold/.agents/skills/hitch-theory/SKILL.md +1 -1
  31. package/scaffold/.github/workflows/hitch.yml +1 -1
  32. package/scaffold/applet/src/client/styles/globals.css +2 -0
  33. package/dist/build-CcT-wNN9.js +0 -2
  34. package/dist/generate-wIN3IYqL.js +0 -2
package/dist/react.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
- import { BetweenHorizontalEnd, BetweenVerticalEnd, Bold, Code, Heading, Italic, Link, List, ListMinus, ListOrdered, ListTodo, Quote, Strikethrough, Table, Trash2 } from "lucide-react";
2
+ import { BetweenHorizontalEnd, BetweenVerticalEnd, Bold, Code, Heading, Italic, Link, List, ListMinus, ListOrdered, ListTodo, Quote, Strikethrough, Table, Trash2, X } from "lucide-react";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  import { Streamdown, defaultRehypePlugins, defaultUrlTransform } from "streamdown";
5
+ import { GapCursor } from "@tiptap/pm/gapcursor";
6
+ import { NodeSelection } from "@tiptap/pm/state";
5
7
  import { EditorContent, NodeViewWrapper, ReactNodeViewRenderer, useEditor, useEditorState } from "@tiptap/react";
6
8
  import { Button, Toolbar } from "react-aria-components";
7
9
  import { twMerge } from "cn";
@@ -123,11 +125,14 @@ function Markdown({ children, files, className }) {
123
125
  function sanitizeAltText(text) {
124
126
  return text.replaceAll(/[[\]()\\]/g, "").replaceAll(/\s+/g, " ");
125
127
  }
128
+ function editorMarkdown(editor) {
129
+ return editor.getMarkdown().trimEnd();
130
+ }
126
131
  function tableHasBlockCells(node) {
127
132
  return (node.content ?? []).some((row) => (row.content ?? []).some((cell) => cell.content?.length !== 1 || cell.content[0]?.type !== "paragraph"));
128
133
  }
129
134
  function hitchImageNode(files) {
130
- return function HitchImageNode({ node }) {
135
+ return function HitchImageNode({ node, selected, editor, deleteNode }) {
131
136
  const src = String(node.attrs.src);
132
137
  const alt = typeof node.attrs.alt === "string" ? node.attrs.alt : void 0;
133
138
  const id = src.startsWith("hitch-file://") ? src.slice(13) : void 0;
@@ -152,42 +157,46 @@ function hitchImageNode(files) {
152
157
  active = false;
153
158
  };
154
159
  }, [files, id]);
155
- if (id === void 0) return /* @__PURE__ */ jsx(NodeViewWrapper, {
156
- as: "span",
157
- children: /* @__PURE__ */ jsx("img", {
158
- src,
159
- alt,
160
- className: "max-w-full rounded-lg",
161
- "data-drag-handle": true
162
- })
163
- });
164
- if (files === void 0 || state.error !== void 0) return /* @__PURE__ */ jsx(NodeViewWrapper, {
165
- as: "span",
160
+ const content = id === void 0 ? /* @__PURE__ */ jsx("img", {
161
+ src,
162
+ alt,
163
+ className: "max-w-full rounded-lg",
164
+ "data-drag-handle": true
165
+ }) : files === void 0 || state.error !== void 0 ? /* @__PURE__ */ jsx("span", {
166
166
  className: "text-muted-foreground text-sm",
167
167
  children: alt ?? "Image unavailable"
168
- });
169
- if (state.id !== id || state.url === void 0) return /* @__PURE__ */ jsx(NodeViewWrapper, {
170
- as: "span",
168
+ }) : state.id !== id || state.url === void 0 ? /* @__PURE__ */ jsx("span", {
171
169
  className: "inline-block min-h-16 min-w-32 animate-pulse rounded-md bg-muted p-3 text-muted-foreground text-sm",
172
170
  role: "img",
173
171
  "aria-label": alt ?? "Loading image",
174
172
  children: alt ?? "Loading image"
173
+ }) : /* @__PURE__ */ jsx("img", {
174
+ src: state.url,
175
+ alt,
176
+ className: "max-w-full rounded-lg",
177
+ "data-drag-handle": true,
178
+ onError: () => {
179
+ invalidateDownloadUrl(id);
180
+ setState({
181
+ id,
182
+ error: /* @__PURE__ */ new Error("image failed to load")
183
+ });
184
+ }
175
185
  });
176
- return /* @__PURE__ */ jsx(NodeViewWrapper, {
186
+ return /* @__PURE__ */ jsxs(NodeViewWrapper, {
177
187
  as: "span",
178
- children: /* @__PURE__ */ jsx("img", {
179
- src: state.url,
180
- alt,
181
- className: "max-w-full rounded-lg",
182
- "data-drag-handle": true,
183
- onError: () => {
184
- invalidateDownloadUrl(id);
185
- setState({
186
- id,
187
- error: /* @__PURE__ */ new Error("image failed to load")
188
- });
189
- }
190
- })
188
+ className: twMerge("relative inline-block max-w-full rounded-lg", selected && "in-[.ProseMirror[contenteditable=true]:focus-within]:ring-2 in-[.ProseMirror[contenteditable=true]:focus-within]:ring-ring"),
189
+ children: [content, selected && /* @__PURE__ */ jsx("button", {
190
+ type: "button",
191
+ contentEditable: false,
192
+ "aria-label": "Remove image",
193
+ className: "absolute top-1.5 right-1.5 hidden size-6 in-[.ProseMirror[contenteditable=true]:focus-within]:inline-flex items-center justify-center rounded-full bg-background/90 text-foreground shadow-sm outline-none hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/30 [&_svg]:size-3.5",
194
+ onClick: () => {
195
+ deleteNode();
196
+ editor.commands.focus();
197
+ },
198
+ children: /* @__PURE__ */ jsx(X, {})
199
+ })]
191
200
  });
192
201
  };
193
202
  }
@@ -223,7 +232,7 @@ function findImage(view, src) {
223
232
  });
224
233
  return found;
225
234
  }
226
- function MarkdownEditor({ value, onChange, files, id, placeholder, required, minLength, maxLength, disabled, className, onUploadError }) {
235
+ function MarkdownEditor({ value, onChange, files, id, placeholder, required, minLength, maxLength, disabled, autoFocus, className, onUploadError }) {
227
236
  const validationRef = useRef(null);
228
237
  const lastEmittedRef = useRef(value);
229
238
  const filesRef = useRef(files);
@@ -260,6 +269,7 @@ function MarkdownEditor({ value, onChange, files, id, placeholder, required, min
260
269
  content: value,
261
270
  contentType: "markdown",
262
271
  editable: !disabled,
272
+ autofocus: autoFocus ? "end" : false,
263
273
  editorProps: {
264
274
  attributes: { class: editorClassName },
265
275
  handlePaste: (view, event) => {
@@ -280,8 +290,17 @@ function MarkdownEditor({ value, onChange, files, id, placeholder, required, min
280
290
  return true;
281
291
  }
282
292
  },
283
- onUpdate: ({ editor: updatedEditor }) => {
284
- const markdown = updatedEditor.getMarkdown();
293
+ onCreate: ({ editor: createdEditor }) => {
294
+ if (autoFocus) createdEditor.commands.focus("end");
295
+ else if (createdEditor.state.selection instanceof NodeSelection) {
296
+ const { state, view } = createdEditor;
297
+ view.dispatch(state.tr.setSelection(new GapCursor(state.doc.resolve(0))));
298
+ }
299
+ },
300
+ onUpdate: ({ editor: updatedEditor, transaction }) => {
301
+ if (!transaction.docChanged) return;
302
+ const markdown = editorMarkdown(updatedEditor);
303
+ if (markdown === lastEmittedRef.current) return;
285
304
  lastEmittedRef.current = markdown;
286
305
  onChange(markdown);
287
306
  }
@@ -327,10 +346,11 @@ function MarkdownEditor({ value, onChange, files, id, placeholder, required, min
327
346
  }
328
347
  }
329
348
  useEffect(() => {
330
- if (value !== lastEmittedRef.current && value !== editor.getMarkdown()) editor.commands.setContent(value, {
349
+ if (value !== lastEmittedRef.current && value !== editorMarkdown(editor)) editor.commands.setContent(value, {
331
350
  contentType: "markdown",
332
351
  emitUpdate: false
333
352
  });
353
+ lastEmittedRef.current = value;
334
354
  }, [editor, value]);
335
355
  useEffect(() => editor.setEditable(!disabled, false), [disabled, editor]);
336
356
  const active = useEditorState({
@@ -474,10 +494,11 @@ function MarkdownEditor({ value, onChange, files, id, placeholder, required, min
474
494
  }
475
495
  ];
476
496
  return /* @__PURE__ */ jsxs("div", {
477
- className: twMerge("overflow-hidden rounded-2xl border border-input bg-background", className),
497
+ className: twMerge("flex flex-col overflow-hidden rounded-2xl border border-input bg-background", className),
478
498
  children: [
499
+ /* @__PURE__ */ jsx(EditorContent, { editor }),
479
500
  /* @__PURE__ */ jsxs(Toolbar, {
480
- className: "flex flex-wrap gap-1 border-b border-input p-1",
501
+ className: "order-first flex flex-wrap gap-1 border-b border-input p-1",
481
502
  "aria-label": "Markdown formatting",
482
503
  children: [items.map(({ title, active: isActive, icon: Icon, command }) => /* @__PURE__ */ jsx(Button, {
483
504
  className: twMerge(buttonClassName, isActive && "bg-muted text-foreground"),
@@ -496,7 +517,6 @@ function MarkdownEditor({ value, onChange, files, id, placeholder, required, min
496
517
  children: /* @__PURE__ */ jsx(Icon, { className: iconClassName })
497
518
  }, title))] })]
498
519
  }),
499
- /* @__PURE__ */ jsx(EditorContent, { editor }),
500
520
  /* @__PURE__ */ jsx("textarea", {
501
521
  ref: validationRef,
502
522
  id,
@@ -1,4 +1,4 @@
1
- import { t as loadAppletConfig } from "./load-config-OKZU3UKn.js";
1
+ import { t as loadAppletConfig } from "./load-config-DI-e1Ool.js";
2
2
  import { t as question } from "./prompt-ChS-grYO.js";
3
3
  import { RPCLink } from "@orpc/client/fetch";
4
4
  import { readFile } from "node:fs/promises";
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as createWorkflow, i as WorkflowStep, n as WorkflowDefinition, r as WorkflowDuration } from "./workflow-DurM6Fwx.js";
2
- import { _ as RegisteredWorkflows, f as RegisteredContract, g as RegisteredVariables, p as RegisteredManifest } from "./manifest-I-9s8R36.js";
3
- import { t as NarrowContract } from "./contract-QUvEGiRI.js";
2
+ import { _ as RegisteredWorkflows, f as RegisteredContract, g as RegisteredVariables, p as RegisteredManifest } from "./manifest-C6_F4bL8.js";
3
+ import { t as NarrowContract } from "./contract-CsUpTBe0.js";
4
4
  import { t as WithBatchRecorder } from "./batch-Qa4B4-Yp.js";
5
5
  import { o as WithFilesUpload } from "./upload-DKZzz5kW.js";
6
6
  import { ContractRouterClient } from "@orpc/contract";
@@ -1,16 +1,26 @@
1
1
  //#region src/telephony/phone.d.ts
2
2
  type PhoneState = {
3
+ owner: boolean;
3
4
  status: "off" | "connecting" | "ready" | "error";
4
5
  error: string | null;
5
- active: null | {
6
- callId: string;
7
- to: string;
6
+ incoming: null | {
7
+ id: string;
8
+ from: string;
9
+ };
10
+ active: null | ({
8
11
  from: string;
9
12
  state: "connecting" | "ringing" | "active" | "held" | "ended";
10
13
  muted: boolean;
11
14
  startedAt: number;
15
+ } & ({
16
+ direction: "outbound";
17
+ callId: string;
18
+ to: string;
12
19
  recording: boolean;
13
- };
20
+ } | {
21
+ direction: "inbound";
22
+ recording: false;
23
+ }));
14
24
  };
15
25
  type CallInput = {
16
26
  numberId: string;
@@ -31,6 +41,8 @@ declare class Phone {
31
41
  private readonly api;
32
42
  private client;
33
43
  private currentCall;
44
+ private incomingCall;
45
+ private readonly acceptsCalls;
34
46
  private connectPromise;
35
47
  private releaseLock;
36
48
  private lockAbortController;
@@ -44,8 +56,11 @@ declare class Phone {
44
56
  private audio;
45
57
  private firstLogin;
46
58
  private callStarting;
59
+ private reconnectOnPageShow;
60
+ private connectionRequested;
47
61
  constructor(options?: {
48
62
  baseUrl?: string;
63
+ acceptsCalls?: boolean;
49
64
  });
50
65
  subscribe: (listener: Listener) => (() => void);
51
66
  getSnapshot: () => PhoneState;
@@ -55,8 +70,12 @@ declare class Phone {
55
70
  private connectInternal;
56
71
  private connectOwned;
57
72
  disconnect: () => void;
73
+ private pageHide;
74
+ private pageShow;
58
75
  private beforeUnload;
59
76
  call: ({ numberId, to }: CallInput) => Promise<void>;
77
+ answer: () => Promise<void>;
78
+ decline: () => Promise<void>;
60
79
  private sendCommand;
61
80
  private onChannelMessage;
62
81
  private runCommand;
@@ -65,6 +84,7 @@ declare class Phone {
65
84
  }
66
85
  declare const createPhone: (options?: {
67
86
  baseUrl?: string;
87
+ acceptsCalls?: boolean;
68
88
  }) => Phone;
69
89
  //#endregion
70
90
  //#region src/telephony/use-phone.d.ts
package/dist/telephony.js CHANGED
@@ -28,9 +28,12 @@ const createTelephonyApi = (baseUrl = location.origin) => {
28
28
  return response.json();
29
29
  };
30
30
  return {
31
- token: (deviceKey) => request("voice/token", {
31
+ token: (deviceKey, acceptsCalls) => request("voice/token", {
32
32
  method: "POST",
33
- body: JSON.stringify({ deviceKey })
33
+ body: JSON.stringify({
34
+ deviceKey,
35
+ acceptsCalls
36
+ })
34
37
  }),
35
38
  startCall: (numberId, to) => request("calls", {
36
39
  method: "POST",
@@ -165,14 +168,18 @@ const DEVICE_KEY = "hitch:telephony:device";
165
168
  let singleton;
166
169
  var Phone = class {
167
170
  state = {
171
+ owner: false,
168
172
  status: "off",
169
173
  error: null,
174
+ incoming: null,
170
175
  active: null
171
176
  };
172
177
  listeners = /* @__PURE__ */ new Set();
173
178
  api;
174
179
  client = null;
175
180
  currentCall = null;
181
+ incomingCall = null;
182
+ acceptsCalls;
176
183
  connectPromise = null;
177
184
  releaseLock = null;
178
185
  lockAbortController = null;
@@ -186,8 +193,11 @@ var Phone = class {
186
193
  audio = null;
187
194
  firstLogin = true;
188
195
  callStarting = false;
196
+ reconnectOnPageShow = false;
197
+ connectionRequested = false;
189
198
  constructor(options = {}) {
190
199
  this.api = createTelephonyApi(options.baseUrl);
200
+ this.acceptsCalls = options.acceptsCalls ?? false;
191
201
  }
192
202
  subscribe = (listener) => {
193
203
  this.listeners.add(listener);
@@ -248,6 +258,7 @@ var Phone = class {
248
258
  };
249
259
  }
250
260
  connect = () => {
261
+ this.connectionRequested = true;
251
262
  this.connectPromise ??= this.connectInternal().finally(() => {
252
263
  this.connectPromise = null;
253
264
  });
@@ -255,6 +266,8 @@ var Phone = class {
255
266
  };
256
267
  async connectInternal() {
257
268
  if (this.state.status === "ready") return;
269
+ window.addEventListener("pagehide", this.pageHide);
270
+ window.addEventListener("pageshow", this.pageShow);
258
271
  this.channel ??= new BroadcastChannel("hitch:telephony");
259
272
  this.channel.onmessage = this.onChannelMessage;
260
273
  this.update({
@@ -275,8 +288,10 @@ var Phone = class {
275
288
  });
276
289
  this.ownsPhone = true;
277
290
  this.update({
291
+ owner: true,
278
292
  status: "connecting",
279
293
  error: null,
294
+ incoming: null,
280
295
  active: null
281
296
  });
282
297
  await this.connectOwned();
@@ -297,6 +312,7 @@ var Phone = class {
297
312
  return;
298
313
  }
299
314
  this.ownsPhone = true;
315
+ this.update({ owner: true });
300
316
  await this.connectOwned();
301
317
  }
302
318
  async connectOwned() {
@@ -311,7 +327,7 @@ var Phone = class {
311
327
  8e3
312
328
  ]) {
313
329
  if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
314
- const { token } = await this.api.token(deviceKey);
330
+ const { token } = await this.api.token(deviceKey, this.acceptsCalls);
315
331
  const { TelnyxRTC: Client } = await import("@telnyx/webrtc");
316
332
  const client = new Client({
317
333
  login_token: token,
@@ -320,15 +336,35 @@ var Phone = class {
320
336
  this.client = client;
321
337
  client.on("telnyx.notification", ({ type, call }) => {
322
338
  if (type !== "callUpdate" || !call) return;
323
- if (call.direction === "inbound") {
324
- call.hangup();
339
+ if (call === this.currentCall) {
340
+ this.onCallUpdate(call);
341
+ return;
342
+ }
343
+ if (call.direction !== "inbound") return;
344
+ if ([
345
+ "hangup",
346
+ "destroy",
347
+ "purge"
348
+ ].includes(call.state)) {
349
+ if (call === this.incomingCall) {
350
+ this.incomingCall = null;
351
+ this.update({ incoming: null });
352
+ }
353
+ return;
354
+ }
355
+ if (!this.acceptsCalls || this.currentCall || this.callStarting || this.incomingCall && this.incomingCall !== call) {
356
+ call.hangup().catch((error) => console.error("Call decline failed", error));
325
357
  return;
326
358
  }
327
- if (call === this.currentCall) this.onCallUpdate(call);
359
+ this.incomingCall = call;
360
+ if (call.state === "ringing") this.update({ incoming: {
361
+ id: call.id,
362
+ from: call.options.remoteCallerNumber ?? ""
363
+ } });
328
364
  });
329
365
  client.on("telnyx.warning", (warning) => {
330
366
  if (Number(warning.warning.code) !== 34001) return;
331
- this.api.token(deviceKey).then(({ token: nextToken }) => client.login({ creds: { login_token: nextToken } })).catch((error) => {
367
+ this.api.token(deviceKey, this.acceptsCalls).then(({ token: nextToken }) => client.login({ creds: { login_token: nextToken } })).catch((error) => {
332
368
  console.error("Phone token renewal failed", error);
333
369
  this.update({ error: String(error) });
334
370
  });
@@ -341,7 +377,6 @@ var Phone = class {
341
377
  });
342
378
  this.firstLogin = false;
343
379
  this.update({ status: "ready" });
344
- window.addEventListener("pagehide", this.disconnect);
345
380
  return;
346
381
  } catch (error) {
347
382
  await client.disconnect().catch((disconnectError) => console.error("Phone disconnect failed", disconnectError));
@@ -351,6 +386,7 @@ var Phone = class {
351
386
  }
352
387
  } catch (error) {
353
388
  this.update({
389
+ owner: false,
354
390
  status: "error",
355
391
  error: error instanceof Error ? error.message : String(error)
356
392
  });
@@ -360,13 +396,16 @@ var Phone = class {
360
396
  }
361
397
  }
362
398
  disconnect = () => {
399
+ this.connectionRequested = false;
363
400
  this.lockAbortController?.abort();
364
401
  this.lockAbortController = null;
365
402
  this.connectResolve?.();
366
403
  this.connectResolve = null;
367
404
  if (this.pollTimer) clearTimeout(this.pollTimer);
368
405
  this.pollTimer = null;
369
- window.removeEventListener("pagehide", this.disconnect);
406
+ window.removeEventListener("pagehide", this.pageHide);
407
+ window.removeEventListener("pageshow", this.pageShow);
408
+ this.reconnectOnPageShow = false;
370
409
  window.removeEventListener("beforeunload", this.beforeUnload);
371
410
  this.client?.disconnect().catch((error) => console.error("Phone disconnect failed", error));
372
411
  this.client = null;
@@ -377,20 +416,35 @@ var Phone = class {
377
416
  this.pendingCommands.clear();
378
417
  this.releaseLock?.();
379
418
  this.releaseLock = null;
419
+ this.currentCall = null;
420
+ this.incomingCall = null;
380
421
  this.update({
422
+ owner: false,
381
423
  status: "off",
424
+ incoming: null,
382
425
  active: null
383
426
  });
384
427
  this.ownsPhone = false;
385
428
  this.channel?.close();
386
429
  this.channel = null;
387
430
  };
431
+ pageHide = () => {
432
+ const reconnect = this.connectionRequested;
433
+ this.disconnect();
434
+ this.reconnectOnPageShow = reconnect;
435
+ if (reconnect) window.addEventListener("pageshow", this.pageShow);
436
+ };
437
+ pageShow = (event) => {
438
+ if (!event.persisted || !this.reconnectOnPageShow) return;
439
+ this.reconnectOnPageShow = false;
440
+ this.connect();
441
+ };
388
442
  beforeUnload = (event) => {
389
443
  event.preventDefault();
390
444
  event.returnValue = "";
391
445
  };
392
446
  call = async ({ numberId, to }) => {
393
- if (this.callStarting || this.state.active) throw new Error("A call is already in progress.");
447
+ if (this.callStarting || this.state.active || this.incomingCall || this.state.incoming) throw new Error("A call is already in progress.");
394
448
  if (this.state.status !== "ready") throw new Error("The phone is not ready.");
395
449
  if (!this.ownsPhone) return this.sendCommand("call", {
396
450
  numberId,
@@ -417,6 +471,7 @@ var Phone = class {
417
471
  this.update({
418
472
  error: null,
419
473
  active: {
474
+ direction: "outbound",
420
475
  callId,
421
476
  to,
422
477
  from: callerNumber,
@@ -432,6 +487,62 @@ var Phone = class {
432
487
  this.callStarting = false;
433
488
  }
434
489
  };
490
+ answer = async () => {
491
+ if (!this.state.incoming) throw new Error("There is no incoming call.");
492
+ if (!this.ownsPhone) return this.sendCommand("answer", this.state.incoming.id);
493
+ const call = this.incomingCall;
494
+ if (!call) throw new Error("There is no incoming call.");
495
+ if (!this.client) throw new Error("The phone is not ready.");
496
+ this.audio ??= document.createElement("audio");
497
+ this.audio.autoplay = true;
498
+ this.audio.hidden = true;
499
+ if (!this.audio.isConnected) document.body.append(this.audio);
500
+ this.client.remoteElement = this.audio;
501
+ call.options.remoteElement = this.audio;
502
+ this.currentCall = call;
503
+ this.incomingCall = null;
504
+ this.update({
505
+ incoming: null,
506
+ error: null,
507
+ active: {
508
+ direction: "inbound",
509
+ from: this.state.incoming.from,
510
+ state: "connecting",
511
+ muted: false,
512
+ startedAt: Date.now(),
513
+ recording: false
514
+ }
515
+ });
516
+ window.addEventListener("beforeunload", this.beforeUnload);
517
+ try {
518
+ await call.answer();
519
+ } catch (error) {
520
+ if (this.currentCall === call) {
521
+ this.currentCall = null;
522
+ this.incomingCall = call;
523
+ this.update({
524
+ active: null,
525
+ incoming: {
526
+ id: call.id,
527
+ from: call.options.remoteCallerNumber ?? ""
528
+ }
529
+ });
530
+ window.removeEventListener("beforeunload", this.beforeUnload);
531
+ }
532
+ throw error;
533
+ }
534
+ };
535
+ decline = async () => {
536
+ if (!this.state.incoming) throw new Error("There is no incoming call.");
537
+ if (!this.ownsPhone) return this.sendCommand("decline", this.state.incoming.id);
538
+ const call = this.incomingCall;
539
+ if (!call) throw new Error("There is no incoming call.");
540
+ await call.hangup();
541
+ if (this.incomingCall === call) {
542
+ this.incomingCall = null;
543
+ this.update({ incoming: null });
544
+ }
545
+ };
435
546
  sendCommand(action, value) {
436
547
  const id = crypto.randomUUID();
437
548
  return new Promise((resolve, reject) => {
@@ -462,7 +573,10 @@ var Phone = class {
462
573
  else if (data.type === "state" && !this.ownsPhone && "state" in data) {
463
574
  const state = data.state;
464
575
  if (typeof state !== "object" || state === null || !("status" in state)) return;
465
- this.state = state;
576
+ this.state = {
577
+ ...state,
578
+ owner: false
579
+ };
466
580
  this.listeners.forEach((listener) => listener());
467
581
  this.connectResolve?.();
468
582
  this.connectResolve = null;
@@ -483,6 +597,10 @@ var Phone = class {
483
597
  }));
484
598
  };
485
599
  async runCommand(data) {
600
+ if (data.action === "answer" || data.action === "decline") {
601
+ if (!this.incomingCall || data.value !== this.incomingCall.id) throw new Error("That call is no longer ringing.");
602
+ return data.action === "answer" ? this.answer() : this.decline();
603
+ }
486
604
  if (data.action === "call") {
487
605
  const value = data.value;
488
606
  if (!value || typeof value !== "object" || !("numberId" in value) || !("to" in value) || typeof value.numberId !== "string" || typeof value.to !== "string") throw new Error("Invalid call command.");
@@ -518,7 +636,7 @@ var Phone = class {
518
636
  }
519
637
  }
520
638
  async pollCall(callId) {
521
- if (this.state.active?.callId !== callId || this.state.active.state === "active") return;
639
+ if (this.state.active?.direction !== "outbound" || this.state.active.callId !== callId || this.state.active.state === "active") return;
522
640
  try {
523
641
  const { status } = await this.api.getCall(callId);
524
642
  if ([
@@ -533,7 +651,8 @@ var Phone = class {
533
651
  } catch (error) {
534
652
  console.error("Call status check failed", error);
535
653
  }
536
- if (this.getSnapshot().active?.callId === callId) this.pollTimer = setTimeout(() => void this.pollCall(callId), 1e4);
654
+ const active = this.getSnapshot().active;
655
+ if (active?.direction === "outbound" && active.callId === callId) this.pollTimer = setTimeout(() => void this.pollCall(callId), 1e4);
537
656
  }
538
657
  onCallUpdate(call) {
539
658
  const active = this.state.active;
@@ -562,15 +681,15 @@ var Phone = class {
562
681
  return;
563
682
  }
564
683
  const mapped = state === "active" ? "active" : state === "held" ? "held" : state === "ringing" ? "ringing" : "connecting";
565
- if (mapped === "active" && active.recording && !this.stopRecording && !this.recordingPromise) {
684
+ if (mapped === "active" && active.direction === "outbound" && active.recording && !this.stopRecording && !this.recordingPromise) {
566
685
  this.recordingPromise = startRecording(active.callId, call, this.api, (error) => {
567
686
  console.error("Call recording failed", error);
568
- if (this.state.active?.callId === active.callId) this.update({ active: {
687
+ if (this.state.active?.direction === "outbound" && this.state.active.callId === active.callId) this.update({ active: {
569
688
  ...this.state.active,
570
689
  recording: false
571
690
  } });
572
691
  }).then((stop) => {
573
- if (this.state.active?.callId === active.callId) this.stopRecording = stop;
692
+ if (this.state.active?.direction === "outbound" && this.state.active.callId === active.callId) this.stopRecording = stop;
574
693
  return stop;
575
694
  });
576
695
  this.recordingPromise.catch((error) => {
@@ -1,5 +1,5 @@
1
1
  import { n as fetchWithOrgAuth } from "./workspace-DruAZPmV.js";
2
- import { t as loadAppletConfig } from "./load-config-OKZU3UKn.js";
2
+ import { t as loadAppletConfig } from "./load-config-DI-e1Ool.js";
3
3
  import { t as question } from "./prompt-ChS-grYO.js";
4
4
  import { z } from "zod";
5
5
  //#region src/variables.ts
package/dist/vite.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as RegisteredAppletSourceDefinition } from "./manifest-I-9s8R36.js";
1
+ import { o as RegisteredAppletSourceDefinition } from "./manifest-C6_F4bL8.js";
2
2
  import { PluginOption } from "vite";
3
3
  //#region ../shared/auth/src/store.d.mts
4
4
  interface SessionCredentials {
package/dist/vite.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { n as meSchema } from "./me-BW7Paary.js";
2
- import { i as APPLET_CODE_PATTERN, n as appletManifestSchema, r as appletSourceDefinitionSchema } from "./manifest-4ZAc6HUk.js";
2
+ import { i as APPLET_CODE_PATTERN, n as appletManifestSchema, r as appletSourceDefinitionSchema } from "./manifest-CJSL9GuC.js";
3
3
  import { a as openQueryCacheDatabase } from "./query-cache-CXmAyLv3.js";
4
4
  import { a as workflowOutcomeSchema, c as HITCH_SESSION_HEADER, l as HITCH_USER_HEADER, n as runWorkflowJournal } from "./workflow-engine-MogejNfF.js";
5
5
  import { c as bearerToken, d as resolveAuthForOrg, f as resolveInstanceUrl, l as organizationInstanceUrl, t as resolveWorkspaceOrg, u as rejectEnvironmentKey } from "./workspace-DruAZPmV.js";
6
- import { c as writeClientEntry, i as writeRegisterDeclaration, l as writeManifestGen, r as syncContract, s as writeAppletRoot, u as writeServerEntry } from "./generate-XB-72Fzj.js";
6
+ import { c as writeClientEntry, i as writeRegisterDeclaration, l as writeManifestGen, r as syncContract, s as writeAppletRoot, u as writeServerEntry } from "./generate-BDHb2kjw.js";
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { appendFile, readdir, rm, writeFile } from "node:fs/promises";
@@ -473,7 +473,7 @@ function applet(sourceDefinition, options = {}) {
473
473
  const routesDirectory = resolve(resolvedRoot, "src/client/routes");
474
474
  const hasClient = existsSync(routesDirectory);
475
475
  const hasServer = existsSync(resolve(resolvedRoot, "src/server/main.ts"));
476
- if (!hasServer && parsedDefinition.data.telephony?.events.length) throw new Error("An applet with telephony events must have src/server/main.ts.");
476
+ if (!hasServer && parsedDefinition.data.telephony?.events?.length) throw new Error("An applet with telephony events must have src/server/main.ts.");
477
477
  if (!hasServer && (parsedDefinition.data.schedules?.length ?? 0) > 0) throw new Error("An applet with schedules must have src/server/main.ts.");
478
478
  const legacyMain = resolve(resolvedRoot, "src/client/main.tsx");
479
479
  const indexHtml = resolve(resolvedRoot, "index.html");
@@ -504,6 +504,11 @@ function applet(sourceDefinition, options = {}) {
504
504
  org: resolveWorkspaceOrg(callerRoot),
505
505
  ...parsedDefinition.data
506
506
  });
507
+ const clsxFromCn = {
508
+ name: "hitch-clsx",
509
+ resolveId: (id) => id === "clsx" ? "\0hitch:clsx" : void 0,
510
+ load: (id) => id === "\0hitch:clsx" ? "export { clsx, clsx as default } from \"cn\";" : void 0
511
+ };
507
512
  const orchestrator = {
508
513
  name: "hitch-applet",
509
514
  enforce: "pre",
@@ -524,7 +529,7 @@ function applet(sourceDefinition, options = {}) {
524
529
  clearedOutDir = true;
525
530
  }
526
531
  if (environment.command === "build") themeStylesheetUrl = `${organizationInstanceUrl(await resolveInstanceUrl(void 0, options.store), manifest.org)}/theme.css`;
527
- if (environment.command === "build" && manifest.objects.length > 0) {
532
+ if (environment.command === "build") {
528
533
  contractSync ??= syncContract(resolve(root, ".hitch"), manifest, options);
529
534
  await contractSync;
530
535
  }
@@ -608,9 +613,10 @@ function applet(sourceDefinition, options = {}) {
608
613
  base: `/${manifest.code}/`,
609
614
  test: { include: ["test/**/*.test.{ts,tsx}"] },
610
615
  resolve: { alias: {
611
- clsx: "cn",
616
+ "clsx/lite": "cn/lite",
612
617
  "tailwind-merge": "cn"
613
618
  } },
619
+ optimizeDeps: { rolldownOptions: { plugins: [clsxFromCn] } },
614
620
  ...hasServer && { environments: { worker: { build: { rollupOptions: { output: { codeSplitting: false } } } } } }
615
621
  };
616
622
  },
@@ -801,9 +807,14 @@ function applet(sourceDefinition, options = {}) {
801
807
  ...plugin,
802
808
  apply: ((_config, { mode }) => mode !== "test")
803
809
  }));
810
+ const hasClientSource = existsSync(resolve("src/client/routes"));
804
811
  return [
805
812
  orchestrator,
806
- ...existsSync(resolve("src/client/routes")) ? [tanstackRouter({
813
+ {
814
+ ...clsxFromCn,
815
+ enforce: "pre"
816
+ },
817
+ ...hasClientSource ? [tanstackRouter({
807
818
  generatedRouteTree: ".hitch/routeTree.gen.ts",
808
819
  routesDirectory: "src/client/routes",
809
820
  routeFileIgnorePattern: "__root",