@talex-touch/utils 1.0.21 → 1.0.23

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.
@@ -166,6 +166,8 @@ export function structuredStrictStringify(value: unknown): string {
166
166
  return JSON.stringify(serialize(value, 'root'));
167
167
  }
168
168
 
169
+ export * from './timing'
170
+
169
171
  export { runAdaptiveTaskQueue, type AdaptiveTaskQueueOptions } from './task-queue'
170
172
 
171
173
  export * from './time'
@@ -0,0 +1,250 @@
1
+ export type TimingMeta = Record<string, unknown>
2
+
3
+ export interface TimingRecord {
4
+ label: string
5
+ durationMs: number
6
+ startedAt: number
7
+ endedAt: number
8
+ iteration?: number
9
+ meta?: TimingMeta
10
+ error?: unknown
11
+ }
12
+
13
+ export interface TimingStats {
14
+ label: string
15
+ count: number
16
+ totalMs: number
17
+ avgMs: number
18
+ maxMs: number
19
+ minMs: number
20
+ lastMs: number
21
+ lastStartedAt?: number
22
+ lastEndedAt?: number
23
+ errorCount: number
24
+ lastError?: unknown
25
+ }
26
+
27
+ export interface TimingSummary extends TimingStats {
28
+ history: TimingRecord[]
29
+ }
30
+
31
+ export interface TimingManagerConfig {
32
+ autoLog?: boolean
33
+ historyLimit?: number
34
+ logger?: (message: string, entry: TimingRecord, stats: TimingStats) => void
35
+ formatter?: (entry: TimingRecord, stats: TimingStats) => string
36
+ }
37
+
38
+ export interface TimingOptions {
39
+ autoLog?: boolean
40
+ storeHistory?: boolean
41
+ logger?: (message: string, entry: TimingRecord, stats: TimingStats) => void
42
+ formatter?: (entry: TimingRecord, stats: TimingStats) => string
43
+ historyLimit?: number
44
+ }
45
+
46
+ export class TimingManager {
47
+ private readonly stats = new Map<string, TimingStats>()
48
+ private readonly history = new Map<string, TimingRecord[]>()
49
+ private readonly moduleStats = new Map<string, TimingStats>()
50
+
51
+ constructor(private readonly config: TimingManagerConfig = {}) {}
52
+
53
+ createTiming(label: string, options: TimingOptions = {}): TimingScope {
54
+ return new TimingScope(this, label, options)
55
+ }
56
+
57
+ record(label: string, record: TimingRecord, options: TimingOptions = {}): void {
58
+ const stats = this.updateStats(this.stats, label, record)
59
+ const moduleKey = this.extractModuleKey(label)
60
+ this.updateStats(this.moduleStats, moduleKey, { ...record, label: moduleKey })
61
+
62
+ if (options.storeHistory ?? true) {
63
+ const limit = options.historyLimit ?? this.config.historyLimit ?? 50
64
+ const list = this.history.get(label) ?? []
65
+ list.push(record)
66
+ if (list.length > limit) {
67
+ list.splice(0, list.length - limit)
68
+ }
69
+ this.history.set(label, list)
70
+ }
71
+
72
+ const shouldLog = options.autoLog ?? this.config.autoLog ?? true
73
+ if (shouldLog) {
74
+ const formatter = options.formatter ?? this.config.formatter ?? defaultFormatter
75
+ const logger = options.logger ?? this.config.logger ?? defaultLogger
76
+ logger(formatter(record, stats), record, stats)
77
+ }
78
+ }
79
+
80
+ getStats(label: string): TimingStats | undefined {
81
+ const stats = this.stats.get(label)
82
+ if (!stats) return undefined
83
+ return { ...stats }
84
+ }
85
+
86
+ getAllStats(): TimingStats[] {
87
+ return Array.from(this.stats.values()).map((s) => ({ ...s }))
88
+ }
89
+
90
+ getHistory(label: string): TimingRecord[] {
91
+ return [...(this.history.get(label) ?? [])]
92
+ }
93
+
94
+ getModuleStats(moduleKey?: string): TimingStats[] | TimingStats | undefined {
95
+ if (moduleKey) {
96
+ const stats = this.moduleStats.get(moduleKey)
97
+ return stats ? { ...stats } : undefined
98
+ }
99
+ return Array.from(this.moduleStats.values()).map((s) => ({ ...s }))
100
+ }
101
+
102
+ reset(label?: string): void {
103
+ if (!label) {
104
+ this.stats.clear()
105
+ this.history.clear()
106
+ this.moduleStats.clear()
107
+ return
108
+ }
109
+ this.stats.delete(label)
110
+ this.history.delete(label)
111
+ }
112
+
113
+ private updateStats(target: Map<string, TimingStats>, label: string, record: TimingRecord): TimingStats {
114
+ const { durationMs, error } = record
115
+ const next = target.get(label) ?? {
116
+ label,
117
+ count: 0,
118
+ totalMs: 0,
119
+ avgMs: 0,
120
+ maxMs: Number.NEGATIVE_INFINITY,
121
+ minMs: Number.POSITIVE_INFINITY,
122
+ lastMs: 0,
123
+ errorCount: 0
124
+ }
125
+
126
+ next.count += 1
127
+ next.totalMs += durationMs
128
+ next.avgMs = next.totalMs / next.count
129
+ next.maxMs = Math.max(next.maxMs, durationMs)
130
+ next.minMs = Math.min(next.minMs, durationMs)
131
+ next.lastMs = durationMs
132
+ next.lastStartedAt = record.startedAt
133
+ next.lastEndedAt = record.endedAt
134
+
135
+ if (error) {
136
+ next.errorCount += 1
137
+ next.lastError = error
138
+ }
139
+
140
+ target.set(label, next)
141
+ return next
142
+ }
143
+
144
+ private extractModuleKey(label: string): string {
145
+ const [moduleKey] = label.split(':')
146
+ return moduleKey || label
147
+ }
148
+ }
149
+
150
+ export class TimingScope {
151
+ constructor(
152
+ private readonly manager: TimingManager,
153
+ private readonly label: string,
154
+ private readonly options: TimingOptions
155
+ ) {}
156
+
157
+ async cost<T>(fn: () => Promise<T> | T, meta: TimingMeta = {}, overrides: TimingOptions = {}): Promise<T> {
158
+ const startedAt = now()
159
+ try {
160
+ const result = await fn()
161
+ this.finish(startedAt, meta, undefined, overrides)
162
+ return result
163
+ } catch (error) {
164
+ this.finish(startedAt, meta, error, overrides)
165
+ throw error
166
+ }
167
+ }
168
+
169
+ async count<T>(iterations: number, fn: (iteration: number) => Promise<T> | T, meta: TimingMeta | ((iteration: number) => TimingMeta) = {}, overrides: TimingOptions = {}): Promise<T[]> {
170
+ const results: T[] = []
171
+ for (let index = 0; index < iterations; index++) {
172
+ const iterationMeta = typeof meta === 'function' ? meta(index) : meta
173
+ const startedAt = now()
174
+ try {
175
+ const value = await fn(index)
176
+ this.finish(startedAt, { ...iterationMeta, iteration: index }, undefined, overrides)
177
+ results.push(value)
178
+ } catch (error) {
179
+ this.finish(startedAt, { ...iterationMeta, iteration: index }, error, overrides)
180
+ throw error
181
+ }
182
+ }
183
+ return results
184
+ }
185
+
186
+ mark(durationMs: number, meta: TimingMeta = {}, overrides: TimingOptions = {}): void {
187
+ const endedAt = now()
188
+ const startedAt = endedAt - durationMs
189
+ this.manager.record(
190
+ this.label,
191
+ {
192
+ label: this.label,
193
+ durationMs,
194
+ startedAt,
195
+ endedAt,
196
+ meta
197
+ },
198
+ { ...this.options, ...overrides }
199
+ )
200
+ }
201
+
202
+ getStats(): TimingStats | undefined {
203
+ return this.manager.getStats(this.label)
204
+ }
205
+
206
+ getHistory(): TimingRecord[] {
207
+ return this.manager.getHistory(this.label)
208
+ }
209
+
210
+ private finish(startedAt: number, meta: TimingMeta, error: unknown, overrides: TimingOptions): void {
211
+ const endedAt = now()
212
+ const record: TimingRecord = {
213
+ label: this.label,
214
+ durationMs: endedAt - startedAt,
215
+ startedAt,
216
+ endedAt,
217
+ meta,
218
+ error
219
+ }
220
+
221
+ this.manager.record(this.label, record, { ...this.options, ...overrides })
222
+ }
223
+ }
224
+
225
+ function defaultFormatter(record: TimingRecord, stats: TimingStats): string {
226
+ const duration = record.durationMs.toFixed(2)
227
+ return `⏱ [${record.label}] ${duration} ms (avg: ${stats.avgMs.toFixed(2)} ms, max: ${stats.maxMs.toFixed(2)} ms, count: ${stats.count})`
228
+ }
229
+
230
+ function defaultLogger(message: string): void {
231
+ console.log(message)
232
+ }
233
+
234
+ const now = (() => {
235
+ if (typeof globalThis !== 'undefined') {
236
+ const perf = (globalThis as typeof globalThis & { performance?: { now?: () => number } }).performance
237
+ if (perf?.now) {
238
+ return () => perf.now()
239
+ }
240
+ }
241
+ return () => Date.now()
242
+ })()
243
+
244
+ const timingManagerInstance = new TimingManager()
245
+
246
+ export const timingManager = timingManagerInstance
247
+
248
+ export function createTiming(label: string, options: TimingOptions = {}): TimingScope {
249
+ return timingManagerInstance.createTiming(label, options)
250
+ }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "module": "./index.ts",
6
6
  "license": "MPL-2.0",
