@plotday/twister 0.35.0 → 0.36.0

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 (66) hide show
  1. package/bin/templates/AGENTS.template.md +187 -182
  2. package/cli/templates/AGENTS.template.md +187 -182
  3. package/dist/docs/assets/hierarchy.js +1 -1
  4. package/dist/docs/assets/navigation.js +1 -1
  5. package/dist/docs/assets/search.js +1 -1
  6. package/dist/docs/classes/index.Options.html +10 -0
  7. package/dist/docs/classes/tool.ITool.html +1 -1
  8. package/dist/docs/classes/tools_integrations.Integrations.html +4 -4
  9. package/dist/docs/classes/tools_network.Network.html +1 -1
  10. package/dist/docs/classes/tools_plot.Plot.html +1 -1
  11. package/dist/docs/classes/tools_store.Store.html +1 -1
  12. package/dist/docs/classes/tools_tasks.Tasks.html +1 -1
  13. package/dist/docs/enums/tools_integrations.AuthProvider.html +11 -11
  14. package/dist/docs/hierarchy.html +1 -1
  15. package/dist/docs/interfaces/utils_types.ToolShed.html +5 -5
  16. package/dist/docs/modules/index.html +1 -1
  17. package/dist/docs/types/index.BooleanDef.html +7 -0
  18. package/dist/docs/types/index.NumberDef.html +9 -0
  19. package/dist/docs/types/index.OptionDef.html +2 -0
  20. package/dist/docs/types/index.OptionsSchema.html +3 -0
  21. package/dist/docs/types/index.ResolvedOptions.html +4 -0
  22. package/dist/docs/types/index.SelectDef.html +8 -0
  23. package/dist/docs/types/index.TextDef.html +8 -0
  24. package/dist/docs/types/tools_integrations.AuthToken.html +4 -4
  25. package/dist/docs/types/tools_integrations.Authorization.html +4 -4
  26. package/dist/docs/types/tools_integrations.IntegrationOptions.html +2 -2
  27. package/dist/docs/types/tools_integrations.IntegrationProviderConfig.html +6 -6
  28. package/dist/docs/types/tools_integrations.Syncable.html +4 -2
  29. package/dist/docs/types/utils_types.BuiltInTools.html +2 -2
  30. package/dist/docs/types/utils_types.ExtractBuildReturn.html +1 -1
  31. package/dist/docs/types/utils_types.InferOptions.html +1 -1
  32. package/dist/docs/types/utils_types.InferTools.html +1 -1
  33. package/dist/docs/types/utils_types.JSONValue.html +1 -1
  34. package/dist/docs/types/utils_types.PromiseValues.html +1 -1
  35. package/dist/docs/types/utils_types.ToolBuilder.html +2 -2
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/llm-docs/index.d.ts.map +1 -1
  41. package/dist/llm-docs/index.js +2 -0
  42. package/dist/llm-docs/index.js.map +1 -1
  43. package/dist/llm-docs/options.d.ts +9 -0
  44. package/dist/llm-docs/options.d.ts.map +1 -0
  45. package/dist/llm-docs/options.js +8 -0
  46. package/dist/llm-docs/options.js.map +1 -0
  47. package/dist/llm-docs/tools/integrations.d.ts +1 -1
  48. package/dist/llm-docs/tools/integrations.d.ts.map +1 -1
  49. package/dist/llm-docs/tools/integrations.js +1 -1
  50. package/dist/llm-docs/tools/integrations.js.map +1 -1
  51. package/dist/llm-docs/twist-guide-template.d.ts +1 -1
  52. package/dist/llm-docs/twist-guide-template.d.ts.map +1 -1
  53. package/dist/llm-docs/twist-guide-template.js +1 -1
  54. package/dist/llm-docs/twist-guide-template.js.map +1 -1
  55. package/dist/options.d.ts +104 -0
  56. package/dist/options.d.ts.map +1 -0
  57. package/dist/options.js +40 -0
  58. package/dist/options.js.map +1 -0
  59. package/dist/tools/integrations.d.ts +2 -0
  60. package/dist/tools/integrations.d.ts.map +1 -1
  61. package/dist/tools/integrations.js.map +1 -1
  62. package/dist/twist-guide.d.ts +1 -1
  63. package/dist/twist-guide.d.ts.map +1 -1
  64. package/dist/utils/types.d.ts +5 -1
  65. package/dist/utils/types.d.ts.map +1 -1
  66. package/package.json +6 -1
