jupyterlab-codex-sidebar 0.1.4 → 0.1.6
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/.claude/settings.local.json +9 -0
- package/.github/workflows/unit-tests.yml +27 -0
- package/.jupyterlab-playwright.log +0 -0
- package/README.md +83 -9
- package/docs/images/codex-sidebar-screenshot.png +0 -0
- package/jupyterlab_codex/handlers.py +938 -297
- package/jupyterlab_codex/labextension/package.json +13 -3
- package/jupyterlab_codex/labextension/static/525.224526d045c727069de6.js +2 -0
- package/jupyterlab_codex/labextension/static/737.e7de3ad9dd6ded798340.js +1 -0
- package/jupyterlab_codex/labextension/static/remoteEntry.6ef5e7167763a316c000.js +1 -0
- package/jupyterlab_codex/protocol.py +297 -0
- package/jupyterlab_codex/runner.py +58 -15
- package/jupyterlab_codex/sessions.py +582 -97
- package/lib/codexChat.d.ts +13 -0
- package/lib/codexChat.js +2506 -0
- package/lib/codexChat.js.map +1 -0
- package/lib/codexChatAttachmentDedup.d.ts +10 -0
- package/lib/codexChatAttachmentDedup.js +35 -0
- package/lib/codexChatAttachmentDedup.js.map +1 -0
- package/lib/codexChatAttachmentLimit.d.ts +18 -0
- package/lib/codexChatAttachmentLimit.js +50 -0
- package/lib/codexChatAttachmentLimit.js.map +1 -0
- package/lib/codexChatAttachmentState.d.ts +15 -0
- package/lib/codexChatAttachmentState.js +16 -0
- package/lib/codexChatAttachmentState.js.map +1 -0
- package/lib/codexChatDocumentUtils.d.ts +70 -0
- package/lib/codexChatDocumentUtils.js +506 -0
- package/lib/codexChatDocumentUtils.js.map +1 -0
- package/lib/codexChatFormatting.d.ts +11 -0
- package/lib/codexChatFormatting.js +83 -0
- package/lib/codexChatFormatting.js.map +1 -0
- package/lib/codexChatNotice.d.ts +3 -0
- package/lib/codexChatNotice.js +74 -0
- package/lib/codexChatNotice.js.map +1 -0
- package/lib/codexChatPersistence.d.ts +35 -0
- package/lib/codexChatPersistence.js +158 -0
- package/lib/codexChatPersistence.js.map +1 -0
- package/lib/codexChatPrimitives.d.ts +44 -0
- package/lib/codexChatPrimitives.js +156 -0
- package/lib/codexChatPrimitives.js.map +1 -0
- package/lib/codexChatRender.d.ts +24 -0
- package/lib/codexChatRender.js +293 -0
- package/lib/codexChatRender.js.map +1 -0
- package/lib/codexChatSessionFactory.d.ts +15 -0
- package/lib/codexChatSessionFactory.js +45 -0
- package/lib/codexChatSessionFactory.js.map +1 -0
- package/lib/codexChatSessionKey.d.ts +3 -0
- package/lib/codexChatSessionKey.js +14 -0
- package/lib/codexChatSessionKey.js.map +1 -0
- package/lib/codexChatStorage.d.ts +4 -0
- package/lib/codexChatStorage.js +37 -0
- package/lib/codexChatStorage.js.map +1 -0
- package/lib/codexSessionResolver.d.ts +12 -0
- package/lib/codexSessionResolver.js +38 -0
- package/lib/codexSessionResolver.js.map +1 -0
- package/lib/handlers/activitySummarizer.d.ts +15 -0
- package/lib/handlers/activitySummarizer.js +327 -0
- package/lib/handlers/activitySummarizer.js.map +1 -0
- package/lib/handlers/codexMessageTypes.d.ts +30 -0
- package/lib/handlers/codexMessageTypes.js +2 -0
- package/lib/handlers/codexMessageTypes.js.map +1 -0
- package/lib/handlers/codexMessageUtils.d.ts +46 -0
- package/lib/handlers/codexMessageUtils.js +144 -0
- package/lib/handlers/codexMessageUtils.js.map +1 -0
- package/lib/handlers/handleCodexSocketMessage.d.ts +107 -0
- package/lib/handlers/handleCodexSocketMessage.js +78 -0
- package/lib/handlers/handleCodexSocketMessage.js.map +1 -0
- package/lib/handlers/sessionSyncHandler.d.ts +34 -0
- package/lib/handlers/sessionSyncHandler.js +181 -0
- package/lib/handlers/sessionSyncHandler.js.map +1 -0
- package/lib/hooks/useCodexSocket.d.ts +15 -0
- package/lib/hooks/useCodexSocket.js +84 -0
- package/lib/hooks/useCodexSocket.js.map +1 -0
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/panel.d.ts +1 -11
- package/lib/panel.js +1 -2815
- package/lib/panel.js.map +1 -1
- package/lib/protocol.d.ts +235 -0
- package/lib/protocol.js +278 -0
- package/lib/protocol.js.map +1 -0
- package/package.json +13 -3
- package/playwright.config.cjs +27 -0
- package/playwright.unit.config.cjs +19 -0
- package/pyproject.toml +1 -1
- package/release.sh +52 -14
- package/scripts/run_playwright_e2e.sh +96 -0
- package/scripts/run_playwright_freeze_repro.sh +58 -0
- package/scripts/run_playwright_queue_repro.sh +60 -0
- package/scripts/run_playwright_repro.sh +55 -0
- package/src/codexChat.tsx +3914 -0
- package/src/codexChatAttachmentDedup.ts +47 -0
- package/src/codexChatAttachmentLimit.ts +81 -0
- package/src/codexChatAttachmentState.ts +37 -0
- package/src/codexChatDocumentUtils.ts +644 -0
- package/src/codexChatFormatting.ts +94 -0
- package/src/codexChatNotice.ts +95 -0
- package/src/codexChatPersistence.ts +191 -0
- package/src/codexChatPrimitives.tsx +446 -0
- package/src/codexChatRender.tsx +376 -0
- package/src/codexChatSessionFactory.ts +79 -0
- package/src/codexChatSessionKey.ts +16 -0
- package/src/codexChatStorage.ts +36 -0
- package/src/codexSessionResolver.ts +56 -0
- package/src/handlers/activitySummarizer.ts +369 -0
- package/src/handlers/codexMessageTypes.ts +34 -0
- package/src/handlers/codexMessageUtils.ts +217 -0
- package/src/handlers/handleCodexSocketMessage.ts +204 -0
- package/src/handlers/sessionSyncHandler.ts +308 -0
- package/src/hooks/useCodexSocket.ts +109 -0
- package/src/index.ts +1 -1
- package/src/panel.tsx +1 -4184
- package/src/protocol.ts +582 -0
- package/style/index.css +480 -11
- package/test-results/.last-run.json +4 -0
- package/test.py +0 -0
- package/tests/e2e/cell-output-error-tail.spec.js +156 -0
- package/tests/e2e/codex-ui-test-helpers.js +138 -0
- package/tests/e2e/fixtures/notebooks/error-output-tail.ipynb +58 -0
- package/tests/e2e/fixtures/notebooks/error-output-tail.py +19 -0
- package/tests/e2e/fixtures/notebooks/tab1.ipynb +322 -0
- package/tests/e2e/fixtures/notebooks/tab1.py +272 -0
- package/tests/e2e/fixtures/notebooks/tab2.ipynb +252 -0
- package/tests/e2e/fixtures/notebooks/tab2.py +231 -0
- package/tests/e2e/fixtures/notebooks/tab3.ipynb +403 -0
- package/tests/e2e/fixtures/notebooks/tab3.py +331 -0
- package/tests/e2e/fixtures/notebooks/tab4.py +339 -0
- package/tests/e2e/freeze-notebook-tabs-repro.spec.js +295 -0
- package/tests/e2e/mock-codex-cli-flood.py +127 -0
- package/tests/e2e/mock-codex-cli-prompt-echo.py +88 -0
- package/tests/e2e/mock-codex-cli.py +95 -0
- package/tests/e2e/queue-multitab-repro.spec.js +189 -0
- package/tests/test_handlers.py +116 -0
- package/tests/test_protocol.py +169 -0
- package/tests/test_session_store_limits.py +50 -0
- package/tests/unit/codexChatAttachmentDedup.spec.ts +56 -0
- package/tests/unit/codexChatAttachmentLimit.spec.ts +57 -0
- package/tests/unit/codexChatAttachmentState.spec.ts +71 -0
- package/tests/unit/codexChatDocumentUtils.spec.ts +63 -0
- package/tests/unit/codexChatLimit.spec.ts +18 -0
- package/tests/unit/codexChatNotice.spec.ts +45 -0
- package/tests/unit/codexChatPersistence.spec.ts +199 -0
- package/tests/unit/codexChatSessionFactory.spec.ts +94 -0
- package/tests/unit/codexChatSessionKey.spec.ts +18 -0
- package/tests/unit/codexMessageUtils.spec.ts +89 -0
- package/tests/unit/codexSessionResolver.spec.ts +92 -0
- package/tests/unit/handleCodexSocketMessage.spec.ts +476 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/webpack.config.js +6 -0
- package/jupyterlab_codex/labextension/static/504.335f3447c84ba3d74517.js +0 -2
- package/jupyterlab_codex/labextension/static/972.8e856719e40acc1ef4cb.js +0 -1
- package/jupyterlab_codex/labextension/static/remoteEntry.a2982f776a1f0f515640.js +0 -1
- /package/jupyterlab_codex/labextension/static/{504.335f3447c84ba3d74517.js.LICENSE.txt → 525.224526d045c727069de6.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { CodexRateLimitsSnapshot } from './handlers/codexMessageTypes';
|
|
2
|
+
import { coerceRateLimitsSnapshot as coerceRateLimitsSnapshotShared } from './handlers/codexMessageUtils';
|
|
3
|
+
|
|
4
|
+
export function truncateMiddle(value: string, max: number): string {
|
|
5
|
+
if (value.length <= max) {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
const ellipsis = '...';
|
|
9
|
+
if (max <= ellipsis.length) {
|
|
10
|
+
return value.slice(0, max);
|
|
11
|
+
}
|
|
12
|
+
const head = Math.max(0, Math.floor((max - ellipsis.length) * 0.6));
|
|
13
|
+
const tail = Math.max(0, max - head - ellipsis.length);
|
|
14
|
+
return `${value.slice(0, head)}${ellipsis}${value.slice(value.length - tail)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function coerceRateLimitsSnapshot(raw: any): CodexRateLimitsSnapshot | null {
|
|
18
|
+
return coerceRateLimitsSnapshotShared(raw);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function clampNumber(value: number, min: number, max: number): number {
|
|
22
|
+
return Math.min(max, Math.max(min, value));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function percentLeftFromUsed(usedPercent: number | null): number | null {
|
|
26
|
+
if (typeof usedPercent !== 'number' || !Number.isFinite(usedPercent)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return Math.round(clampNumber(100 - usedPercent, 0, 100));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function safeParseDateMs(value: string | null): number | null {
|
|
33
|
+
if (!value) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const ms = Date.parse(value);
|
|
37
|
+
return Number.isFinite(ms) ? ms : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function formatDurationShort(ms: number): string {
|
|
41
|
+
if (!Number.isFinite(ms)) {
|
|
42
|
+
return '0m';
|
|
43
|
+
}
|
|
44
|
+
const totalMinutes = Math.max(0, Math.floor(ms / 60000));
|
|
45
|
+
const days = Math.floor(totalMinutes / (60 * 24));
|
|
46
|
+
const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
|
|
47
|
+
const minutes = totalMinutes % 60;
|
|
48
|
+
if (days > 0) {
|
|
49
|
+
return `${days}d ${hours}h`;
|
|
50
|
+
}
|
|
51
|
+
if (hours > 0) {
|
|
52
|
+
return `${hours}h ${minutes}m`;
|
|
53
|
+
}
|
|
54
|
+
return `${minutes}m`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatRunDuration(ms: number): string {
|
|
58
|
+
if (!Number.isFinite(ms)) {
|
|
59
|
+
return '0s';
|
|
60
|
+
}
|
|
61
|
+
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
|
62
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
63
|
+
const seconds = totalSeconds % 60;
|
|
64
|
+
if (minutes > 0) {
|
|
65
|
+
return `${minutes}m ${seconds}s`;
|
|
66
|
+
}
|
|
67
|
+
return `${seconds}s`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function formatResetsIn(resetsAtSec: number | null, nowMs: number): string {
|
|
71
|
+
if (typeof resetsAtSec !== 'number' || !Number.isFinite(resetsAtSec)) {
|
|
72
|
+
return 'Unknown';
|
|
73
|
+
}
|
|
74
|
+
const diffMs = resetsAtSec * 1000 - nowMs;
|
|
75
|
+
if (diffMs <= 0) {
|
|
76
|
+
return 'Overdue';
|
|
77
|
+
}
|
|
78
|
+
return formatDurationShort(diffMs);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function formatTokenCount(value: number | null): string {
|
|
82
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
83
|
+
return '--';
|
|
84
|
+
}
|
|
85
|
+
return value.toLocaleString();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getBrowserNotificationPermission(): NotificationPermission | 'unsupported' {
|
|
89
|
+
if (typeof window === 'undefined' || typeof window.Notification === 'undefined') {
|
|
90
|
+
return 'unsupported';
|
|
91
|
+
}
|
|
92
|
+
return window.Notification.permission;
|
|
93
|
+
}
|
|
94
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
type SessionNoticeParseResult = {
|
|
2
|
+
rest: string;
|
|
3
|
+
value: string | null;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const SESSION_STARTED_EN_PATTERN = /^(?:session started|session start|new thread started)/i;
|
|
7
|
+
const SESSION_STARTED_EN_COLON_PATTERN = /^(?:session start:|new thread started:)/i;
|
|
8
|
+
const SESSION_STARTED_I18N_PREFIX =
|
|
9
|
+
/^(?:session|new thread|new session|thread|conversation|セッション|スレッド|チャット|会話|会議|会话|对话|대화|세션|새로운\s*세션|새\s*세션|스레드|새로운\s*스레드|새\s*스레드|sesión|conversación|conversacion|hilo|nueva sesión|nuevo hilo)/i;
|
|
10
|
+
const SESSION_STARTED_I18N_SUFFIX =
|
|
11
|
+
/(?:start(?:ed|ing)?|started|start|resume|resumed|resolving|initialize(?:d|s)?|initializing|restarted|reopen|restart|시작|시작됨|시작되었습니다|起動|开始|已开始|已啟動|開始|開始しました|开始しました|已啟動しました|始ま|始動|会话开始|会議開始|会話開始|会話を開始|iniciada|iniciado|iniciar|inicio|inicio de|comenzó|comenzada|开始しました|始動|開始され|始まった)/i;
|
|
12
|
+
|
|
13
|
+
function normalizeWhitespace(value: string): string {
|
|
14
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function splitLeadingPhrases(value: string): boolean {
|
|
18
|
+
const leading = value.slice(0, 72);
|
|
19
|
+
return SESSION_STARTED_I18N_PREFIX.test(leading);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SESSION_RESOLVED_START_VALUES = new Set([
|
|
23
|
+
'client',
|
|
24
|
+
'client-new',
|
|
25
|
+
'force-new',
|
|
26
|
+
'mapping',
|
|
27
|
+
'mapping-on-missing',
|
|
28
|
+
'mapping-on-mismatch',
|
|
29
|
+
'new',
|
|
30
|
+
'new-on-mismatch'
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export function isStructuredSessionStartResolution(value: unknown): boolean {
|
|
34
|
+
if (typeof value !== 'string') {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return SESSION_RESOLVED_START_VALUES.has(value.trim().toLowerCase());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractTrailingParenValue(text: string): SessionNoticeParseResult {
|
|
41
|
+
const trimmed = text.trim();
|
|
42
|
+
if (!trimmed.endsWith(')')) {
|
|
43
|
+
return { rest: trimmed, value: null };
|
|
44
|
+
}
|
|
45
|
+
const start = trimmed.lastIndexOf('(');
|
|
46
|
+
if (start < 0) {
|
|
47
|
+
return { rest: trimmed, value: null };
|
|
48
|
+
}
|
|
49
|
+
const inner = trimmed.slice(start + 1, -1).trim();
|
|
50
|
+
if (!inner) {
|
|
51
|
+
return { rest: trimmed, value: null };
|
|
52
|
+
}
|
|
53
|
+
return { rest: trimmed.slice(0, start).trimEnd(), value: inner };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatSessionStartedNotice(label: string, time: string | null): string {
|
|
57
|
+
return `${label}${time ? ` (${time})` : ''}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeSessionStartedNotice(text: string): string | null {
|
|
61
|
+
const raw = text.trim();
|
|
62
|
+
const { rest, value } = extractTrailingParenValue(raw);
|
|
63
|
+
|
|
64
|
+
if (SESSION_STARTED_EN_PATTERN.test(rest) || SESSION_STARTED_EN_COLON_PATTERN.test(rest)) {
|
|
65
|
+
return formatSessionStartedNotice('Session started', value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const normalizedRest = normalizeWhitespace(rest);
|
|
69
|
+
if (SESSION_STARTED_I18N_PREFIX.test(normalizedRest) && SESSION_STARTED_I18N_SUFFIX.test(normalizedRest)) {
|
|
70
|
+
if (splitLeadingPhrases(normalizedRest) && normalizedRest.length <= 120) {
|
|
71
|
+
return `${normalizedRest}${value ? ` (${value})` : ''}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isSessionStartNotice(text: string, sessionResolution?: unknown): boolean {
|
|
79
|
+
if (isStructuredSessionStartResolution(sessionResolution)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const normalized = normalizeSessionStartedNotice(text);
|
|
84
|
+
if (normalized !== null) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const trimmed = text.trimStart();
|
|
89
|
+
const normalizedTrimmed = normalizeWhitespace(trimmed);
|
|
90
|
+
return (
|
|
91
|
+
SESSION_STARTED_EN_COLON_PATTERN.test(normalizedTrimmed) ||
|
|
92
|
+
/^(?:new thread started|session started|session start)/i.test(normalizedTrimmed) ||
|
|
93
|
+
(SESSION_STARTED_I18N_PREFIX.test(normalizedTrimmed) && SESSION_STARTED_I18N_SUFFIX.test(normalizedTrimmed))
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasStoredValue,
|
|
3
|
+
safeLocalStorageGet,
|
|
4
|
+
safeLocalStorageRemove,
|
|
5
|
+
safeLocalStorageSet
|
|
6
|
+
} from './codexChatStorage';
|
|
7
|
+
|
|
8
|
+
const SESSION_THREADS_STORAGE_KEY = 'jupyterlab-codex:session-threads';
|
|
9
|
+
const SESSION_THREADS_EVENT_KEY = 'jupyterlab-codex:session-threads:event';
|
|
10
|
+
const DELETE_ALL_PENDING_KEY = 'jupyterlab-codex:delete-all-pending';
|
|
11
|
+
export const SESSION_KEY_SEPARATOR = '\u0000';
|
|
12
|
+
|
|
13
|
+
export type SessionThreadSyncEvent = {
|
|
14
|
+
kind: 'new-thread';
|
|
15
|
+
sessionKey: string;
|
|
16
|
+
notebookPath: string;
|
|
17
|
+
threadId: string;
|
|
18
|
+
source: string;
|
|
19
|
+
id: string;
|
|
20
|
+
issuedAt: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const STORAGE_KEY_SESSION_THREADS = SESSION_THREADS_STORAGE_KEY;
|
|
24
|
+
export const STORAGE_KEY_SESSION_THREADS_EVENT = SESSION_THREADS_EVENT_KEY;
|
|
25
|
+
export const STORAGE_KEY_DELETE_ALL_PENDING = DELETE_ALL_PENDING_KEY;
|
|
26
|
+
|
|
27
|
+
export function parseSessionKey(sessionKey: string): { path: string } {
|
|
28
|
+
if (!sessionKey) {
|
|
29
|
+
return { path: '' };
|
|
30
|
+
}
|
|
31
|
+
const separatorIndex = sessionKey.indexOf(SESSION_KEY_SEPARATOR);
|
|
32
|
+
if (separatorIndex < 0) {
|
|
33
|
+
return { path: sessionKey };
|
|
34
|
+
}
|
|
35
|
+
return { path: sessionKey.slice(0, separatorIndex) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readStoredSessionThreads(): Record<string, string> {
|
|
39
|
+
const raw = safeLocalStorageGet(STORAGE_KEY_SESSION_THREADS);
|
|
40
|
+
if (!raw) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
const result: Record<string, string> = {};
|
|
49
|
+
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
50
|
+
if (!key || typeof value !== 'string') {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const threadId = value.trim();
|
|
54
|
+
if (!threadId) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
result[key] = threadId;
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
} catch {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getStoredSessionThreadCount(): number {
|
|
66
|
+
const mapping = readStoredSessionThreads();
|
|
67
|
+
const uniquePaths = new Set<string>();
|
|
68
|
+
for (const key of Object.keys(mapping)) {
|
|
69
|
+
const { path } = parseSessionKey(key);
|
|
70
|
+
if (path) {
|
|
71
|
+
uniquePaths.add(path);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return uniquePaths.size;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function readStoredThreadId(path: string, sessionKey: string): string {
|
|
78
|
+
const normalizedPath = path.trim();
|
|
79
|
+
const normalizedSessionKey = sessionKey || '';
|
|
80
|
+
if (!normalizedSessionKey) {
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
const mapping = readStoredSessionThreads();
|
|
84
|
+
const exactMatch = mapping[normalizedSessionKey];
|
|
85
|
+
if (exactMatch) {
|
|
86
|
+
return exactMatch;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!normalizedPath) {
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
for (const [key, threadId] of Object.entries(mapping)) {
|
|
93
|
+
if (!threadId) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const parsed = parseSessionKey(key);
|
|
97
|
+
if (parsed.path === normalizedPath) {
|
|
98
|
+
return threadId;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function persistStoredSessionThreads(sessions: Map<string, { threadId?: string }>): void {
|
|
105
|
+
const mapping: Record<string, string> = {};
|
|
106
|
+
for (const [sessionKey, session] of sessions) {
|
|
107
|
+
if (!sessionKey || !session?.threadId) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
mapping[sessionKey] = session.threadId;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
safeLocalStorageSet(STORAGE_KEY_SESSION_THREADS, JSON.stringify(mapping));
|
|
114
|
+
} catch {
|
|
115
|
+
// Ignore storage failures; in-memory sessions still work.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function coerceSessionThreadSyncEvent(value: string): SessionThreadSyncEvent | null {
|
|
120
|
+
if (!value) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
let raw: unknown;
|
|
124
|
+
try {
|
|
125
|
+
raw = JSON.parse(value);
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (!raw || typeof raw !== 'object') {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const event = raw as Record<string, unknown>;
|
|
134
|
+
const sessionKey = typeof event.sessionKey === 'string' ? event.sessionKey.trim() : '';
|
|
135
|
+
const notebookPath = typeof event.notebookPath === 'string' ? event.notebookPath.trim() : '';
|
|
136
|
+
const threadId = typeof event.threadId === 'string' ? event.threadId.trim() : '';
|
|
137
|
+
const source = typeof event.source === 'string' ? event.source.trim() : '';
|
|
138
|
+
const id = typeof event.id === 'string' ? event.id.trim() : '';
|
|
139
|
+
if (!sessionKey || !notebookPath || !threadId || !id || event.kind !== 'new-thread') {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const issuedAt =
|
|
143
|
+
typeof event.issuedAt === 'number' && Number.isFinite(event.issuedAt) ? event.issuedAt : Date.now();
|
|
144
|
+
return { kind: 'new-thread', sessionKey, notebookPath, threadId, source, id, issuedAt };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function buildSessionThreadSyncEvent(params: {
|
|
148
|
+
sessionKey: string;
|
|
149
|
+
notebookPath: string;
|
|
150
|
+
threadId: string;
|
|
151
|
+
source: string;
|
|
152
|
+
createEventId: () => string;
|
|
153
|
+
}): SessionThreadSyncEvent {
|
|
154
|
+
const sessionKey = params.sessionKey.trim();
|
|
155
|
+
const notebookPath = params.notebookPath.trim();
|
|
156
|
+
const threadId = params.threadId.trim();
|
|
157
|
+
const source = params.source.trim();
|
|
158
|
+
return {
|
|
159
|
+
kind: 'new-thread',
|
|
160
|
+
sessionKey,
|
|
161
|
+
notebookPath,
|
|
162
|
+
threadId,
|
|
163
|
+
source,
|
|
164
|
+
id: params.createEventId(),
|
|
165
|
+
issuedAt: Date.now()
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function writeSessionThreadSyncEvent(payload: SessionThreadSyncEvent): void {
|
|
170
|
+
try {
|
|
171
|
+
safeLocalStorageSet(STORAGE_KEY_SESSION_THREADS_EVENT, JSON.stringify(payload));
|
|
172
|
+
} catch {
|
|
173
|
+
// Ignore sync write failures; local tab still updates immediately.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function markDeleteAllPending(): void {
|
|
178
|
+
safeLocalStorageSet(STORAGE_KEY_DELETE_ALL_PENDING, '1');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function clearDeleteAllPending(): void {
|
|
182
|
+
safeLocalStorageRemove(STORAGE_KEY_DELETE_ALL_PENDING);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function hasDeleteAllPending(): boolean {
|
|
186
|
+
return safeLocalStorageGet(STORAGE_KEY_DELETE_ALL_PENDING) === '1';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function hasStoredDeleteAllPending(): boolean {
|
|
190
|
+
return hasStoredValue(STORAGE_KEY_DELETE_ALL_PENDING);
|
|
191
|
+
}
|