7
7
  "private": false,
8
- "version": "1.0.21",
8
+ "version": "1.0.23",
9
9
  "scripts": {
10
10
  "publish": "npm publish --access public"
11
11
  },
package/plugin/index.ts CHANGED
@@ -377,4 +377,5 @@ export interface IManifest {
377
377
  export type { LogLevel, LogItem, LogDataType, IPluginLogger } from './log/types'
378
378
  export * from './sdk/index'
379
379
  export * from './providers'
380
+ export * from './install'
380
381
  export * from './risk'
@@ -0,0 +1,43 @@
1
+ export type PluginInstallTaskStage =
2
+ | 'queued'
3
+ | 'downloading'
4
+ | 'awaiting-confirmation'
5
+ | 'installing'
6
+ | 'completed'
7
+ | 'failed'
8
+ | 'cancelled'
9
+
10
+ export interface PluginInstallProgressEvent {
11
+ taskId: string
12
+ stage: PluginInstallTaskStage
13
+ progress?: number
14
+ message?: string
15
+ error?: string
16
+ /** 安装源标识,例如下载 URL。 */
17
+ source?: string
18
+ /** 官方插件标记。 */
19
+ official?: boolean
20
+ /** 插件唯一标识或名称(由客户端提供)。 */
21
+ pluginId?: string
22
+ pluginName?: string
23
+ /** 队列中的剩余任务数量(包含当前任务)。 */
24
+ remaining?: number
25
+ /** 当前任务在队列中的位置(0 表示正在处理)。 */
26
+ position?: number
27
+ }
28
+
29
+ export interface PluginInstallConfirmRequest {
30
+ taskId: string
31
+ pluginName?: string
32
+ pluginId?: string
33
+ source?: string
34
+ official?: boolean
35
+ }
36
+
37
+ export type PluginInstallConfirmDecision = 'accept' | 'reject'
38
+
39
+ export interface PluginInstallConfirmResponse {
40
+ taskId: string
41
+ decision: PluginInstallConfirmDecision
42
+ reason?: string
43
+ }
@@ -12,6 +12,7 @@ export class PluginLoggerManager {
12
12
  private readonly pluginLogDir: string
13
13
  private readonly sessionLogPath: string
14
14
  private readonly pluginInfoPath: string
15
+ private readonly sessionStart: string
15
16
  private buffer: LogItem[] = []
16
17
  private flushInterval: NodeJS.Timeout
17
18
  private onLogAppend?: (log: LogItem) => void
@@ -25,15 +26,15 @@ export class PluginLoggerManager {
25
26
  constructor(baseDir: string, pluginInfo: ITouchPlugin, onLogAppend?: (log: LogItem) => void) {
26
27
  this.pluginInfo = pluginInfo
27
28
  this.onLogAppend = onLogAppend
28
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
29
+ this.sessionStart = new Date().toISOString()
30
+ const timestamp = this.sessionStart.replace(/[:.]/g, '-')
29
31
  const sessionFolder = `${timestamp}_${pluginInfo.name.replace(/[^a-zA-Z0-9-_]/g, '_')}`
30
32
 
31
33
  this.pluginLogDir = path.resolve(baseDir, 'logs', sessionFolder)
32
34
  this.sessionLogPath = path.resolve(this.pluginLogDir, 'session.log')
33
35
  this.pluginInfoPath = path.resolve(this.pluginLogDir, 'touch-plugin.info')
34
36
 
35
- this.ensureDirectory()
36
- this.createPluginInfoFile()
37
+ this.ensureLogEnvironment(true)
37
38
  this.flushInterval = setInterval(() => this.flush(), 5000)
38
39
  }
39
40
 
@@ -52,8 +53,17 @@ export class PluginLoggerManager {
52
53
  flush(): void {
53
54
  if (this.buffer.length === 0) return
54
55
  const lines = this.buffer.map((item) => structuredStrictStringify(item)).join('\n') + '\n'
55
- fs.appendFileSync(this.sessionLogPath, lines)
56
- this.buffer = []
56
+ try {
57
+ this.ensureLogEnvironment()
58
+ fs.appendFileSync(this.sessionLogPath, lines)
59
+ this.buffer = []
60
+ } catch (error) {
61
+ if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') throw error
62
+ // Directory or file was removed; rebuild and retry once.
63
+ this.ensureLogEnvironment(true)
64
+ fs.appendFileSync(this.sessionLogPath, lines)
65
+ this.buffer = []
66
+ }
57
67
  }
58
68
 
59
69
  /**
@@ -83,12 +93,12 @@ export class PluginLoggerManager {
83
93
  /**
84
94
  * Creates the touch-plugin.info file with plugin information.
85
95
  */
86
- private createPluginInfoFile(): void {
96
+ private writePluginInfoFile(): void {
87
97
  const pluginInfo = {
88
98
  name: this.pluginInfo.name,
89
99
  version: this.pluginInfo.version,
90
100
  description: this.pluginInfo.desc,
91
- sessionStart: new Date().toISOString(),
101
+ sessionStart: this.sessionStart,
92
102
  icon: this.pluginInfo.icon,
93
103
  platforms: this.pluginInfo.platforms,
94
104
  status: this.pluginInfo.status,
@@ -103,11 +113,19 @@ export class PluginLoggerManager {
103
113
  }
104
114
 
105
115
  /**
106
- * Ensures the log directory exists.
116
+ * Ensures the log directory and related files exist.
107
117
  */
108
- private ensureDirectory(): void {
118
+ private ensureLogEnvironment(forceInfo = false): void {
109
119
  if (!fs.existsSync(this.pluginLogDir)) {
110
120
  fs.mkdirSync(this.pluginLogDir, { recursive: true })
111
121
  }
122
+
123
+ if (!fs.existsSync(this.sessionLogPath)) {
124
+ fs.writeFileSync(this.sessionLogPath, '')
125
+ }
126
+
127
+ if (forceInfo || !fs.existsSync(this.pluginInfoPath)) {
128
+ this.writePluginInfoFile()
129
+ }
112
130
  }
113
131
  }
@@ -17,6 +17,8 @@ export interface PluginInstallRequest {
17
17
  hintType?: PluginProviderType
18
18
  /** 可选元数据,在调用链中透传。 */
19
19
  metadata?: Record<string, unknown>
20
+ /** 客户端附加信息,仅用于 UI 状态同步。 */
21
+ clientMetadata?: Record<string, unknown>
20
22
  }
21
23
 
22
24
  export interface PluginProviderContext {
@@ -0,0 +1,48 @@
1
+ import type { ITouchClientChannel } from '@talex-touch/utils/channel';
2
+ import { genChannel } from '../channel';
3
+ import type { IPluginRendererChannel, PluginChannelHandler } from './types';
4
+
5
+ const ensureClientChannel = (): ITouchClientChannel => genChannel();
6
+
7
+ export function createPluginRendererChannel(): IPluginRendererChannel {
8
+ const client = ensureClientChannel();
9
+
10
+ return {
11
+ send(eventName, payload) {
12
+ return client.send(eventName, payload);
13
+ },
14
+
15
+ sendSync(eventName, payload) {
16
+ return client.sendSync(eventName, payload);
17
+ },
18
+
19
+ on(eventName, handler) {
20
+ return client.regChannel(eventName, handler);
21
+ },
22
+
23
+ once(eventName, handler) {
24
+ let dispose: () => void = () => void 0;
25
+ const wrapped: PluginChannelHandler = (event) => {
26
+ dispose();
27
+ handler(event);
28
+ };
29
+
30
+ dispose = client.regChannel(eventName, wrapped);
31
+ return dispose;
32
+ },
33
+
34
+ get raw() {
35
+ return client;
36
+ }
37
+ };
38
+ }
39
+
40
+ let cachedRendererChannel: IPluginRendererChannel | null = null;
41
+
42
+ export function usePluginRendererChannel(): IPluginRendererChannel {
43
+ if (!cachedRendererChannel) {
44
+ cachedRendererChannel = createPluginRendererChannel();
45
+ }
46
+
47
+ return cachedRendererChannel;
48
+ }
@@ -0,0 +1,99 @@
1
+ import type { PluginClipboardHistoryResponse, PluginClipboardItem } from './types'
2
+
3
+ const ensurePluginChannel = () => {
4
+ const channel = (window as any)?.$channel
5
+ if (!channel) {
6
+ throw new Error('[Plugin SDK] Clipboard channel requires plugin renderer context with $channel available.')
7
+ }
8
+ return channel
9
+ }
10
+
11
+ const normalizeItem = (item: PluginClipboardItem | null): PluginClipboardItem | null => {
12
+ if (!item) return item
13
+ if (!item.meta && typeof item.metadata === 'string') {
14
+ try {
15
+ const parsed = JSON.parse(item.metadata)
16
+ return { ...item, meta: parsed }
17
+ } catch {
18
+ return { ...item, meta: null }
19
+ }
20
+ }
21
+ return item
22
+ }
23
+
24
+ export interface ClipboardHistoryOptions {
25
+ page?: number
26
+ }
27
+
28
+ export interface ClipboardFavoriteOptions {
29
+ id: number
30
+ isFavorite: boolean
31
+ }
32
+
33
+ export interface ClipboardDeleteOptions {
34
+ id: number
35
+ }
36
+
37
+ export interface ClipboardApplyOptions {
38
+ item?: PluginClipboardItem
39
+ text?: string
40
+ html?: string | null
41
+ files?: string[]
42
+ delayMs?: number
43
+ hideCoreBox?: boolean
44
+ type?: PluginClipboardItem['type']
45
+ }
46
+
47
+ export function useClipboardHistory() {
48
+ const channel = ensurePluginChannel()
49
+
50
+ return {
51
+ async getLatest(): Promise<PluginClipboardItem | null> {
52
+ const result = await channel.send('clipboard:get-latest')
53
+ return normalizeItem(result)
54
+ },
55
+
56
+ async getHistory(options: ClipboardHistoryOptions = {}): Promise<PluginClipboardHistoryResponse> {
57
+ const { page = 1 } = options
58
+ const response = await channel.send('clipboard:get-history', { page })
59
+ const history = Array.isArray(response?.history)
60
+ ? response.history.map((item: PluginClipboardItem) => normalizeItem(item) ?? item)
61
+ : []
62
+ return {
63
+ ...response,
64
+ history
65
+ }
66
+ },
67
+
68
+ async setFavorite(options: ClipboardFavoriteOptions): Promise<void> {
69
+ await channel.send('clipboard:set-favorite', options)
70
+ },
71
+
72
+ async deleteItem(options: ClipboardDeleteOptions): Promise<void> {
73
+ await channel.send('clipboard:delete-item', options)
74
+ },
75
+
76
+ async clearHistory(): Promise<void> {
77
+ await channel.send('clipboard:clear-history')
78
+ },
79
+
80
+ onDidChange(callback: (item: PluginClipboardItem) => void): () => void {
81
+ return channel.regChannel('core-box:clipboard-change', ({ data }) => {
82
+ const item = (data && 'item' in data ? data.item : data) as PluginClipboardItem
83
+ callback(normalizeItem(item) ?? item)
84
+ })
85
+ },
86
+
87
+ /**
88
+ * Writes the provided clipboard payload to the system clipboard and issues a paste command
89
+ * to the foreground application.
90
+ */
91
+ async applyToActiveApp(options: ClipboardApplyOptions = {}): Promise<boolean> {
92
+ const response = await channel.send('clipboard:apply-to-active-app', options)
93
+ if (typeof response === 'object' && response) {
94
+ return Boolean((response as any).success)
95
+ }
96
+ return true
97
+ }
98
+ }
99
+ }
@@ -10,6 +10,7 @@ export type BridgeHook<T = any> = (data: T) => void
10
10
 
11
11
  const __hooks: Record<BridgeEvent, Array<BridgeHook>> = {
12
12
  [BridgeEventForCoreBox.CORE_BOX_INPUT_CHANGE]: [],
13
+ [BridgeEventForCoreBox.CORE_BOX_CLIPBOARD_CHANGE]: []
13
14
  }
14
15
 
15
16
  /**
@@ -66,3 +67,5 @@ export const createBridgeHook = <T>(type: BridgeEvent) => (hook: BridgeHook<T>)
66
67
  * @param data The input change data (string).
67
68
  */
68
69
  export const onCoreBoxInputChange = createBridgeHook<{ query: string }>(BridgeEventForCoreBox.CORE_BOX_INPUT_CHANGE)
70
+
71
+ export const onCoreBoxClipboardChange = createBridgeHook<{ item: any }>(BridgeEventForCoreBox.CORE_BOX_CLIPBOARD_CHANGE)
@@ -17,4 +17,7 @@ export * from './window/index'
17
17
  export * from './hooks/index'
18
18
  export * from './service/index'
19
19
 
20
- export * from './storage'
20
+ export * from './channel'
21
+ export * from './clipboard'
22
+ export * from './storage'
23
+ export * from './system'
@@ -0,0 +1,14 @@
1
+ import type { ActiveAppSnapshot } from './types'
2
+
3
+ const ensurePluginChannel = () => {
4
+ const channel = (window as any)?.$channel
5
+ if (!channel) {
6
+ throw new Error('[Plugin SDK] System channel requires plugin renderer context with $channel available.')
7
+ }
8
+ return channel
9
+ }
10
+
11
+ export async function getActiveAppSnapshot(options: { forceRefresh?: boolean } = {}): Promise<ActiveAppSnapshot | null> {
12
+ const channel = ensurePluginChannel()
13
+ return channel.send('system:get-active-app', options)
14
+ }
@@ -4,6 +4,124 @@
4
4
  * @version 1.0.0
5
5
  */
6
6
 
7
+ import type { ITouchChannel, ITouchClientChannel, StandardChannelData } from '@talex-touch/utils/channel';
8
+
9
+ /**
10
+ * Handler signature for plugin channel events.
11
+ */
12
+ export type PluginChannelHandler = (event: StandardChannelData) => any;
13
+
14
+ /**
15
+ * Bridge exposed to plugin backends for channel-based communication.
16
+ */
17
+ export interface IPluginChannelBridge {
18
+ /**
19
+ * Sends a payload to the main renderer process.
20
+ * @param eventName - Channel event name.
21
+ * @param payload - Optional data payload.
22
+ */
23
+ sendToMain<T = any>(eventName: string, payload?: any): Promise<T>;
24
+
25
+ /**
26
+ * Sends a payload to this plugin's renderer view.
27
+ * @param eventName - Channel event name.
28
+ * @param payload - Optional data payload.
29
+ */
30
+ sendToRenderer<T = any>(eventName: string, payload?: any): Promise<T>;
31
+
32
+ /**
33
+ * Registers a handler for main renderer messages.
34
+ * @param eventName - Channel event name to listen for.
35
+ * @param handler - Handler invoked with the raw channel event.
36
+ * @returns Unsubscribe function.
37
+ */
38
+ onMain(eventName: string, handler: PluginChannelHandler): () => void;
39
+
40
+ /**
41
+ * Registers a handler for renderer-originated messages scoped to this plugin.
42
+ * @param eventName - Channel event name to listen for.
43
+ * @param handler - Handler invoked with the raw channel event.
44
+ * @returns Unsubscribe function.
45
+ */
46
+ onRenderer(eventName: string, handler: PluginChannelHandler): () => void;
47
+
48
+ /**
49
+ * Access to the underlying channel implementation for advanced scenarios.
50
+ */
51
+ readonly raw: ITouchChannel;
52
+ }
53
+
54
+ /**
55
+ * Renderer-side helper for plugin webviews to interact with the bridge channel.
56
+ */
57
+ export interface IPluginRendererChannel {
58
+ /**
59
+ * Sends a message asynchronously and resolves with the reply payload.
60
+ */
61
+ send<T = any>(eventName: string, payload?: any): Promise<T>;
62
+
63
+ /**
64
+ * Sends a message synchronously and returns the reply payload.
65
+ */
66
+ sendSync<T = any>(eventName: string, payload?: any): T;
67
+
68
+ /**
69
+ * Registers a handler for renderer channel events.
70
+ * @returns Unsubscribe function.
71
+ */
72
+ on(eventName: string, handler: PluginChannelHandler): () => void;
73
+
74
+ /**
75
+ * Registers a one-off handler for a renderer channel event.
76
+ * @returns Unsubscribe function (no-op after invocation).
77
+ */
78
+ once(eventName: string, handler: PluginChannelHandler): () => void;
79
+
80
+ /**
81
+ * Provides access to the raw client channel.
82
+ */
83
+ readonly raw: ITouchClientChannel;
84
+ }
85
+
86
+ /**
87
+ * Clipboard history item shared with plugin renderers.
88
+ */
89
+ export interface PluginClipboardItem {
90
+ id?: number
91
+ type: 'text' | 'image' | 'files'
92
+ content: string
93
+ thumbnail?: string | null
94
+ rawContent?: string | null
95
+ sourceApp?: string | null
96
+ timestamp?: string | number | Date | null
97
+ isFavorite?: boolean | null
98
+ metadata?: string | null
99
+ meta?: Record<string, unknown> | null
100
+ }
101
+
102
+ /**
103
+ * Clipboard history pagination response structure.
104
+ */
105
+ export interface PluginClipboardHistoryResponse {
106
+ history: PluginClipboardItem[]
107
+ total: number
108
+ page: number
109
+ pageSize: number
110
+ }
111
+
112
+ export interface ActiveAppSnapshot {
113
+ identifier: string | null
114
+ displayName: string | null
115
+ bundleId: string | null
116
+ processId: number | null
117
+ executablePath: string | null
118
+ platform: 'macos' | 'windows' | 'linux' | null
119
+ windowTitle: string | null
120
+ url?: string | null
121
+ icon?: string | null
122
+ lastUpdated: number
123
+ }
124
+
7
125
  /**
8
126
  * Plugin utilities interface providing core functionality for plugin development
9
127
  *
@@ -28,6 +146,12 @@ export interface IPluginUtils {
28
146
  */
29
147
  clipboard: IClipboardManager;
30
148
 
149
+ /**
150
+ * Channel bridge for communicating with renderer and main processes
151
+ * @see {@link IPluginChannelBridge}
152
+ */
153
+ channel: IPluginChannelBridge;
154
+
31
155
  /**
32
156
  * Search result manager for handling search operations
33
157
  * @see {@link ISearchManager}