@@ -70,28 +70,41 @@ New event/task/conversation?
70
70
  ```typescript
71
71
  import {
72
72
  type Activity,
73
+ type NewActivityWithNotes,
74
+ type ActivityFilter,
73
75
  type Priority,
74
76
  type ToolBuilder,
75
- twist,
77
+ Twist,
78
+ ActivityType,
76
79
  } from "@plotday/twister";
77
- import { Plot } from "@plotday/twister/tools/plot";
78
- import { Uuid } from "@plotday/twister/utils/uuid";
80
+ import { ActivityAccess, Plot } from "@plotday/twister/tools/plot";
81
+ // Import your tools:
82
+ // import { GoogleCalendar } from "@plotday/tool-google-calendar";
83
+ // import { Linear } from "@plotday/tool-linear";
79
84
 
80
85
  export default class MyTwist extends Twist<MyTwist> {
81
86
  build(build: ToolBuilder) {
82
87
  return {
83
- plot: build(Plot),
88
+ // myTool: build(MyTool, {
89
+ // onItem: this.handleItem,
90
+ // onSyncableDisabled: this.onSyncableDisabled,
91
+ // }),
92
+ plot: build(Plot, {
93
+ activity: { access: ActivityAccess.Create },
94
+ }),
84
95
  };
85
96
  }
86
97
 
87
- async activate(priority: Pick<Priority, "id">) {
88
- // Called when twist is enabled for a priority
89
- // Common actions: request auth, create setup activities
98
+ async activate(_priority: Pick<Priority, "id">) {
99
+ // Auth and resource selection handled in the twist edit modal.
90
100
  }
91
101
 
92
- async activity(activity: Activity) {
93
- // Called when an activity is routed to this twist
94
- // Common actions: process external events, update activities
102
+ async handleItem(activity: NewActivityWithNotes): Promise<void> {
103
+ await this.tools.plot.createActivity(activity);
104
+ }
105
+
106
+ async onSyncableDisabled(filter: ActivityFilter): Promise<void> {
107
+ await this.tools.plot.updateActivity({ match: filter, archived: true });
95
108
  }
96
109
  }
97
110
  ```
@@ -145,76 +158,111 @@ Add tool dependencies to `package.json`:
145
158
  }
146
159
  ```
147
160
 
148
- #### Common External Tools
161
+ #### Available External Tools
149
162
 
150
- - `@plotday/tool-google-calendar`: Google Calendar integration
151
- - `@plotday/tool-outlook-calendar`: Outlook Calendar integration
152
- - `@plotday/tool-google-contacts`: Google Contacts integration
163
+ - `@plotday/tool-google-calendar`: Google Calendar sync (CalendarTool)
164
+ - `@plotday/tool-outlook-calendar`: Outlook Calendar sync (CalendarTool)
165
+ - `@plotday/tool-google-contacts`: Google Contacts sync (supporting tool)
166
+ - `@plotday/tool-google-drive`: Google Drive sync (DocumentTool)
167
+ - `@plotday/tool-gmail`: Gmail sync (MessagingTool)
168
+ - `@plotday/tool-slack`: Slack sync (MessagingTool)
169
+ - `@plotday/tool-linear`: Linear sync (ProjectTool)
170
+ - `@plotday/tool-jira`: Jira sync (ProjectTool)
171
+ - `@plotday/tool-asana`: Asana sync (ProjectTool)
153
172
 
154
173
  ## Lifecycle Methods
155
174
 
156
175
  ### activate(priority: Pick<Priority, "id">)
157
176
 
158
- Called when the twist is enabled for a priority. Common patterns:
177
+ Called when the twist is enabled for a priority. Auth and resource selection are handled automatically via the twist edit modal when using external tools with Integrations.
159
178
 
160
- **Request Authentication:**
179
+ Most twists have an empty or minimal `activate()`:
161
180
 
162
181
  ```typescript
