@peers-app/peers-ui 0.15.4 → 0.16.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.
- package/.github/workflows/publish.yml +3 -3
- package/dist/components/markdown-editor/editor.js +3 -1
- package/dist/components/markdown-editor/move-line-plugin.d.ts +18 -0
- package/dist/components/markdown-editor/move-line-plugin.js +161 -0
- package/dist/components/markdown-editor/select-line-boundary-plugin.d.ts +8 -0
- package/dist/components/markdown-editor/select-line-boundary-plugin.js +71 -0
- package/dist/screens/data-explorer/data-explorer.js +64 -16
- package/package.json +3 -3
- package/src/components/markdown-editor/editor.tsx +4 -0
- package/src/components/markdown-editor/move-line-plugin.tsx +186 -0
- package/src/components/markdown-editor/select-line-boundary-plugin.tsx +95 -0
- package/src/screens/data-explorer/data-explorer.tsx +271 -76
- package/docs/conversation-tab.md +0 -201
- package/docs/getting-started.md +0 -284
- package/docs/knowledge.md +0 -187
- package/docs/tabs-ui.md +0 -681
- package/docs/user-contacts-ui.md +0 -384
|
@@ -13,11 +13,19 @@ interface ITableInfo {
|
|
|
13
13
|
bytes?: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
interface IDeletedTableInfo {
|
|
17
|
+
tableName: string;
|
|
18
|
+
tableId: string;
|
|
19
|
+
deletedAt: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
type DataExplorerElectronApi = {
|
|
17
23
|
getCurrentContext: () => Promise<{ contextName: string }>;
|
|
18
24
|
getAllTables: () => Promise<ITableInfo[]>;
|
|
19
25
|
compactDatabase: (beforeTimestamp?: number) => Promise<void>;
|
|
20
26
|
resetChangeTracking: () => Promise<void>;
|
|
27
|
+
deleteTable: (tableName: string) => Promise<void>;
|
|
28
|
+
getDeletedTables: () => Promise<IDeletedTableInfo[]>;
|
|
21
29
|
};
|
|
22
30
|
|
|
23
31
|
type WindowWithDataExplorer = Window & {
|
|
@@ -28,11 +36,112 @@ function getDataExplorerApi(): DataExplorerElectronApi | undefined {
|
|
|
28
36
|
return (window as WindowWithDataExplorer).electronAPI?.dataExplorer;
|
|
29
37
|
}
|
|
30
38
|
|
|
39
|
+
function formatRelativeTime(isoString: string): string {
|
|
40
|
+
const date = new Date(isoString);
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const diffMs = now - date.getTime();
|
|
43
|
+
const diffMin = Math.floor(diffMs / 60_000);
|
|
44
|
+
if (diffMin < 1) return "just now";
|
|
45
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
46
|
+
const diffHrs = Math.floor(diffMin / 60);
|
|
47
|
+
if (diffHrs < 24) return `${diffHrs}h ago`;
|
|
48
|
+
const diffDays = Math.floor(diffHrs / 24);
|
|
49
|
+
if (diffDays < 30) return `${diffDays}d ago`;
|
|
50
|
+
return date.toLocaleDateString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function DeleteConfirmModal({
|
|
54
|
+
tableName,
|
|
55
|
+
onConfirm,
|
|
56
|
+
onCancel,
|
|
57
|
+
}: {
|
|
58
|
+
tableName: string;
|
|
59
|
+
onConfirm: () => void;
|
|
60
|
+
onCancel: () => void;
|
|
61
|
+
}) {
|
|
62
|
+
const [confirmText, setConfirmText] = useState("");
|
|
63
|
+
const [deleting, setDeleting] = useState(false);
|
|
64
|
+
|
|
65
|
+
const handleConfirm = async () => {
|
|
66
|
+
setDeleting(true);
|
|
67
|
+
try {
|
|
68
|
+
await onConfirm();
|
|
69
|
+
} finally {
|
|
70
|
+
setDeleting(false);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
className="position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
|
|
77
|
+
style={{ zIndex: 1050, backgroundColor: "rgba(0,0,0,0.5)" }}
|
|
78
|
+
onClick={onCancel}
|
|
79
|
+
>
|
|
80
|
+
<div
|
|
81
|
+
className="card shadow-lg"
|
|
82
|
+
style={{ maxWidth: 480, width: "100%" }}
|
|
83
|
+
onClick={(e) => e.stopPropagation()}
|
|
84
|
+
>
|
|
85
|
+
<div className="card-header bg-danger text-white d-flex justify-content-between align-items-center">
|
|
86
|
+
<strong>Delete Table</strong>
|
|
87
|
+
<button type="button" className="btn-close btn-close-white" onClick={onCancel} />
|
|
88
|
+
</div>
|
|
89
|
+
<div className="card-body">
|
|
90
|
+
<div className="alert alert-danger mb-3">
|
|
91
|
+
<i className="bi bi-exclamation-triangle-fill me-2" />
|
|
92
|
+
This will permanently delete all data for <strong>{tableName}</strong> on this device
|
|
93
|
+
and all synced devices once synced.
|
|
94
|
+
</div>
|
|
95
|
+
<label className="form-label">
|
|
96
|
+
Type <strong>{tableName}</strong> to confirm:
|
|
97
|
+
</label>
|
|
98
|
+
<input
|
|
99
|
+
type="text"
|
|
100
|
+
className="form-control"
|
|
101
|
+
value={confirmText}
|
|
102
|
+
onChange={(e) => setConfirmText(e.target.value)}
|
|
103
|
+
placeholder={tableName}
|
|
104
|
+
// biome-ignore lint/a11y/noAutofocus: confirmation input needs immediate focus
|
|
105
|
+
autoFocus
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="card-footer d-flex justify-content-end gap-2">
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
className="btn btn-secondary"
|
|
112
|
+
onClick={onCancel}
|
|
113
|
+
disabled={deleting}
|
|
114
|
+
>
|
|
115
|
+
Cancel
|
|
116
|
+
</button>
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
className="btn btn-danger"
|
|
120
|
+
disabled={confirmText !== tableName || deleting}
|
|
121
|
+
onClick={handleConfirm}
|
|
122
|
+
>
|
|
123
|
+
{deleting ? (
|
|
124
|
+
<>
|
|
125
|
+
<span className="spinner-border spinner-border-sm me-1" />
|
|
126
|
+
Deleting...
|
|
127
|
+
</>
|
|
128
|
+
) : (
|
|
129
|
+
"Delete Table"
|
|
130
|
+
)}
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
31
138
|
export function DataExplorerList() {
|
|
32
139
|
const [tables, setTables] = useState<ITableInfo[]>([]);
|
|
140
|
+
const [deletedTables, setDeletedTables] = useState<IDeletedTableInfo[]>([]);
|
|
33
141
|
const [loading, setLoading] = useState(true);
|
|
34
142
|
const [currentContext, setCurrentContext] = useState<string>("");
|
|
35
143
|
const [compacting, setCompacting] = useState(false);
|
|
144
|
+
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
|
36
145
|
|
|
37
146
|
const formatBytes = (bytes: number | undefined) => {
|
|
38
147
|
if (bytes === undefined) return "-";
|
|
@@ -48,10 +157,10 @@ export function DataExplorerList() {
|
|
|
48
157
|
const kb = 1024;
|
|
49
158
|
const mb = kb * 1024;
|
|
50
159
|
|
|
51
|
-
if (bytes < 10 * kb) return "text-secondary";
|
|
52
|
-
if (bytes < 10 * mb) return "text-primary";
|
|
53
|
-
if (bytes < 100 * mb) return "text-warning";
|
|
54
|
-
return "text-danger";
|
|
160
|
+
if (bytes < 10 * kb) return "text-secondary";
|
|
161
|
+
if (bytes < 10 * mb) return "text-primary";
|
|
162
|
+
if (bytes < 100 * mb) return "text-warning";
|
|
163
|
+
return "text-danger";
|
|
55
164
|
};
|
|
56
165
|
|
|
57
166
|
const loadTables = useCallback(async () => {
|
|
@@ -64,22 +173,21 @@ export function DataExplorerList() {
|
|
|
64
173
|
return;
|
|
65
174
|
}
|
|
66
175
|
|
|
67
|
-
// Get current context info
|
|
68
176
|
const contextInfo = await api.getCurrentContext();
|
|
69
177
|
setCurrentContext(contextInfo.contextName);
|
|
70
178
|
|
|
71
|
-
// Get all tables from IPC
|
|
72
179
|
const allTablesInfo = await api.getAllTables();
|
|
73
|
-
|
|
74
|
-
// Sort by registered status first, then by table name
|
|
75
180
|
allTablesInfo.sort((a: ITableInfo, b: ITableInfo) => {
|
|
76
181
|
if (a.isRegistered !== b.isRegistered) {
|
|
77
182
|
return a.isRegistered ? -1 : 1;
|
|
78
183
|
}
|
|
79
184
|
return a.tableName.localeCompare(b.tableName);
|
|
80
185
|
});
|
|
81
|
-
|
|
82
186
|
setTables(allTablesInfo);
|
|
187
|
+
|
|
188
|
+
const deleted = await api.getDeletedTables();
|
|
189
|
+
deleted.sort((a, b) => a.tableName.localeCompare(b.tableName));
|
|
190
|
+
setDeletedTables(deleted);
|
|
83
191
|
} catch (error) {
|
|
84
192
|
console.error("Error loading tables:", error);
|
|
85
193
|
} finally {
|
|
@@ -87,6 +195,22 @@ export function DataExplorerList() {
|
|
|
87
195
|
}
|
|
88
196
|
}, []);
|
|
89
197
|
|
|
198
|
+
const handleDeleteTable = async () => {
|
|
199
|
+
if (!deleteTarget) return;
|
|
200
|
+
const api = getDataExplorerApi();
|
|
201
|
+
if (!api) return;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
await api.deleteTable(deleteTarget);
|
|
205
|
+
setDeleteTarget(null);
|
|
206
|
+
setLoading(true);
|
|
207
|
+
await loadTables();
|
|
208
|
+
} catch (error: unknown) {
|
|
209
|
+
console.error("Error deleting table:", error);
|
|
210
|
+
alert(`Error deleting table: ${error instanceof Error ? error.message : String(error)}`);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
90
214
|
const compactDatabase = async (beforeTimestamp?: number) => {
|
|
91
215
|
try {
|
|
92
216
|
setCompacting(true);
|
|
@@ -110,7 +234,6 @@ export function DataExplorerList() {
|
|
|
110
234
|
await api.compactDatabase(beforeTimestamp);
|
|
111
235
|
alert(`Database compacted successfully!${isExtreme ? " (Extreme mode)" : ""}`);
|
|
112
236
|
|
|
113
|
-
// Reload tables to show updated sizes
|
|
114
237
|
setLoading(true);
|
|
115
238
|
await loadTables();
|
|
116
239
|
} catch (error: unknown) {
|
|
@@ -141,7 +264,6 @@ export function DataExplorerList() {
|
|
|
141
264
|
await api.resetChangeTracking();
|
|
142
265
|
alert("Change tracking reset successfully!");
|
|
143
266
|
|
|
144
|
-
// Reload tables to show updated sizes
|
|
145
267
|
setLoading(true);
|
|
146
268
|
await loadTables();
|
|
147
269
|
} catch (error: unknown) {
|
|
@@ -180,8 +302,19 @@ export function DataExplorerList() {
|
|
|
180
302
|
);
|
|
181
303
|
}
|
|
182
304
|
|
|
305
|
+
const registeredTables = tables.filter((t) => t.isRegistered);
|
|
306
|
+
const unregisteredTables = tables.filter((t) => !t.isRegistered);
|
|
307
|
+
|
|
183
308
|
return (
|
|
184
309
|
<div className="container-fluid p-4">
|
|
310
|
+
{deleteTarget && (
|
|
311
|
+
<DeleteConfirmModal
|
|
312
|
+
tableName={deleteTarget}
|
|
313
|
+
onConfirm={handleDeleteTable}
|
|
314
|
+
onCancel={() => setDeleteTarget(null)}
|
|
315
|
+
/>
|
|
316
|
+
)}
|
|
317
|
+
|
|
185
318
|
{/* Navigation Tabs */}
|
|
186
319
|
<ul className="nav nav-tabs mb-4">
|
|
187
320
|
<li className="nav-item">
|
|
@@ -303,14 +436,14 @@ export function DataExplorerList() {
|
|
|
303
436
|
<div className="card mb-4">
|
|
304
437
|
<div className="card-body">
|
|
305
438
|
<h5 className="card-title">
|
|
306
|
-
Registered Tables ({
|
|
439
|
+
Registered Tables ({registeredTables.length})
|
|
307
440
|
<span className="badge bg-success ms-2">Active</span>
|
|
308
441
|
</h5>
|
|
309
442
|
<p className="text-muted small">
|
|
310
443
|
These tables are registered with the TableContainer and actively used by the
|
|
311
444
|
application.
|
|
312
445
|
</p>
|
|
313
|
-
{
|
|
446
|
+
{registeredTables.length === 0 ? (
|
|
314
447
|
<p className="text-muted">No registered tables found</p>
|
|
315
448
|
) : (
|
|
316
449
|
<div className="table-responsive">
|
|
@@ -321,27 +454,36 @@ export function DataExplorerList() {
|
|
|
321
454
|
<th>Table ID</th>
|
|
322
455
|
<th className="text-end">Row Count</th>
|
|
323
456
|
<th className="text-end">Bytes</th>
|
|
457
|
+
<th style={{ width: "1%" }}></th>
|
|
324
458
|
</tr>
|
|
325
459
|
</thead>
|
|
326
460
|
<tbody>
|
|
327
|
-
{
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
</
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
461
|
+
{registeredTables.map((table) => (
|
|
462
|
+
<tr key={table.tableName}>
|
|
463
|
+
<td>
|
|
464
|
+
<strong>{table.tableName}</strong>
|
|
465
|
+
</td>
|
|
466
|
+
<td>
|
|
467
|
+
<code className="text-muted small">{table.tableId || "system"}</code>
|
|
468
|
+
</td>
|
|
469
|
+
<td className="text-end">
|
|
470
|
+
{table.rowCount !== undefined ? table.rowCount.toLocaleString() : "-"}
|
|
471
|
+
</td>
|
|
472
|
+
<td className={`text-end fw-bold ${getBytesColorClass(table.bytes)}`}>
|
|
473
|
+
{formatBytes(table.bytes)}
|
|
474
|
+
</td>
|
|
475
|
+
<td>
|
|
476
|
+
<button
|
|
477
|
+
type="button"
|
|
478
|
+
className="btn btn-sm btn-outline-danger border-0"
|
|
479
|
+
title={`Delete ${table.tableName}`}
|
|
480
|
+
onClick={() => setDeleteTarget(table.tableName)}
|
|
481
|
+
>
|
|
482
|
+
<i className="bi bi-trash" />
|
|
483
|
+
</button>
|
|
484
|
+
</td>
|
|
485
|
+
</tr>
|
|
486
|
+
))}
|
|
345
487
|
</tbody>
|
|
346
488
|
</table>
|
|
347
489
|
</div>
|
|
@@ -350,11 +492,11 @@ export function DataExplorerList() {
|
|
|
350
492
|
</div>
|
|
351
493
|
|
|
352
494
|
{/* Unregistered Tables (Raw SQLite) */}
|
|
353
|
-
{
|
|
354
|
-
<div className="card">
|
|
495
|
+
{unregisteredTables.length > 0 && (
|
|
496
|
+
<div className="card mb-4">
|
|
355
497
|
<div className="card-body">
|
|
356
498
|
<h5 className="card-title">
|
|
357
|
-
Unregistered Tables ({
|
|
499
|
+
Unregistered Tables ({unregisteredTables.length})
|
|
358
500
|
<span className="badge bg-warning text-dark ms-2">Raw SQLite</span>
|
|
359
501
|
</h5>
|
|
360
502
|
<p className="text-muted small">
|
|
@@ -370,51 +512,104 @@ export function DataExplorerList() {
|
|
|
370
512
|
<th className="text-end">Row Count</th>
|
|
371
513
|
<th className="text-end">Bytes</th>
|
|
372
514
|
<th>SQL Definition</th>
|
|
515
|
+
<th style={{ width: "1%" }}></th>
|
|
516
|
+
</tr>
|
|
517
|
+
</thead>
|
|
518
|
+
<tbody>
|
|
519
|
+
{unregisteredTables.map((table) => (
|
|
520
|
+
<tr key={table.tableName}>
|
|
521
|
+
<td>
|
|
522
|
+
<strong>{table.tableName}</strong>
|
|
523
|
+
</td>
|
|
524
|
+
<td>
|
|
525
|
+
<span className="badge bg-secondary">{table.type || "table"}</span>
|
|
526
|
+
</td>
|
|
527
|
+
<td className="text-end">
|
|
528
|
+
{table.rowCount !== undefined ? table.rowCount.toLocaleString() : "-"}
|
|
529
|
+
</td>
|
|
530
|
+
<td className={`text-end fw-bold ${getBytesColorClass(table.bytes)}`}>
|
|
531
|
+
{formatBytes(table.bytes)}
|
|
532
|
+
</td>
|
|
533
|
+
<td>
|
|
534
|
+
{table.sql ? (
|
|
535
|
+
<details>
|
|
536
|
+
<summary
|
|
537
|
+
className="cursor-pointer text-primary"
|
|
538
|
+
style={{ cursor: "pointer" }}
|
|
539
|
+
>
|
|
540
|
+
<small>View SQL</small>
|
|
541
|
+
</summary>
|
|
542
|
+
<pre
|
|
543
|
+
className="mt-2 p-2 bg-body-secondary border rounded"
|
|
544
|
+
style={{
|
|
545
|
+
fontSize: "0.75rem",
|
|
546
|
+
maxHeight: "300px",
|
|
547
|
+
overflow: "auto",
|
|
548
|
+
}}
|
|
549
|
+
>
|
|
550
|
+
<code className="text-body">{table.sql}</code>
|
|
551
|
+
</pre>
|
|
552
|
+
</details>
|
|
553
|
+
) : (
|
|
554
|
+
<span className="text-muted small">-</span>
|
|
555
|
+
)}
|
|
556
|
+
</td>
|
|
557
|
+
<td>
|
|
558
|
+
<button
|
|
559
|
+
type="button"
|
|
560
|
+
className="btn btn-sm btn-outline-danger border-0"
|
|
561
|
+
title={`Delete ${table.tableName}`}
|
|
562
|
+
onClick={() => setDeleteTarget(table.tableName)}
|
|
563
|
+
>
|
|
564
|
+
<i className="bi bi-trash" />
|
|
565
|
+
</button>
|
|
566
|
+
</td>
|
|
567
|
+
</tr>
|
|
568
|
+
))}
|
|
569
|
+
</tbody>
|
|
570
|
+
</table>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
)}
|
|
575
|
+
|
|
576
|
+
{/* Deleted Tables */}
|
|
577
|
+
{deletedTables.length > 0 && (
|
|
578
|
+
<div className="card mb-4">
|
|
579
|
+
<div className="card-body">
|
|
580
|
+
<h5 className="card-title">
|
|
581
|
+
Deleted Tables ({deletedTables.length})
|
|
582
|
+
<span className="badge bg-danger ms-2">Deleted</span>
|
|
583
|
+
</h5>
|
|
584
|
+
<p className="text-muted small">
|
|
585
|
+
These tables have been deleted. Their definitions are retained so other devices know
|
|
586
|
+
to stop syncing data for them.
|
|
587
|
+
</p>
|
|
588
|
+
<div className="table-responsive">
|
|
589
|
+
<table className="table table-hover table-sm">
|
|
590
|
+
<thead>
|
|
591
|
+
<tr>
|
|
592
|
+
<th>Table Name</th>
|
|
593
|
+
<th>Table ID</th>
|
|
594
|
+
<th>Deleted At</th>
|
|
373
595
|
</tr>
|
|
374
596
|
</thead>
|
|
375
597
|
<tbody>
|
|
376
|
-
{
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
</
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
</td>
|
|
392
|
-
<td>
|
|
393
|
-
{table.sql ? (
|
|
394
|
-
<details>
|
|
395
|
-
<summary
|
|
396
|
-
className="cursor-pointer text-primary"
|
|
397
|
-
style={{ cursor: "pointer" }}
|
|
398
|
-
>
|
|
399
|
-
<small>View SQL</small>
|
|
400
|
-
</summary>
|
|
401
|
-
<pre
|
|
402
|
-
className="mt-2 p-2 bg-body-secondary border rounded"
|
|
403
|
-
style={{
|
|
404
|
-
fontSize: "0.75rem",
|
|
405
|
-
maxHeight: "300px",
|
|
406
|
-
overflow: "auto",
|
|
407
|
-
}}
|
|
408
|
-
>
|
|
409
|
-
<code className="text-body">{table.sql}</code>
|
|
410
|
-
</pre>
|
|
411
|
-
</details>
|
|
412
|
-
) : (
|
|
413
|
-
<span className="text-muted small">-</span>
|
|
414
|
-
)}
|
|
415
|
-
</td>
|
|
416
|
-
</tr>
|
|
417
|
-
))}
|
|
598
|
+
{deletedTables.map((table) => (
|
|
599
|
+
<tr key={table.tableId}>
|
|
600
|
+
<td>
|
|
601
|
+
<strong className="text-muted">{table.tableName}</strong>
|
|
602
|
+
</td>
|
|
603
|
+
<td>
|
|
604
|
+
<code className="text-muted small">{table.tableId}</code>
|
|
605
|
+
</td>
|
|
606
|
+
<td>
|
|
607
|
+
<span title={new Date(table.deletedAt).toLocaleString()}>
|
|
608
|
+
{formatRelativeTime(table.deletedAt)}
|
|
609
|
+
</span>
|
|
610
|
+
</td>
|
|
611
|
+
</tr>
|
|
612
|
+
))}
|
|
418
613
|
</tbody>
|
|
419
614
|
</table>
|
|
420
615
|
</div>
|
package/docs/conversation-tab.md
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
# Conversation Tab Implementation Plan
|
|
2
|
-
|
|
3
|
-
## Current State Analysis
|
|
4
|
-
|
|
5
|
-
### Existing Thread/Conversation System
|
|
6
|
-
The codebase has a robust thread/conversation system with the following components:
|
|
7
|
-
|
|
8
|
-
#### **Data Model**
|
|
9
|
-
- **IMessage interface** with hierarchical threading via `messageParentId`
|
|
10
|
-
- **Thread Logic**: Parent message + replies with same `messageParentId`
|
|
11
|
-
- **Channel Integration**: Threads exist within channels but are filtered from main channel view
|
|
12
|
-
|
|
13
|
-
#### **State Management**
|
|
14
|
-
```typescript
|
|
15
|
-
// Existing persistent variables in globals.tsx
|
|
16
|
-
openThreads: persistentVar<(string | IMessage)[]> // Array of open thread IDs
|
|
17
|
-
threadViewOpen: persistentVar<boolean> // Controls side panel visibility
|
|
18
|
-
openThread(thread): Function // Opens thread in side panel
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
#### **Existing Components**
|
|
22
|
-
- **ThreadContainer**: Main thread UI with tabbed interface for multiple threads
|
|
23
|
-
- **ThreadMessageList**: Displays messages within specific thread
|
|
24
|
-
- **MessageDisplay**: Individual message rendering with thread context
|
|
25
|
-
- **OpenThreads**: Dropdown for thread switching
|
|
26
|
-
|
|
27
|
-
#### **Current Limitations**
|
|
28
|
-
- ❌ No URL routing for threads (`/threads/[id]`)
|
|
29
|
-
- ❌ No standalone thread detail views (always side panel)
|
|
30
|
-
- ❌ No deep-linking to specific conversations
|
|
31
|
-
- ❌ No integration with tabs system
|
|
32
|
-
|
|
33
|
-
## Implementation Plan: Phase 1 - Conversation Tabs
|
|
34
|
-
|
|
35
|
-
### **Goal**: Integrate existing thread system with tabs architecture
|
|
36
|
-
|
|
37
|
-
### **Step 1: Thread Routing Integration**
|
|
38
|
-
Add thread detail routes to main Router component:
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
41
|
-
// In router.tsx
|
|
42
|
-
if (path.match(/^threads\/([a-zA-Z0-9]{25})/)) {
|
|
43
|
-
const threadId = path.split('/')[1];
|
|
44
|
-
return <ThreadDetailView threadId={threadId} />;
|
|
45
|
-
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### **Step 2: Create ThreadDetailView Component**
|
|
49
|
-
New component that renders thread as main content instead of side panel:
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
// src/screens/threads/thread-details.tsx
|
|
53
|
-
export const ThreadDetails = (props: { threadId: string }) => {
|
|
54
|
-
const thread = usePromise(async () => {
|
|
55
|
-
const parentMessage = await Messages().get(props.threadId);
|
|
56
|
-
if (!parentMessage) return null;
|
|
57
|
-
updateActiveTabTitle(`Thread: ${parentMessage.message.slice(0, 30)}...`);
|
|
58
|
-
return parentMessage;
|
|
59
|
-
}, [props.threadId]);
|
|
60
|
-
|
|
61
|
-
// Render thread messages in main content area
|
|
62
|
-
return <ThreadMessageList threadId={props.threadId} />;
|
|
63
|
-
};
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### **Step 3: Thread System App Enhancement**
|
|
67
|
-
Update the threads system app to support individual thread opening:
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
// src/system-apps/threads.app.ts
|
|
71
|
-
export const threadsApp: IAppNav = {
|
|
72
|
-
name: 'Threads',
|
|
73
|
-
iconClassName: 'bi-cpu',
|
|
74
|
-
navigationPath: 'shell', // Main shell for overview
|
|
75
|
-
alwaysNewTab: false // Allow switching to existing thread list
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// New system app for individual conversations
|
|
79
|
-
export const conversationApp: IAppNav = {
|
|
80
|
-
name: 'Conversation',
|
|
81
|
-
iconClassName: 'bi-chat-dots',
|
|
82
|
-
navigationPath: 'threads/', // Will be dynamic per thread
|
|
83
|
-
alwaysNewTab: true // Each conversation gets its own tab
|
|
84
|
-
};
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### **Step 4: Thread Opening Integration**
|
|
88
|
-
Modify existing `openThread()` function to support both side panel and tab modes:
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
// Enhanced openThread function
|
|
92
|
-
const openThreadInTab = (threadId: string, messagePreview?: string) => {
|
|
93
|
-
const threadPath = `threads/${threadId}`;
|
|
94
|
-
const threadTitle = messagePreview
|
|
95
|
-
? `Thread: ${messagePreview.slice(0, 30)}...`
|
|
96
|
-
: 'Thread';
|
|
97
|
-
|
|
98
|
-
// Open thread in new tab
|
|
99
|
-
onOpenTab({
|
|
100
|
-
packageId: 'system-apps',
|
|
101
|
-
path: threadPath,
|
|
102
|
-
title: threadTitle,
|
|
103
|
-
iconClassName: 'bi-chat-dots',
|
|
104
|
-
closable: true
|
|
105
|
-
}, true); // Always new tab for threads
|
|
106
|
-
};
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### **Step 5: State Migration Strategy**
|
|
110
|
-
**Option A: Merge openThreads into activeTabs**
|
|
111
|
-
- Convert existing `openThreads` entries to tab format
|
|
112
|
-
- Remove separate thread state management
|
|
113
|
-
- Rely on activeTabs for persistence
|
|
114
|
-
|
|
115
|
-
**Option B: Parallel Systems (Recommended Phase 1)**
|
|
116
|
-
- Keep existing `openThreads` for side panel (backwards compatibility)
|
|
117
|
-
- Add new tab-based thread opening
|
|
118
|
-
- Allow users to choose: quick side panel vs dedicated tab
|
|
119
|
-
|
|
120
|
-
### **Step 6: Thread List Enhancement**
|
|
121
|
-
Create a thread list view for the main "Threads" app:
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
// src/screens/threads/thread-list.tsx
|
|
125
|
-
export const ThreadList = () => {
|
|
126
|
-
// Show user's recent threads/conversations
|
|
127
|
-
// Each thread gets "Open in Tab" button
|
|
128
|
-
// Integration with search functionality
|
|
129
|
-
};
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## Benefits of Tab-Based Threads
|
|
133
|
-
|
|
134
|
-
### **User Experience**
|
|
135
|
-
- **Multiple Conversations**: Several AI chats open simultaneously
|
|
136
|
-
- **Context Preservation**: Threads survive app reloads
|
|
137
|
-
- **Deep Linking**: Direct URLs to specific conversations
|
|
138
|
-
- **Mobile Friendly**: Tabs work better than sliding panels on mobile
|
|
139
|
-
|
|
140
|
-
### **Technical Benefits**
|
|
141
|
-
- **Consistent Architecture**: Threads follow same patterns as other detail screens
|
|
142
|
-
- **Dynamic Titles**: Smart titles showing conversation context
|
|
143
|
-
- **Search Integration**: Threads appear in app launcher search
|
|
144
|
-
- **Grid Layout Ready**: Perfect foundation for Phase 2 grid system
|
|
145
|
-
|
|
146
|
-
## Implementation Phases
|
|
147
|
-
|
|
148
|
-
### **Phase 1: Basic Thread Tabs** (This Plan)
|
|
149
|
-
- [ ] Add thread routing to Router component
|
|
150
|
-
- [ ] Create ThreadDetails component
|
|
151
|
-
- [ ] Update openThread function for tab support
|
|
152
|
-
- [ ] Add thread list view
|
|
153
|
-
- [ ] Update system apps configuration
|
|
154
|
-
|
|
155
|
-
### **Phase 2: State Unification**
|
|
156
|
-
- [ ] Migrate openThreads data to activeTabs
|
|
157
|
-
- [ ] Remove duplicate thread state management
|
|
158
|
-
- [ ] Enhance thread persistence and sync
|
|
159
|
-
|
|
160
|
-
### **Phase 3: Advanced Features**
|
|
161
|
-
- [ ] Thread search and filtering
|
|
162
|
-
- [ ] Conversation previews in app launcher
|
|
163
|
-
- [ ] Thread grouping and organization
|
|
164
|
-
- [ ] Integration with AI assistant selection
|
|
165
|
-
|
|
166
|
-
### **Phase 4: Grid Layout Integration** (Future)
|
|
167
|
-
- [ ] Split screen: content + conversation
|
|
168
|
-
- [ ] Multiple conversations in grid cells
|
|
169
|
-
- [ ] Flexible conversation positioning
|
|
170
|
-
- [ ] Context-aware conversation suggestions
|
|
171
|
-
|
|
172
|
-
## Technical Considerations
|
|
173
|
-
|
|
174
|
-
### **Thread ID Format**
|
|
175
|
-
- Existing thread IDs are message IDs (25-character strings)
|
|
176
|
-
- URL pattern: `/threads/[messageId]`
|
|
177
|
-
- Compatible with existing message lookup
|
|
178
|
-
|
|
179
|
-
### **Title Generation Strategy**
|
|
180
|
-
```typescript
|
|
181
|
-
const generateThreadTitle = (parentMessage: IMessage): string => {
|
|
182
|
-
// Use first 30 characters of parent message
|
|
183
|
-
const preview = parentMessage.message.slice(0, 30);
|
|
184
|
-
return `Thread: ${preview}${parentMessage.message.length > 30 ? '...' : ''}`;
|
|
185
|
-
};
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
### **Integration Points**
|
|
189
|
-
- **Router**: Add thread routing patterns
|
|
190
|
-
- **ThreadMessageList**: Reuse existing component
|
|
191
|
-
- **TabsLayout**: Thread tabs with conversation icons
|
|
192
|
-
- **App Launcher**: Threads searchable and launchable
|
|
193
|
-
|
|
194
|
-
## Migration Path
|
|
195
|
-
|
|
196
|
-
1. **Implement parallel systems** (old side panel + new tabs)
|
|
197
|
-
2. **User choice** during transition period
|
|
198
|
-
3. **Gradual migration** of state and preferences
|
|
199
|
-
4. **Remove legacy** side panel system once tabs proven
|
|
200
|
-
|
|
201
|
-
This approach ensures no disruption to existing users while providing enhanced capabilities for power users who want multiple conversations open simultaneously.
|