agent-messenger 2.20.1 → 2.20.3

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 (59) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bun.lock +41 -0
  3. package/dist/package.json +2 -1
  4. package/dist/src/platforms/webex/client.d.ts +2 -0
  5. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  6. package/dist/src/platforms/webex/client.js +31 -7
  7. package/dist/src/platforms/webex/client.js.map +1 -1
  8. package/dist/src/platforms/webex/commands/message.d.ts.map +1 -1
  9. package/dist/src/platforms/webex/commands/message.js +17 -13
  10. package/dist/src/platforms/webex/commands/message.js.map +1 -1
  11. package/dist/src/platforms/webex/encryption.d.ts +7 -0
  12. package/dist/src/platforms/webex/encryption.d.ts.map +1 -1
  13. package/dist/src/platforms/webex/encryption.js +12 -1
  14. package/dist/src/platforms/webex/encryption.js.map +1 -1
  15. package/dist/src/platforms/webex/kms-key-provider.d.ts +20 -0
  16. package/dist/src/platforms/webex/kms-key-provider.d.ts.map +1 -0
  17. package/dist/src/platforms/webex/kms-key-provider.js +78 -0
  18. package/dist/src/platforms/webex/kms-key-provider.js.map +1 -0
  19. package/dist/src/platforms/webex/markdown-to-html.d.ts +1 -0
  20. package/dist/src/platforms/webex/markdown-to-html.d.ts.map +1 -1
  21. package/dist/src/platforms/webex/markdown-to-html.js +1 -1
  22. package/dist/src/platforms/webex/markdown-to-html.js.map +1 -1
  23. package/dist/src/tui/adapters/line-adapter.d.ts +3 -0
  24. package/dist/src/tui/adapters/line-adapter.d.ts.map +1 -1
  25. package/dist/src/tui/adapters/line-adapter.js +26 -0
  26. package/dist/src/tui/adapters/line-adapter.js.map +1 -1
  27. package/dist/src/tui/app.d.ts.map +1 -1
  28. package/dist/src/tui/app.js +8 -0
  29. package/dist/src/tui/app.js.map +1 -1
  30. package/docs/content/docs/cli/webex.mdx +1 -1
  31. package/package.json +2 -1
  32. package/skills/agent-channeltalk/SKILL.md +1 -1
  33. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  34. package/skills/agent-discord/SKILL.md +1 -1
  35. package/skills/agent-discordbot/SKILL.md +1 -1
  36. package/skills/agent-instagram/SKILL.md +1 -1
  37. package/skills/agent-kakaotalk/SKILL.md +1 -1
  38. package/skills/agent-line/SKILL.md +1 -1
  39. package/skills/agent-slack/SKILL.md +1 -1
  40. package/skills/agent-slackbot/SKILL.md +1 -1
  41. package/skills/agent-teams/SKILL.md +1 -1
  42. package/skills/agent-telegram/SKILL.md +1 -1
  43. package/skills/agent-telegrambot/SKILL.md +1 -1
  44. package/skills/agent-webex/SKILL.md +1 -1
  45. package/skills/agent-webex/references/authentication.md +1 -1
  46. package/skills/agent-wechatbot/SKILL.md +1 -1
  47. package/skills/agent-whatsapp/SKILL.md +1 -1
  48. package/skills/agent-whatsappbot/SKILL.md +1 -1
  49. package/src/platforms/webex/client.test.ts +25 -0
  50. package/src/platforms/webex/client.ts +37 -8
  51. package/src/platforms/webex/commands/message.test.ts +12 -0
  52. package/src/platforms/webex/commands/message.ts +22 -14
  53. package/src/platforms/webex/encryption.test.ts +58 -3
  54. package/src/platforms/webex/encryption.ts +19 -1
  55. package/src/platforms/webex/kms-key-provider.ts +99 -0
  56. package/src/platforms/webex/markdown-to-html.ts +1 -1
  57. package/src/platforms/webex/typings/webex-message-handler.d.ts +45 -0
  58. package/src/tui/adapters/line-adapter.ts +28 -0
  59. package/src/tui/app.ts +8 -0
@@ -32,6 +32,7 @@ let mockGetMessage: ReturnType<typeof spyOn>
32
32
  let mockDeleteMessage: ReturnType<typeof spyOn>