163
182
  async activate(_priority: Pick<Priority, "id">) {
164
- const authLink = await this.tools.externalTool.requestAuth(
165
- this.onAuthComplete,
166
- "google"
167
- );
168
-
169
- await this.tools.plot.createActivity({
170
- type: ActivityType.Note,
171
- title: "Connect your account",
172
- notes: [
173
- {
174
- content: "Click the link below to connect your account and start syncing.",
175
- links: [authLink],
176
- },
177
- ],
178
- });
183
+ // Auth and resource selection are handled in the twist edit modal.
184
+ // Only add custom initialization here if needed.
179
185
  }
180
186
  ```
181
187
 
182
- **Store Parent Activity for Later:**
188
+ **Store Parent Activity for Later (optional):**
183
189
 
184
190
  ```typescript
185
- const activity = await this.tools.plot.createActivity({
186
- type: ActivityType.Note,
187
- title: "Setup",
188
- notes: [
189
- {
190
- content: "Your twist is being set up. Configuration steps will appear here.",
191
- },
192
- ],
193
- });
194
-
195
- await this.set("setup_activity_id", activity.id);
191
+ async activate(_priority: Pick<Priority, "id">) {
192
+ const activityId = await this.tools.plot.createActivity({
193
+ type: ActivityType.Note,
194
+ title: "Setup complete",
195
+ notes: [{
196
+ content: "Your twist is ready. Activities will appear as they sync.",
197
+ }],
198
+ });
199
+ await this.set("setup_activity_id", activityId);
200
+ }
196
201
  ```
197
202
 
198
- ### activity(activity: Activity)
203
+ ### Event Callbacks (via build options)
199
204
 
200
- Called when an activity is routed to the twist. Common patterns:
205
+ Twists respond to events through callbacks declared in `build()`:
201
206
 
202
- **Create Activities from External Events:**
207
+ **Receive synced items from a tool (most common):**
203
208
 
204
209
  ```typescript
205
- async activity(activity: Activity) {
210
+ build(build: ToolBuilder) {
211
+ return {
212
+ myTool: build(MyTool, {
213
+ onItem: this.handleItem,
214
+ onSyncableDisabled: this.onSyncableDisabled,
215
+ }),
216
+ plot: build(Plot, { activity: { access: ActivityAccess.Create } }),
217
+ };
218
+ }
219
+
220
+ async handleItem(activity: NewActivityWithNotes): Promise<void> {
206
221
  await this.tools.plot.createActivity(activity);
207
222
  }
223
+
224
+ async onSyncableDisabled(filter: ActivityFilter): Promise<void> {
225
+ await this.tools.plot.updateActivity({ match: filter, archived: true });
226
+ }
208
227
  ```
209
228
 
210
- **Update Based on User Action:**
229
+ **React to activity changes (for two-way sync):**
211
230
 
212
231
  ```typescript
213
- async activity(activity: Activity) {
214
- if (activity.completed) {
215
- await this.handleCompletion(activity);
216
- }
232
+ plot: build(Plot, {
233
+ activity: {
234
+ access: ActivityAccess.Create,
235
+ updated: this.onActivityUpdated,
236
+ },
237
+ note: {
238
+ created: this.onNoteCreated,
239
+ },
240
+ }),
241
+
242
+ async onActivityUpdated(activity: Activity, changes: { tagsAdded, tagsRemoved }): Promise<void> {
243
+ const tool = this.getToolForActivity(activity);
244
+ if (tool?.updateIssue) await tool.updateIssue(activity);
217
245
  }
246
+
247
+ async onNoteCreated(note: Note): Promise<void> {
248
+ if (note.author.type === ActorType.Twist) return; // Prevent loops
249
+ // Sync note to external service as a comment
250
+ }
251
+ ```
252
+
253
+ **Respond to mentions (AI twist pattern):**
254
+
255
+ ```typescript
256
+ plot: build(Plot, {
257
+ activity: { access: ActivityAccess.Respond },
258
+ note: {
259
+ intents: [{
260
+ description: "Respond to general questions",
261
+ examples: ["What's the weather?", "Help me plan my week"],
262
+ handler: this.respond,
263
+ }],
264
+ },
265
+ }),
218
266
  ```
219
267
 
220
268
  ## Activity Links
@@ -224,19 +272,19 @@ Activity links enable user interaction:
224
272
  ```typescript
225
273
  import { type ActivityLink, ActivityLinkType } from "@plotday/twister";
226
274
 
227
- // URL link
275
+ // External URL link
228
276
  const urlLink: ActivityLink = {
229
277
  title: "Open website",
230
- type: ActivityLinkType.url,
278
+ type: ActivityLinkType.external,
231
279
  url: "https://example.com",
232
280
  };
233
281
 
234
- // Callback link (uses Callbacks tool)
235
- const token = await this.callback(this.onLinkClicked, "context");
282
+ // Callback link (uses Callbacks tool — use linkCallback, not callback)
283
+ const token = await this.linkCallback(this.onLinkClicked, "context");
236
284
  const callbackLink: ActivityLink = {
237
285
  title: "Click me",
238
286
  type: ActivityLinkType.callback,
239
- token: token,
287
+ callback: token,
240
288
  };
241
289
 
242
290
  // Add to activity note
@@ -250,70 +298,87 @@ await this.tools.plot.createActivity({
250
298
  },
251
299
  ],
252
300
  });
301
+
302
+ // Callback handler receives the ActivityLink as first argument
303
+ async onLinkClicked(link: ActivityLink, context: string): Promise<void> {
304
+ // Handle link click
305
+ }
253
306
  ```
254
307
 
255
308
  ## Authentication Pattern
256
309
 
257
- Common pattern for OAuth authentication:
310
+ Auth is handled automatically via the Integrations tool. Tools declare their OAuth provider in `build()`, and users connect in the twist edit modal. **You do not need to create auth activities manually.**
258
311
 
259
312
  ```typescript