33
33
  let mockEditMessage: ReturnType<typeof spyOn>
34
34
  let mockLogin: ReturnType<typeof spyOn>
35
+ let mockDispose: ReturnType<typeof spyOn>
35
36
  let consoleLogSpy: ReturnType<typeof spyOn>
36
37
  let consoleErrorSpy: ReturnType<typeof spyOn>
37
38
  let processExitSpy: ReturnType<typeof spyOn>
@@ -47,6 +48,7 @@ beforeEach(() => {
47
48
  mockLogin = protoSpy('login').mockImplementation(async function (this: WebexClient) {
48
49
  return this
49
50
  })
51
+ mockDispose = protoSpy('dispose').mockResolvedValue(undefined)
50
52
  mockSendMessage = protoSpy('sendMessage').mockResolvedValue(mockMessage)
51
53
  mockSendDirectMessage = protoSpy('sendDirectMessage').mockResolvedValue(mockMessage)
52
54
  mockListMessages = protoSpy('listMessages').mockResolvedValue([mockMessage, mockMessage2])
@@ -78,6 +80,7 @@ it('calls sendMessage with correct args and outputs result', async () => {
78
80
  expect(output).toContain('msg_123')
79
81
  expect(output).toContain('space_456')
80
82
  expect(output).toContain('user@example.com')
83
+ expect(mockDispose).toHaveBeenCalled()
81
84
  })
82
85
 
83
86
  it('passes markdown option when --markdown flag is set on send', async () => {
@@ -86,6 +89,14 @@ it('passes markdown option when --markdown flag is set on send', async () => {
86
89
  expect(mockSendMessage).toHaveBeenCalledWith('space_456', '**bold**', { markdown: true })
87
90
  })
88
91
 
92
+ it('disposes the client when send fails', async () => {
93
+ mockSendMessage.mockRejectedValue(new WebexError('Send failed', 'send_failed'))
94
+
95
+ await sendAction('space_456', 'Hello world', { pretty: false })
96
+
97
+ expect(mockDispose).toHaveBeenCalled()
98
+ })
99
+
89
100
  it('exits with code 1 when not authenticated on send', async () => {
90
101
  mockLogin.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
91
102
 
@@ -166,6 +177,7 @@ it('calls editMessage with roomId in args and outputs result', async () => {
166
177
  const output = consoleLogSpy.mock.calls[0][0]
167
178
  expect(output).toContain('msg_123')
168
179
  expect(output).toContain('Updated message')
180
+ expect(mockDispose).toHaveBeenCalled()
169
181
  })
170
182
 
171
183
  it('passes markdown option to editMessage when --markdown flag is set', async () => {
@@ -6,14 +6,23 @@ import { formatOutput } from '@/shared/utils/output'
6
6
  import { WebexClient } from '../client'
7
7
  import type { WebexMessage } from '../types'
8
8
 
9
+ async function withWebexClient<T>(run: (client: WebexClient) => Promise<T>): Promise<T> {
10
+ const client = new WebexClient()
11
+ try {
12
+ await client.login()
13
+ return await run(client)
14
+ } finally {
15
+ await client.dispose()
16
+ }
17
+ }
18
+
9
19
  export async function sendAction(
10
20
  spaceId: string,
11
21
  text: string,
12
22
  options: { markdown?: boolean; pretty?: boolean },
13
23
  ): Promise<void> {
14
24
  try {
15
- const client = await new WebexClient().login()
16
- const message = await client.sendMessage(spaceId, text, { markdown: options.markdown })
25
+ const message = await withWebexClient((client) => client.sendMessage(spaceId, text, { markdown: options.markdown }))
17
26
 
18
27
  const output = {
19
28
  id: message.id,
@@ -31,9 +40,8 @@ export async function sendAction(
31
40
 
32
41
  export async function listAction(spaceId: string, options: { limit?: number; pretty?: boolean }): Promise<void> {
33
42
  try {
34
- const client = await new WebexClient().login()
35
43
  const limit = options.limit ?? 50
36
- const messages = await client.listMessages(spaceId, { max: limit })
44
+ const messages = await withWebexClient((client) => client.listMessages(spaceId, { max: limit }))
37
45
 
38
46
  const output = messages.map((msg: WebexMessage) => ({
39
47
  id: msg.id,
@@ -51,8 +59,7 @@ export async function listAction(spaceId: string, options: { limit?: number; pre
51
59
 
52
60
  export async function getAction(messageId: string, options: { pretty?: boolean }): Promise<void> {
53
61
  try {
54
- const client = await new WebexClient().login()
55
- const message = await client.getMessage(messageId)
62
+ const message = await withWebexClient((client) => client.getMessage(messageId))
56
63
 
57
64
  const output = {
58
65
  id: message.id,
@@ -75,8 +82,7 @@ export async function deleteAction(messageId: string, options: { force?: boolean
75
82
  return process.exit(0)
76
83
  }
77
84
 
78
- const client = await new WebexClient().login()
79
- await client.deleteMessage(messageId)
85
+ await withWebexClient((client) => client.deleteMessage(messageId))
80
86
 
81
87
  console.log(formatOutput({ deleted: messageId }, options.pretty))
82
88
  } catch (error) {
@@ -91,10 +97,11 @@ export async function editAction(
91
97
  options: { markdown?: boolean; pretty?: boolean },
92
98
  ): Promise<void> {
93
99
  try {
94
- const client = await new WebexClient().login()
95
- const message = await client.editMessage(messageId, spaceId, text, {
96
- markdown: options.markdown,
97
- })
100
+ const message = await withWebexClient((client) =>
101
+ client.editMessage(messageId, spaceId, text, {
102
+ markdown: options.markdown,
103
+ }),
104
+ )
98
105
 
99
106
  const output = {
100
107
  id: message.id,
@@ -116,8 +123,9 @@ export async function dmAction(
116
123
  options: { markdown?: boolean; pretty?: boolean },
117
124
  ): Promise<void> {
118
125
  try {
119
- const client = await new WebexClient().login()
120
- const message = await client.sendDirectMessage(email, text, { markdown: options.markdown })
126
+ const message = await withWebexClient((client) =>
127
+ client.sendDirectMessage(email, text, { markdown: options.markdown }),
128
+ )
121
129
 
122
130
  const output = {
123
131
  id: message.id,
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'bun:test'
1
+ import { describe, expect, it, mock } from 'bun:test'
2
2
 
3
3
  import * as jose from 'node-jose'
4
4
 
@@ -11,12 +11,16 @@ const decodeJweHeader = (jwe: string): Record<string, unknown> => {
11
11
  return JSON.parse(json) as Record<string, unknown>
12
12
  }
13
13
 
14
- const createKeyring = async (keyUri: string) => {
14
+ const createSerializedKey = async (keyUri: string) => {
15
15
  const keystore = jose.JWK.createKeyStore()
16
16
  const key = await keystore.generate('oct', 256, { alg: 'A256GCM' })
17
17
  const jwk = key.toJSON(true)
18
+ return JSON.stringify({ uri: keyUri, jwk })
19
+ }
20
+
21
+ const createKeyring = async (keyUri: string) => {
18
22
  const rawKeys = new Map<string, string>()
19
- rawKeys.set(keyUri, JSON.stringify({ jwk }))
23
+ rawKeys.set(keyUri, await createSerializedKey(keyUri))
20
24
  return new WebexEncryptionService(rawKeys)
21
25
  }
22
26
 
@@ -51,4 +55,55 @@ describe('WebexEncryptionService', () => {
51
55
 
52
56
  expect(plaintext).toBe('round trip')
53
57
  })
58
+
59
+ it('getKey returns cached key without calling provider when key is present', async () => {
60
+ const service = await createKeyring(keyUri)
61
+ const provider = { fetchKey: mock(async () => null as string | null) }
62
+ service.setKeyProvider(provider)
63
+
64
+ const key = await service.getKey(keyUri)
65
+
66
+ expect(key).not.toBeNull()
67
+ expect(provider.fetchKey).not.toHaveBeenCalled()
68
+ })
69
+
70
+ it('getKey calls provider and returns key when key is missing', async () => {
71
+ const missingKeyUri = 'kms://kms-aore.wbx2.com/keys/0d7a0dfb-0464-40ce-8f3d-e65a33b61561'
72
+ const serializedKey = await createSerializedKey(missingKeyUri)
73
+ const service = new WebexEncryptionService(new Map())
74
+ const provider = { fetchKey: mock(async () => serializedKey) }
75
+ service.setKeyProvider(provider)
76
+
77
+ const key = await service.getKey(missingKeyUri)
78
+
79
+ expect(key).not.toBeNull()
80
+ expect(provider.fetchKey).toHaveBeenCalledWith(missingKeyUri)
81
+ })
82
+
83
+ it('getKey returns null when provider returns null', async () => {
84
+ const missingKeyUri = 'kms://kms-aore.wbx2.com/keys/13d6256d-f7f1-4b98-8102-4d3d87b2834a'
85
+ const service = new WebexEncryptionService(new Map())
86
+ const provider = { fetchKey: mock(async () => null as string | null) }
87
+ service.setKeyProvider(provider)
88
+
89
+ const key = await service.getKey(missingKeyUri)
90
+
91
+ expect(key).toBeNull()
92
+ expect(provider.fetchKey).toHaveBeenCalledWith(missingKeyUri)
93
+ })
94
+
95
+ it('getKey reuses provider result from raw keys after first fetch', async () => {
96
+ const missingKeyUri = 'kms://kms-aore.wbx2.com/keys/84afb005-5ba5-49c8-bd46-0c5d7ddf1c30'
97
+ const serializedKey = await createSerializedKey(missingKeyUri)
98
+ const service = new WebexEncryptionService(new Map())
99
+ const provider = { fetchKey: mock(async () => serializedKey) }
100
+ service.setKeyProvider(provider)
101
+
102
+ await service.getKey(missingKeyUri)
103
+ ;(service as unknown as { keyCache: Map<string, jose.JWK.Key> }).keyCache.clear()
104
+ const key = await service.getKey(missingKeyUri)
105
+
106
+ expect(key).not.toBeNull()
107
+ expect(provider.fetchKey).toHaveBeenCalledTimes(1)
108
+ })
54
109
  })
@@ -1,23 +1,41 @@
1
1
  import * as jose from 'node-jose'
2
2
 
3
+ export interface WebexKeyProvider {
4
+ fetchKey(keyUri: string): Promise<string | null>
5
+ close?(): Promise<void>
6
+ }
7
+
3
8
  export class WebexEncryptionService {
4
9
  private rawKeys: Map<string, string>
5
10
  private keyCache: Map<string, jose.JWK.Key> = new Map()
11
+ private keyProvider: WebexKeyProvider | null = null
6
12
 
7
13
  constructor(serializedKeys: Map<string, string>) {
8
14
  this.rawKeys = serializedKeys
9
15
  }
10
16
 
17
+ setKeyProvider(provider: WebexKeyProvider): void {
18
+ this.keyProvider = provider
19
+ }
20
+
21
+ async close(): Promise<void> {
22
+ await this.keyProvider?.close?.()
23
+ }
24
+
11
25
  async getKey(keyUri: string): Promise<jose.JWK.Key | null> {
12
26
  const cached = this.keyCache.get(keyUri)
13
27
  if (cached) return cached
14
28
 
15
- const raw = this.rawKeys.get(keyUri)
29
+ let raw = this.rawKeys.get(keyUri)
30
+ if (!raw && this.keyProvider) {
31
+ raw = (await this.keyProvider.fetchKey(keyUri)) ?? undefined
32
+ }
16
33
  if (!raw) return null
17
34
 
18
35
  try {
19
36
  const parsed = JSON.parse(raw) as { jwk: object }
20
37
  const joseKey = await jose.JWK.asKey(parsed.jwk)
38
+ this.rawKeys.set(keyUri, raw)
21
39
  this.keyCache.set(keyUri, joseKey)
22
40
  return joseKey
23
41
  } catch {
@@ -0,0 +1,99 @@
1
+ import { DeviceManager, KmsClient, MercurySocket, noopLogger } from 'webex-message-handler'
2
+ import WebSocket from 'ws'
3
+
4
+ interface KmsKeyProviderOptions {
5
+ token: string
6
+ logger?: { debug(message: string): void }
7
+ }
8
+
9
+ interface Registration {
10
+ webSocketUrl: string
11
+ deviceUrl: string
12
+ userId: string
13
+ encryptionServiceUrl: string
14
+ }
15
+
16
+ type HttpRequest = {
17
+ url: string
18
+ method: string
19
+ headers: Record<string, string>
20
+ body?: string
21
+ }
22
+
23
+ export class KmsKeyProvider {
24
+ private token: string
25
+ private logger?: { debug(message: string): void }
26
+ private mercury: MercurySocket | null = null
27
+ private kms: KmsClient | null = null
28
+ private readyPromise: Promise<void> | null = null
29
+
30
+ constructor(options: KmsKeyProviderOptions) {
31
+ this.token = options.token
32
+ this.logger = options.logger
33
+ }
34
+
35
+ async fetchKey(keyUri: string): Promise<string | null> {
36
+ try {
37
+ await this.ensureReady()
38
+ const key = await this.kms?.getKey(keyUri)
39
+ if (!key) return null
40
+ return JSON.stringify({ uri: keyUri, jwk: key.toJSON(true) })
41
+ } catch (error) {
42
+ this.logger?.debug(`Webex KMS key fetch failed: ${error instanceof Error ? error.message : String(error)}`)
43
+ await this.close()
44
+ this.readyPromise = null
45
+ return null
46
+ }
47
+ }
48
+
49
+ async close(): Promise<void> {
50
+ await this.mercury?.disconnect().catch(() => undefined)
51
+ this.mercury = null
52
+ this.kms = null
53
+ this.readyPromise = null
54
+ }
55
+
56
+ private async ensureReady(): Promise<void> {
57
+ this.readyPromise ??= this.initialize()
58
+ await this.readyPromise
59
+ }
60
+
61
+ private async initialize(): Promise<void> {
62
+ const httpDo = async (req: HttpRequest) => {
63
+ const res = await fetch(req.url, {
64
+ method: req.method,
65
+ headers: req.headers,
66
+ body: req.body,
67
+ })
68
+ return {
69
+ status: res.status,
70
+ ok: res.ok,
71
+ json: () => res.json(),
72
+ text: () => res.text(),
73
+ }
74
+ }
75
+ const wsFactory = (url: string) => new WebSocket(url) as never
76
+ const dm = new DeviceManager({ logger: noopLogger, httpDo })
77
+ const reg = (await dm.register(this.token)) as Registration
78
+ const mercury = new MercurySocket({ logger: noopLogger, wsFactory })
79
+ const kms = new KmsClient({
80
+ token: this.token,
81
+ deviceUrl: reg.deviceUrl,
82
+ userId: reg.userId,
83
+ encryptionServiceUrl: reg.encryptionServiceUrl,
84
+ logger: noopLogger,
85
+ httpDo,
86
+ })
87
+ mercury.on('kms:response', (data: unknown) => kms.handleKmsMessage(data))
88
+ await mercury.connect(reg.webSocketUrl, this.token)
89
+ this.mercury = mercury
90
+ try {
91
+ await kms.initialize()
92
+ } catch (error) {
93
+ await mercury.disconnect().catch(() => undefined)
94
+ this.mercury = null
95
+ throw error
96
+ }
97
+ this.kms = kms
98
+ }
99
+ }
@@ -184,7 +184,7 @@ function isSafeUrl(url: string): boolean {
184
184
  return SAFE_URL_PATTERN.test(url.trim())
185
185
  }
186
186
 
187
- function escapeHtml(value: string): string {
187
+ export function escapeHtml(value: string): string {
188
188
  return value
189
189
  .replaceAll('&', '&amp;')
190
190
  .replaceAll('<', '&lt;')
@@ -0,0 +1,45 @@
1
+ export {}
2
+
3
+ declare module 'webex-message-handler' {
4
+ import type * as jose from 'node-jose'
5
+
6
+ type Logger = Record<string, (...args: unknown[]) => void>
7
+ type HttpRequest = { url: string; method: string; headers: Record<string, string>; body?: string }
8
+ type HttpResponse = { status: number; ok: boolean; json(): Promise<unknown>; text(): Promise<string> }
9
+ type HttpDo = (req: HttpRequest) => Promise<HttpResponse>
10
+
11
+ export const noopLogger: Logger
12
+ export const consoleLogger: Logger
13
+
14
+ export class DeviceManager {
15
+ constructor(options: { logger: Logger; httpDo: HttpDo })
16
+ register(token: string): Promise<{
17
+ webSocketUrl: string
18
+ deviceUrl: string
19
+ userId: string
20
+ services: unknown
21
+ encryptionServiceUrl: string
22
+ }>
23
+ }
24
+
25
+ export class MercurySocket {
26
+ constructor(options: { logger: Logger; wsFactory: (url: string) => unknown })
27
+ on(event: 'kms:response', handler: (data: unknown) => void): void
28
+ connect(webSocketUrl: string, token: string): Promise<void>
29
+ disconnect(): Promise<void>
30
+ }
31
+
32
+ export class KmsClient {
33
+ constructor(options: {
34
+ token: string
35
+ deviceUrl: string
36
+ userId: string
37
+ encryptionServiceUrl: string
38
+ logger: Logger
39
+ httpDo: HttpDo
40
+ })
41
+ initialize(): Promise<void>
42
+ getKey(keyUri: string): Promise<jose.JWK.Key | null>
43
+ handleKmsMessage(data: unknown): void
44
+ }
45
+ }
@@ -1,5 +1,7 @@
1
1
  import { LineClient } from '@/platforms/line/client'
2
2
  import { LineCredentialManager } from '@/platforms/line/credential-manager'
3
+ import { LineListener } from '@/platforms/line/listener'
4
+ import type { LinePushMessageEvent } from '@/platforms/line/types'
3
5
 
4
6
  import type { AuthHint, AuthIO, PlatformAdapter, UnifiedChannel, UnifiedMessage, Workspace } from './types'
5
7
 
@@ -7,6 +9,7 @@ export class LineAdapter implements PlatformAdapter {
7
9
  readonly name = 'LINE'
8
10
 
9
11
  private client: LineClient | null = null
12
+ private listener: LineListener | null = null
10
13
  private credManager = new LineCredentialManager()
11
14
  private currentAccount: Workspace | null = null
12
15
 
@@ -48,6 +51,28 @@ export class LineAdapter implements PlatformAdapter {
48
51
  await client.sendMessage(channelId, text)
49
52
  }
50
53
 
54
+ async startListening(onMessage: (msg: UnifiedMessage) => void): Promise<void> {
55
+ const client = this.ensureClient()
56
+ const listener = new LineListener(client)
57
+ await listener.start()
58
+ listener.on('message', (event: LinePushMessageEvent) => {
59
+ if (event.text === null) return
60
+ onMessage({
61
+ id: event.message_id,
62
+ channelId: event.chat_id,
63
+ author: event.author_id || 'unknown',
64
+ content: event.text,
65
+ timestamp: event.sent_at,
66
+ })
67
+ })
68
+ this.listener = listener
69
+ }
70
+
71
+ stopListening(): void {
72
+ this.listener?.stop()
73
+ this.listener = null
74
+ }
75
+
51
76
  async getWorkspaces(): Promise<Workspace[]> {
52
77
  const accounts = await this.credManager.listAccounts()
53
78
  return accounts.map((acct) => ({
@@ -62,6 +87,9 @@ export class LineAdapter implements PlatformAdapter {
62
87
 
63
88
  const client = new LineClient()
64
89
  await client.login(creds)
90
+ // Persist the selected account so the listener's credential-less reconnects
91
+ // resolve this account instead of a stale current_account.
92
+ await this.credManager.setCurrentAccount(accountId)
65
93
  this.client = client
66
94
  this.currentAccount = { id: creds.account_id, name: creds.display_name ?? creds.account_id }
67
95
  }
package/src/tui/app.ts CHANGED
@@ -647,6 +647,10 @@ export async function createApp(): Promise<void> {
647
647
  const ws = p.workspaces[selectedIndex]
648
648
  try {
649
649
  await p.adapter.switchWorkspace?.(ws.id)
650
+ if (p.listening) {
651
+ p.adapter.stopListening?.()
652
+ p.listening = false
653
+ }
650
654
  p.channels = null
651
655
  renderHeader()
652
656
  } catch {}
@@ -744,6 +748,10 @@ export async function createApp(): Promise<void> {
744
748
  p.adapter
745
749
  .switchWorkspace(workspace.id)
746
750
  .then(() => {
751
+ if (p.listening) {
752
+ p.adapter.stopListening?.()
753
+ p.listening = false
754
+ }
747
755
  p.channels = null
748
756
  p.workspaces = null
749
757
  renderHeader()