260
- async activate(_priority: Pick<Priority, "id">) {
261
- // Request auth link from tool with callback
262
- const authLink = await this.tools.googleTool.requestAuth(
263
- this.onAuthComplete,
264
- "google"
265
- );
266
-
267
- // Create activity with auth link
268
- const activity = await this.tools.plot.createActivity({
269
- type: ActivityType.Note,
270
- title: "Connect Google account",
271
- notes: [
272
- {
273
- content: "Click below to connect your Google account and start syncing.",
274
- links: [authLink],
275
- },
276
- ],
277
- });
278
-
279
- // Store for later use
280
- await this.set("auth_activity_id", activity.id);
313
+ // In your tool's build() method:
314
+ build(build: ToolBuilder) {
315
+ return {
316
+ integrations: build(Integrations, {
317
+ providers: [{
318
+ provider: AuthProvider.Google,
319
+ scopes: ["https://www.googleapis.com/auth/calendar"],
320
+ getSyncables: this.getSyncables, // List available resources after auth
321
+ onSyncEnabled: this.onSyncEnabled, // User enabled a resource
322
+ onSyncDisabled: this.onSyncDisabled, // User disabled a resource
323
+ }],
324
+ }),
325
+ // ...
326
+ };
281
327
  }
282
328
 
283
- async onAuthComplete(authResult: { authToken: string }, provider: string) {
284
- // Store auth token
285
- await this.set(`${provider}_auth`, authResult.authToken);
329
+ // Get a token for API calls:
330
+ const token = await this.tools.integrations.get(AuthProvider.Google, syncableId);
331
+ if (!token) throw new Error("No auth token available");
332
+ const client = new ApiClient({ accessToken: token.token });
333
+ ```
286
334
 
287
- // Continue setup flow
288
- await this.setupSyncOptions(authResult.authToken);
289
- }
335
+ For per-user write-backs (e.g., RSVP, comments attributed to the acting user):
336
+
337
+ ```typescript
338
+ await this.tools.integrations.actAs(
339
+ AuthProvider.Google,
340
+ actorId, // The user who performed the action
341
+ activityId, // Activity to prompt for auth if needed
342
+ this.performWriteBack,
343
+ ...extraArgs
344
+ );
290
345
  ```
291
346
 
292
347
  ## Sync Pattern
293
348
 
294
- ### Recommended: Upsert via Source/Key (Strategy 2)
349
+ ### Recommended: Using External Tools with SyncToolOptions
295
350
 
296
- Pattern for syncing external data using automatic upserts - **no manual ID tracking needed**:
351
+ Most twists use external tools (CalendarTool, ProjectTool, etc.) that handle sync internally. The twist just receives `NewActivityWithNotes` objects and saves them:
297
352
 
298
353
  ```typescript
299
- async startSync(calendarId: string): Promise<void> {
300
- const authToken = await this.get<string>("auth_token");
301
-
302
- await this.tools.calendarTool.startSync(
303
- authToken,
304
- calendarId,
305
- this.handleEvent,
306
- calendarId
307
- );
354
+ build(build: ToolBuilder) {
355
+ return {
356
+ calendarTool: build(GoogleCalendar, {
357
+ onItem: this.handleEvent, // Receives synced items
358
+ onSyncableDisabled: this.onSyncableDisabled, // Clean up when disabled
359
+ }),
360
+ plot: build(Plot, { activity: { access: ActivityAccess.Create } }),
361
+ };
308
362
  }
309
363
 
310
- async handleEvent(
311
- event: ExternalEvent,
312
- calendarId: string
313
- ): Promise<void> {
314
- // Use the event's canonical URL as the source for automatic deduplication
364
+ // Tools deliver NewActivityWithNotes — twist saves them
365
+ async handleEvent(activity: NewActivityWithNotes): Promise<void> {
366
+ await this.tools.plot.createActivity(activity);
367
+ }
368
+
369
+ async onSyncableDisabled(filter: ActivityFilter): Promise<void> {
370
+ await this.tools.plot.updateActivity({ match: filter, archived: true });
371
+ }
372
+ ```
373
+
374
+ ### Custom Sync: Upsert via Source/Key (Strategy 2)
375
+
376
+ For direct API integration without an external tool, use source/key for automatic upserts:
377
+
378
+ ```typescript
379
+ async handleEvent(event: ExternalEvent): Promise<void> {
315
380
  const activity: NewActivityWithNotes = {
316
- source: event.htmlLink, // or event.url, depending on your external system
381
+ source: event.htmlLink, // Canonical URL for automatic deduplication
317
382
  type: ActivityType.Event,
318
383
  title: event.summary || "(No title)",
319
384
  start: event.start?.dateTime || event.start?.date || null,
@@ -321,36 +386,17 @@ async handleEvent(
321
386
  notes: [],
322
387
  };
323
388
 
324
- // Add description as an upsertable note
325
389
  if (event.description) {
326
390
  activity.notes.push({
327
391
  activity: { source: event.htmlLink },
328
- key: "description", // This key enables upserts - same key updates the note
392
+ key: "description", // This key enables note-level upserts
329
393
  content: event.description,
330
394
  });
331
395
  }
332
396
 
333
- // Add attendees as an upsertable note
334
- if (event.attendees?.length) {
335
- const attendeeList = event.attendees
336
- .map(a => `- ${a.email}${a.displayName ? ` (${a.displayName})` : ''}`)
337
- .join('\n');
338
-
339
- activity.notes.push({
340
- activity: { source: event.htmlLink },
341
- key: "attendees", // Different key for different note types
342
- content: `## Attendees\n${attendeeList}`,
343
- });
344
- }
345
-
346
- // Create or update - Plot automatically handles deduplication based on source
397
+ // Create or update Plot handles deduplication automatically
347
398
  await this.tools.plot.createActivity(activity);
348
399
  }
349
-
350
- async stopSync(calendarId: string): Promise<void> {
351
- const authToken = await this.get<string>("auth_token");
352
- await this.tools.calendarTool.stopSync(authToken, calendarId);
353
- }
354
400
  ```
355
401
 
356
402
  ### Advanced: Generate and Store IDs (Strategy 3)
@@ -397,61 +443,20 @@ async handleEventAdvanced(
397
443
  }
398
444
  ```
399
445
 
400
- ## Calendar Selection Pattern
446
+ ## Resource Selection
401
447
 
402
- Pattern for letting users select from multiple calendars/accounts:
448
+ Resource selection (calendars, projects, channels) is handled automatically in the twist edit modal via the Integrations tool. Users see a list of available resources returned by your tool's `getSyncables()` method and toggle them on/off. You do **not** need to build custom selection UI.
403
449
 
404
450
  ```typescript
405
- private async createCalendarSelectionActivity(
406
- provider: string,
407
- calendars: Calendar[],
408
- authToken: string
409
- ): Promise<void> {
410
- const links: ActivityLink[] = [];
411
-
412
- for (const calendar of calendars) {
413
- const token = await this.callback(
414
- this.onCalendarSelected,
415
- provider,
416
- calendar.id,
417
- calendar.name,
418
- authToken
419
- );
420
-
421
- links.push({
422
- title: `📅 ${calendar.name}${calendar.primary ? " (Primary)" : ""}`,
423
- type: ActivityLinkType.callback,
424
- token: token,
425
- });
426
- }
427
-
428
- await this.tools.plot.createActivity({
429
- type: ActivityType.Note,
430
- title: "Which calendars would you like to connect?",
431
- notes: [
432
- {
433
- content: "Select the calendars you want to sync:",
434
- links,
435
- },
436
- ],
437
- });
438
- }
439
-
440
- async onCalendarSelected(
441
- link: ActivityLink,
442
- provider: string,
443
- calendarId: string,
444
- calendarName: string,
445
- authToken: string
446
- ): Promise<void> {
447
- // Start sync for selected calendar
448
- await this.tools.tool.startSync(
449
- authToken,
450
- calendarId,
451
- this.handleEvent,
452
- provider,
453
- calendarId
454
- );
451
+ // In your tool:
452
+ async getSyncables(_auth: Authorization, token: AuthToken): Promise<Syncable[]> {
453
+ const client = new ApiClient({ accessToken: token.token });
454
+ const calendars = await client.listCalendars();
455
+ return calendars.map(c => ({
456
+ id: c.id,
457
+ title: c.name,
458
+ children: c.subCalendars?.map(sc => ({ id: sc.id, title: sc.name })),
459
+ }));
455
460
  }
456
461
  ```
457
462
 
@@ -486,7 +491,7 @@ async startSync(resourceId: string): Promise<void> {
486
491
  await this.runTask(callback);
487
492
  }
488
493
 
489
- async syncBatch(args: any, resourceId: string): Promise<void> {
494
+ async syncBatch(resourceId: string): Promise<void> {
490
495
  // Load state from Store (set by previous execution)
491
496
  const state = await this.get(`sync_state_${resourceId}`);
492
497
 
@@ -506,7 +511,7 @@ async syncBatch(args: any, resourceId: string): Promise<void> {
506
511
  key: "description", // Use key for upsertable notes
507
512
  content: item.description,
508
513
  }],
509
- unread: !state.initialSync, // false for initial sync, true for incremental
514
+ ...(state.initialSync ? { unread: false } : {}), // false for initial, omit for incremental
510
515
  ...(state.initialSync ? { archived: false } : {}), // unarchive on initial only
511
516
  });
512
517
  }
@@ -551,7 +556,7 @@ All sync-based tools should distinguish between initial sync (first import) and
551
556
 
552
557
  | Field | Initial Sync | Incremental Sync | Reason |
553
558
  |-------|--------------|------------------|---------|
554
- | `unread` | `false` | `true` | Avoid notification overload from historical items |
559
+ | `unread` | `false` | *omit* | Initial: mark read for all. Incremental: auto-mark read for author only |
555
560
  | `archived` | `false` | *omit* | Unarchive on install, preserve user choice on updates |
556
561
 
557
562
  **Example:**
@@ -560,14 +565,14 @@ const activity: NewActivity = {
560
565
  type: ActivityType.Event,
561
566
  source: event.url,
562
567
  title: event.title,
563
- unread: !initialSync, // false for initial, true for incremental
564
- ...(initialSync ? { archived: false } : {}), // unarchive on initial only
568
+ ...(initialSync ? { unread: false } : {}), // false for initial, omit for incremental
569
+ ...(initialSync ? { archived: false } : {}), // unarchive on initial only
565
570
  };
566
571
  ```
567
572
 
568
573
  **Why this matters:**
569
- - **Initial sync**: Activities are unarchived and marked as read, preventing spam from bulk historical imports
570
- - **Incremental sync**: New activities appear as unread, and archived state is preserved (respects user's archiving decisions)
574
+ - **Initial sync**: Activities are unarchived and marked as read for all users, preventing spam from bulk historical imports
575
+ - **Incremental sync**: Activities are auto-marked read for the author (twist owner), unread for everyone else. Archived state is preserved
571
576
  - **Reinstall**: Acts as initial sync, so previously archived activities are unarchived (fresh start)
572
577
 
573
578
  ### Two-Way Sync: Avoiding Race Conditions
@@ -1 +1 @@
1
- window.hierarchyData = "eJyNkk1PxCAQhv/LnGex9GNLuW089WJM9GY2BrvoNmWLgTF72PS/G1o1eIITCTzzPgPMDZy15EG+8EIcEZx+N3qg0c4e5A14IcIyq4sGCf2ztQYQpnE+geSlQPhyBiQMRnmv/R1Za9hKsTNdArqegATyp10o220bCMN5NCen52DmHOuuwaZssC0EtqJEUdUo2ga7fYmdqI4LAuc86iWrlYxOFoS6a+Lg6+jJp6L9K60c2/C0pCljyaFPC9TIDn06uP33RffKmDc1TBkXGH5R9leUYRNlPBAz6Q+ntnFJCseIZnFpWiuqOtI+aLpaN6WN8wayn4IMTxv/0qOxlJZ8GkssoOn4bh+/3hNZp9P5PmBshTMMoopnWfmcSaCAsRVOGZblG4H2XUc="
1
+ window.hierarchyData = "eJyNkz1vwyAQhv/LzRdq4i/sLerkpa3UblVUUZs2KAQioEqlyP+9wk4rOsFkCT/3PgccV7DGeAf9Ky3YHsGKDyVGL4120F+BFix8ND8J6GF4MUYBwlHqCXq6ZQhfVkEPo+LOCXfnjVFkocjBnwK6/IEevJs2oWyzLiCMB6kmK3QwU4pVV2O9rbEtGLasRFbWyNoGu6bEjlVIadfsZwRKadRQVj8Z7cwIVVfHwRfpvEtFuze/cGTF05J6G0t2Q1rAJdkN6eD23z3dc6Xe+XjM2MD4i5K/ogwbK+Op0F58Wr7OTFIoI5rEpWktK+PTexD+YuwxbdQrSG4FGZ62iTxPyvi05KyMJwFNx3dNfHrP3liRzncBIwucYWBVPMvc5UyCDxhZ4LQhPMdI8XhO3r7Uk/gmNzAlmOcfZBd90w=="
@@ -1 +1 @@
1
- window.navigationData = "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"
1
+ window.navigationData = "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"