cli-jaw 2.0.2 → 2.0.4
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/README.ja.md +6 -2
- package/README.ko.md +6 -2
- package/README.md +6 -2
- package/dist/src/agent/alert-escalation.js +12 -1
- package/dist/src/agent/alert-escalation.js.map +1 -1
- package/dist/src/agent/error-classifier.js +14 -8
- package/dist/src/agent/error-classifier.js.map +1 -1
- package/dist/src/agent/lifecycle-handler.js +81 -4
- package/dist/src/agent/lifecycle-handler.js.map +1 -1
- package/dist/src/agent/session-persistence.js +2 -0
- package/dist/src/agent/session-persistence.js.map +1 -1
- package/dist/src/agent/spawn.js +70 -9
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/browser/connection.js +69 -15
- package/dist/src/browser/connection.js.map +1 -1
- package/dist/src/browser/runtime-diagnostics.js +39 -0
- package/dist/src/browser/runtime-diagnostics.js.map +1 -1
- package/dist/src/cli/compact.js +5 -1
- package/dist/src/cli/compact.js.map +1 -1
- package/dist/src/core/compact.js +5 -1
- package/dist/src/core/compact.js.map +1 -1
- package/dist/src/manager/lifecycle.js +1 -1
- package/dist/src/manager/lifecycle.js.map +1 -1
- package/dist/src/manager/notes/routes.js +7 -0
- package/dist/src/manager/notes/routes.js.map +1 -1
- package/dist/src/manager/notes/search.js +282 -0
- package/dist/src/manager/notes/search.js.map +1 -0
- package/dist/src/manager/preview-origin-proxy.js +12 -2
- package/dist/src/manager/preview-origin-proxy.js.map +1 -1
- package/dist/src/memory/bootstrap.js +8 -1
- package/dist/src/memory/bootstrap.js.map +1 -1
- package/dist/src/memory/indexing.js +221 -134
- package/dist/src/memory/indexing.js.map +1 -1
- package/dist/src/memory/keyword-expand.js +26 -4
- package/dist/src/memory/keyword-expand.js.map +1 -1
- package/dist/src/memory/reflect.js +119 -8
- package/dist/src/memory/reflect.js.map +1 -1
- package/dist/src/memory/synonyms.js +60 -0
- package/dist/src/memory/synonyms.js.map +1 -0
- package/dist/src/orchestrator/gateway.js +1 -1
- package/dist/src/orchestrator/gateway.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +15 -18
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/orchestrator/state-machine.js +12 -2
- package/dist/src/orchestrator/state-machine.js.map +1 -1
- package/package.json +1 -1
- package/public/dist/assets/{MilkdownWysiwygEditor-Cm3uXfWf.js → MilkdownWysiwygEditor-DIebNZF7.js} +1 -1
- package/public/dist/assets/{app-Be58Cs3Y.js → app-DJ8ys0j5.js} +4 -4
- package/public/dist/assets/{employees-CxdghzoD.js → employees-RJ_wRL09.js} +1 -1
- package/public/dist/assets/insert-image-markdown-kk053MvN.js +22 -0
- package/public/dist/assets/manager-DAe38I94.js +25 -0
- package/public/dist/assets/{manager-DEiyrWDP.css → manager-fQR46YFa.css} +1 -1
- package/public/dist/assets/{memory-CsMNkYtv.js → memory-dJGp6QBv.js} +1 -1
- package/public/dist/assets/memory-w3yQettQ.js +1 -0
- package/public/dist/assets/{render-DGQX46ei.js → render-KVGsbWj1.js} +1 -1
- package/public/dist/assets/{settings-BH213Yv3.js → settings-C7QWaUHB.js} +1 -1
- package/public/dist/assets/settings-DmUCo6lz.js +1 -0
- package/public/dist/assets/{skills-CQtCtHPA.js → skills-CHkTgM7L.js} +1 -1
- package/public/dist/assets/skills-SxG_nfwn.js +1 -0
- package/public/dist/assets/{slash-commands-Dzk1xHWS.js → slash-commands-2ThyUGvX.js} +1 -1
- package/public/dist/assets/slash-commands-BxJkKdhB.js +1 -0
- package/public/dist/assets/{trace-drawer-SRKcfm2S.js → trace-drawer-Dis80M6X.js} +1 -1
- package/public/dist/assets/ui-LhD1VfQs.js +1 -0
- package/public/dist/assets/ui-kS1ZJfez.js +143 -0
- package/public/dist/assets/{ws-CTHQFzM1.js → ws-DVE3eWRj.js} +2 -2
- package/public/dist/index.html +1 -1
- package/public/dist/manager/index.html +2 -2
- package/public/js/features/chat.ts +6 -1
- package/public/js/features/process-block.ts +34 -6
- package/public/js/features/process-step-match.ts +2 -1
- package/public/js/ui.ts +100 -13
- package/public/js/virtual-scroll-bootstrap.ts +8 -1
- package/public/js/virtual-scroll.ts +83 -13
- package/public/js/ws.ts +3 -3
- package/public/locales/en.json +2 -1
- package/public/locales/ja.json +2 -1
- package/public/locales/ko.json +2 -1
- package/public/locales/zh.json +2 -1
- package/public/manager/src/App.tsx +10 -3
- package/public/manager/src/api.ts +17 -0
- package/public/manager/src/main.tsx +1 -0
- package/public/manager/src/notes/NotesFileTree.tsx +14 -22
- package/public/manager/src/notes/NotesSearchSidebar.tsx +118 -0
- package/public/manager/src/notes/NotesSidebar.tsx +65 -23
- package/public/manager/src/notes/NotesWorkspace.tsx +13 -0
- package/public/manager/src/notes/notes-api.ts +1 -0
- package/public/manager/src/notes/notes-search.css +90 -0
- package/public/manager/src/notes/notes-types.ts +2 -0
- package/public/manager/src/preview.ts +20 -1
- package/public/manager/src/types.ts +8 -0
- package/scripts/install-wsl.sh +48 -14
- package/public/dist/assets/insert-image-markdown-DIEa-zjk.js +0 -22
- package/public/dist/assets/manager-UEXd1_9T.js +0 -25
- package/public/dist/assets/memory-DXad_DPO.js +0 -1
- package/public/dist/assets/settings-DXT87G2U.js +0 -1
- package/public/dist/assets/skills-5o_1v0nz.js +0 -1
- package/public/dist/assets/slash-commands-D4-hrrmh.js +0 -1
- package/public/dist/assets/ui-CdRKN2S6.js +0 -141
- package/public/dist/assets/ui-n43jmg_f.js +0 -1
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
DashboardLifecycleResult,
|
|
5
5
|
DashboardNoteAssetResponse,
|
|
6
6
|
DashboardNoteFileResponse,
|
|
7
|
+
DashboardNoteSearchResult,
|
|
7
8
|
DashboardNotesCapabilities,
|
|
8
9
|
DashboardNoteTreeEntry,
|
|
9
10
|
DashboardPutNoteRequest,
|
|
@@ -172,6 +173,22 @@ export async function fetchNotesCapabilities(): Promise<DashboardNotesCapabiliti
|
|
|
172
173
|
return await parseNotesResponse<DashboardNotesCapabilities>(response, `notes capabilities fetch failed: ${response.status}`);
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
export async function searchNotes(
|
|
177
|
+
query: string,
|
|
178
|
+
options: { limit?: number; regex?: boolean; signal?: AbortSignal } = {},
|
|
179
|
+
): Promise<DashboardNoteSearchResult[]> {
|
|
180
|
+
const params = new URLSearchParams({ q: query });
|
|
181
|
+
if (options.limit !== undefined) params.set('limit', String(options.limit));
|
|
182
|
+
if (options.regex) params.set('regex', 'true');
|
|
183
|
+
const init: RequestInit = {};
|
|
184
|
+
if (options.signal) init.signal = options.signal;
|
|
185
|
+
const response = await fetch(`/api/dashboard/notes/search?${params}`, init);
|
|
186
|
+
return await parseNotesResponse<DashboardNoteSearchResult[]>(
|
|
187
|
+
response,
|
|
188
|
+
`notes search failed: ${response.status}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
175
192
|
export async function fetchNoteFile(path: string): Promise<DashboardNoteFileResponse> {
|
|
176
193
|
const response = await fetch(`/api/dashboard/notes/file?path=${encodeURIComponent(path)}`);
|
|
177
194
|
return await parseNotesResponse<DashboardNoteFileResponse>(response, `note fetch failed: ${response.status}`);
|
|
@@ -12,6 +12,7 @@ import './manager-p0-1-1.css';
|
|
|
12
12
|
import './manager-dashboard-settings.css';
|
|
13
13
|
import 'katex/dist/katex.min.css';
|
|
14
14
|
import './manager-notes.css';
|
|
15
|
+
import './notes/notes-search.css';
|
|
15
16
|
import './manager-dashboard-board.css';
|
|
16
17
|
import './manager-dashboard-board-stages.css';
|
|
17
18
|
import './manager-dashboard-board-interactions.css';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState, type
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState, type DragEvent, type KeyboardEvent, type MouseEvent } from 'react';
|
|
2
2
|
import type { NotesTreeEntry } from './notes-types';
|
|
3
3
|
|
|
4
4
|
type NotesTrashItem = { path: string; kind: NotesTreeEntry['kind'] };
|
|
@@ -9,7 +9,6 @@ type NotesFileTreeProps = {
|
|
|
9
9
|
selectedFolderPath: string | null;
|
|
10
10
|
dirtyPath: string | null;
|
|
11
11
|
loading: boolean;
|
|
12
|
-
width: number;
|
|
13
12
|
notesRoot: string | null;
|
|
14
13
|
onSelectPath: (path: string) => void;
|
|
15
14
|
onSelectFolder: (path: string | null) => void;
|
|
@@ -17,9 +16,6 @@ type NotesFileTreeProps = {
|
|
|
17
16
|
onRenamePath: (path: string, kind: NotesTreeEntry['kind']) => void;
|
|
18
17
|
onTrashPath: (path: string, kind: NotesTreeEntry['kind']) => void;
|
|
19
18
|
onTrashPaths: (items: NotesTrashItem[]) => void;
|
|
20
|
-
onCreateNote: () => void;
|
|
21
|
-
onCreateFolder: () => void;
|
|
22
|
-
onRefresh: () => void;
|
|
23
19
|
};
|
|
24
20
|
|
|
25
21
|
function TreeChevron({ expanded }: { expanded: boolean }) {
|
|
@@ -48,7 +44,7 @@ function FileIcon() {
|
|
|
48
44
|
);
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
function NewNoteIcon() {
|
|
47
|
+
export function NewNoteIcon() {
|
|
52
48
|
return (
|
|
53
49
|
<svg viewBox="0 0 18 18" aria-hidden="true" className="notes-tree-action-icon">
|
|
54
50
|
<path d="M5 2.8h5.2L13.5 6v9.2H5V2.8Z" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
|
|
@@ -57,7 +53,7 @@ function NewNoteIcon() {
|
|
|
57
53
|
);
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
function NewFolderIcon() {
|
|
56
|
+
export function NewFolderIcon() {
|
|
61
57
|
return (
|
|
62
58
|
<svg viewBox="0 0 18 18" aria-hidden="true" className="notes-tree-action-icon">
|
|
63
59
|
<path d="M2.5 5.2h5.1l1.2 1.5h6.7v6.7a1.4 1.4 0 0 1-1.4 1.4H3.9a1.4 1.4 0 0 1-1.4-1.4V5.2Z" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
|
|
@@ -66,7 +62,7 @@ function NewFolderIcon() {
|
|
|
66
62
|
);
|
|
67
63
|
}
|
|
68
64
|
|
|
69
|
-
function RefreshIcon() {
|
|
65
|
+
export function RefreshIcon() {
|
|
70
66
|
return (
|
|
71
67
|
<svg viewBox="0 0 18 18" aria-hidden="true" className="notes-tree-action-icon">
|
|
72
68
|
<path d="M14.2 6.3A5.5 5.5 0 0 0 4 5.2L3 6.6M3.8 11.7A5.5 5.5 0 0 0 14 12.8l1-1.4" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
@@ -201,6 +197,12 @@ function renderEntry(
|
|
|
201
197
|
type="button"
|
|
202
198
|
className="notes-tree-folder-button"
|
|
203
199
|
aria-expanded={expanded}
|
|
200
|
+
draggable
|
|
201
|
+
onDragStart={(event) => {
|
|
202
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
203
|
+
event.dataTransfer.setData('application/x-cli-jaw-note-path', entry.path);
|
|
204
|
+
event.dataTransfer.setData('text/plain', entry.path);
|
|
205
|
+
}}
|
|
204
206
|
onClick={(event) => {
|
|
205
207
|
if (event.shiftKey || event.metaKey || event.ctrlKey) {
|
|
206
208
|
onEntryClick(entry.path, event);
|
|
@@ -224,7 +226,7 @@ function renderEntry(
|
|
|
224
226
|
event.stopPropagation();
|
|
225
227
|
const draggedPath = notePathFromDrag(event);
|
|
226
228
|
setDropTargetPath(null);
|
|
227
|
-
if (draggedPath) props.onMovePath(draggedPath, entry.path);
|
|
229
|
+
if (draggedPath && draggedPath !== entry.path && !entry.path.startsWith(`${draggedPath}/`)) props.onMovePath(draggedPath, entry.path);
|
|
228
230
|
}}
|
|
229
231
|
onKeyDown={(event) => {
|
|
230
232
|
if (event.key === 'ArrowRight' && !expanded) {
|
|
@@ -325,7 +327,6 @@ function renderEntry(
|
|
|
325
327
|
}
|
|
326
328
|
|
|
327
329
|
export function NotesFileTree(props: NotesFileTreeProps) {
|
|
328
|
-
const style = { '--notes-tree-width': `${props.width}px` } as CSSProperties;
|
|
329
330
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(() => new Set());
|
|
330
331
|
const [dropTargetPath, setDropTargetPath] = useState<string | null>(null);
|
|
331
332
|
const [multiSelected, setMultiSelected] = useState<Set<string>>(() => new Set());
|
|
@@ -446,9 +447,8 @@ export function NotesFileTree(props: NotesFileTreeProps) {
|
|
|
446
447
|
}, [multiSelected, pathKindLookup, props]);
|
|
447
448
|
|
|
448
449
|
return (
|
|
449
|
-
<
|
|
450
|
-
className={`notes-tree ${dropTargetPath === null ? 'is-root-drop-target' : ''}`}
|
|
451
|
-
style={style}
|
|
450
|
+
<div
|
|
451
|
+
className={`notes-tree-body ${dropTargetPath === null ? 'is-root-drop-target' : ''}`}
|
|
452
452
|
onDragOver={(event) => {
|
|
453
453
|
if (!hasNotePathDrag(event)) return;
|
|
454
454
|
event.preventDefault();
|
|
@@ -461,14 +461,6 @@ export function NotesFileTree(props: NotesFileTreeProps) {
|
|
|
461
461
|
props.onMovePath(draggedPath, null);
|
|
462
462
|
}}
|
|
463
463
|
>
|
|
464
|
-
<div className="notes-tree-header">
|
|
465
|
-
<strong>Notes</strong>
|
|
466
|
-
<div className="notes-tree-actions">
|
|
467
|
-
<button type="button" onClick={props.onCreateNote} title="New note" aria-label="New note"><NewNoteIcon /></button>
|
|
468
|
-
<button type="button" onClick={props.onCreateFolder} title="New folder" aria-label="New folder"><NewFolderIcon /></button>
|
|
469
|
-
<button type="button" onClick={props.onRefresh} disabled={props.loading} title="Refresh notes" aria-label="Refresh notes"><RefreshIcon /></button>
|
|
470
|
-
</div>
|
|
471
|
-
</div>
|
|
472
464
|
{multiSelected.size > 0 && (
|
|
473
465
|
<div className="notes-tree-selection-info">
|
|
474
466
|
{multiSelected.size} selected
|
|
@@ -486,6 +478,6 @@ export function NotesFileTree(props: NotesFileTreeProps) {
|
|
|
486
478
|
{!props.loading && props.entries.length > 0 && (
|
|
487
479
|
<ul className="notes-tree-list">{props.entries.map(entry => renderEntry(entry, props, expandedFolders, toggleFolder, dropTargetPath, setDropTargetPath, multiSelected, pathKindLookup, onEntryClick))}</ul>
|
|
488
480
|
)}
|
|
489
|
-
</
|
|
481
|
+
</div>
|
|
490
482
|
);
|
|
491
483
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { searchNotes } from './notes-api';
|
|
3
|
+
import type { NoteSearchResult } from './notes-types';
|
|
4
|
+
import type { NotesSidebarMode } from './NotesSidebar';
|
|
5
|
+
|
|
6
|
+
type NotesSearchSidebarProps = {
|
|
7
|
+
focusToken: number;
|
|
8
|
+
onSelect: (path: string) => void;
|
|
9
|
+
onModeChange: (mode: NotesSidebarMode) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const MIN_QUERY_LENGTH = 2;
|
|
13
|
+
const SEARCH_DEBOUNCE_MS = 275;
|
|
14
|
+
const SEARCH_LIMIT = 20;
|
|
15
|
+
|
|
16
|
+
function isAbortError(error: unknown): boolean {
|
|
17
|
+
return error instanceof DOMException && error.name === 'AbortError';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function NotesSearchSidebar(props: NotesSearchSidebarProps) {
|
|
21
|
+
const [query, setQuery] = useState('');
|
|
22
|
+
const [results, setResults] = useState<NoteSearchResult[]>([]);
|
|
23
|
+
const [loading, setLoading] = useState(false);
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
26
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
27
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
28
|
+
|
|
29
|
+
const cancelSearch = useCallback((): void => {
|
|
30
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
31
|
+
timerRef.current = null;
|
|
32
|
+
abortRef.current?.abort();
|
|
33
|
+
abortRef.current = null;
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
inputRef.current?.focus();
|
|
38
|
+
}, [props.focusToken]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
return () => cancelSearch();
|
|
42
|
+
}, [cancelSearch]);
|
|
43
|
+
|
|
44
|
+
const runSearch = useCallback((value: string): void => {
|
|
45
|
+
const trimmed = value.trim();
|
|
46
|
+
cancelSearch();
|
|
47
|
+
setError(null);
|
|
48
|
+
if (trimmed.length < MIN_QUERY_LENGTH) {
|
|
49
|
+
setResults([]);
|
|
50
|
+
setLoading(false);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
abortRef.current = controller;
|
|
55
|
+
setLoading(true);
|
|
56
|
+
void searchNotes(trimmed, { limit: SEARCH_LIMIT, signal: controller.signal })
|
|
57
|
+
.then(nextResults => {
|
|
58
|
+
if (!controller.signal.aborted) setResults(nextResults);
|
|
59
|
+
})
|
|
60
|
+
.catch(err => {
|
|
61
|
+
if (controller.signal.aborted || isAbortError(err)) return;
|
|
62
|
+
setResults([]);
|
|
63
|
+
setError((err as Error).message || 'Search failed');
|
|
64
|
+
})
|
|
65
|
+
.finally(() => {
|
|
66
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
67
|
+
});
|
|
68
|
+
}, [cancelSearch]);
|
|
69
|
+
|
|
70
|
+
function handleQueryChange(value: string): void {
|
|
71
|
+
setQuery(value);
|
|
72
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
73
|
+
timerRef.current = setTimeout(() => runSearch(value), SEARCH_DEBOUNCE_MS);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleKeyDown(event: React.KeyboardEvent): void {
|
|
77
|
+
if (event.key !== 'Escape') return;
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
props.onModeChange('files');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<section className="notes-search-sidebar" aria-label="Search notes" onKeyDown={handleKeyDown}>
|
|
84
|
+
<div className="notes-search-sidebar-header">
|
|
85
|
+
<input
|
|
86
|
+
ref={inputRef}
|
|
87
|
+
className="notes-search-input"
|
|
88
|
+
type="search"
|
|
89
|
+
placeholder="Search notes"
|
|
90
|
+
value={query}
|
|
91
|
+
onChange={event => handleQueryChange(event.currentTarget.value)}
|
|
92
|
+
aria-label="Search notes"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="notes-search-sidebar-results" aria-live="polite">
|
|
96
|
+
{loading && <div className="notes-search-loading">Searching...</div>}
|
|
97
|
+
{error && <div className="notes-search-error">{error}</div>}
|
|
98
|
+
{!error && results.map(result => (
|
|
99
|
+
<button
|
|
100
|
+
key={`${result.kind}:${result.path}:${result.line}:${result.context}`}
|
|
101
|
+
type="button"
|
|
102
|
+
className="notes-search-result"
|
|
103
|
+
onClick={() => props.onSelect(result.path)}
|
|
104
|
+
>
|
|
105
|
+
<span className="notes-search-result-path">{result.path}</span>
|
|
106
|
+
<span className="notes-search-result-line">
|
|
107
|
+
{result.kind === 'path' ? 'Path match' : `Line ${result.line}`}
|
|
108
|
+
</span>
|
|
109
|
+
<span className="notes-search-result-context">{result.context}</span>
|
|
110
|
+
</button>
|
|
111
|
+
))}
|
|
112
|
+
{!loading && !error && query.trim().length >= MIN_QUERY_LENGTH && results.length === 0 && (
|
|
113
|
+
<div className="notes-search-empty">No results</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</section>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useEffect, useState, type CSSProperties } from 'react';
|
|
2
2
|
import { createNoteFile, createNoteFolder, renameNotePath, trashNotePath } from './notes-api';
|
|
3
|
-
import { NotesFileTree } from './NotesFileTree';
|
|
3
|
+
import { NewFolderIcon, NewNoteIcon, NotesFileTree, RefreshIcon } from './NotesFileTree';
|
|
4
|
+
import { NotesSearchSidebar } from './NotesSearchSidebar';
|
|
4
5
|
import { publishInvalidation } from '../sync/invalidation-bus';
|
|
5
6
|
import type { NotesTreeEntry } from './notes-types';
|
|
6
7
|
|
|
8
|
+
export type NotesSidebarMode = 'files' | 'search';
|
|
9
|
+
|
|
7
10
|
type NotesSidebarProps = {
|
|
8
11
|
tree: NotesTreeEntry[];
|
|
9
12
|
loading: boolean;
|
|
@@ -12,6 +15,10 @@ type NotesSidebarProps = {
|
|
|
12
15
|
selectedPath: string | null;
|
|
13
16
|
dirtyPath: string | null;
|
|
14
17
|
treeWidth: number;
|
|
18
|
+
mode: NotesSidebarMode;
|
|
19
|
+
searchFocusToken: number;
|
|
20
|
+
onModeChange: (mode: NotesSidebarMode) => void;
|
|
21
|
+
onOpenSearch: () => void;
|
|
15
22
|
onSelectedPathChange: (path: string | null) => void;
|
|
16
23
|
onRefreshTree: (selectPath?: string | null) => Promise<void>;
|
|
17
24
|
};
|
|
@@ -75,7 +82,17 @@ function batchTrashConfirmMessage(items: { path: string; kind: NotesTreeEntry['k
|
|
|
75
82
|
return dirty ? `${label}\n\nThere are unsaved changes inside this selection.` : label;
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
function SearchIcon() {
|
|
86
|
+
return (
|
|
87
|
+
<svg viewBox="0 0 18 18" aria-hidden="true" className="notes-tree-action-icon">
|
|
88
|
+
<path d="M7.8 3.2a4.6 4.6 0 1 1 0 9.2 4.6 4.6 0 0 1 0-9.2Z" fill="none" stroke="currentColor" strokeWidth="1.5" />
|
|
89
|
+
<path d="m11.3 11.3 3.2 3.2" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
90
|
+
</svg>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
78
94
|
export function NotesSidebar(props: NotesSidebarProps) {
|
|
95
|
+
const style = { '--notes-tree-width': `${props.treeWidth}px` } as CSSProperties;
|
|
79
96
|
const [error, setError] = useState<string | null>(null);
|
|
80
97
|
const [status, setStatus] = useState<string | null>(null);
|
|
81
98
|
const [selectedFolderPath, setSelectedFolderPath] = useState<string | null>(null);
|
|
@@ -121,7 +138,7 @@ export function NotesSidebar(props: NotesSidebarProps) {
|
|
|
121
138
|
}
|
|
122
139
|
}
|
|
123
140
|
|
|
124
|
-
async function
|
|
141
|
+
async function movePath(from: string, toFolder: string | null): Promise<void> {
|
|
125
142
|
const to = movePathToFolder(from, toFolder);
|
|
126
143
|
if (from === to) return;
|
|
127
144
|
try {
|
|
@@ -232,27 +249,52 @@ export function NotesSidebar(props: NotesSidebarProps) {
|
|
|
232
249
|
}
|
|
233
250
|
|
|
234
251
|
return (
|
|
235
|
-
|
|
252
|
+
<aside className="notes-tree" style={style}>
|
|
236
253
|
{(props.error || error) && <section className="state error-state">{props.error || error}</section>}
|
|
237
254
|
{status && <section className="state notes-status-state">{status}</section>}
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
<div className="notes-tree-header">
|
|
256
|
+
<strong>Notes</strong>
|
|
257
|
+
<div className="notes-tree-actions">
|
|
258
|
+
<button
|
|
259
|
+
type="button"
|
|
260
|
+
className={props.mode === 'search' ? 'is-active' : ''}
|
|
261
|
+
onClick={() => {
|
|
262
|
+
if (props.mode === 'search') props.onModeChange('files');
|
|
263
|
+
else props.onOpenSearch();
|
|
264
|
+
}}
|
|
265
|
+
title="Search notes"
|
|
266
|
+
aria-label="Search notes"
|
|
267
|
+
aria-pressed={props.mode === 'search'}
|
|
268
|
+
>
|
|
269
|
+
<SearchIcon />
|
|
270
|
+
</button>
|
|
271
|
+
<button type="button" onClick={() => void createNote()} title="New note" aria-label="New note"><NewNoteIcon /></button>
|
|
272
|
+
<button type="button" onClick={() => void createFolder()} title="New folder" aria-label="New folder"><NewFolderIcon /></button>
|
|
273
|
+
<button type="button" onClick={() => void props.onRefreshTree()} disabled={props.loading} title="Refresh notes" aria-label="Refresh notes"><RefreshIcon /></button>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
{props.mode === 'files' ? (
|
|
277
|
+
<NotesFileTree
|
|
278
|
+
entries={props.tree}
|
|
279
|
+
selectedPath={props.selectedPath}
|
|
280
|
+
selectedFolderPath={selectedFolderPath}
|
|
281
|
+
dirtyPath={props.dirtyPath}
|
|
282
|
+
loading={props.loading}
|
|
283
|
+
notesRoot={props.notesRoot}
|
|
284
|
+
onSelectPath={props.onSelectedPathChange}
|
|
285
|
+
onSelectFolder={setSelectedFolderPath}
|
|
286
|
+
onMovePath={(from, toFolder) => void movePath(from, toFolder)}
|
|
287
|
+
onRenamePath={(path, kind) => void renamePath(path, kind)}
|
|
288
|
+
onTrashPath={(path, kind) => void trashPath(path, kind)}
|
|
289
|
+
onTrashPaths={items => void trashPaths(items)}
|
|
290
|
+
/>
|
|
291
|
+
) : (
|
|
292
|
+
<NotesSearchSidebar
|
|
293
|
+
focusToken={props.searchFocusToken}
|
|
294
|
+
onSelect={props.onSelectedPathChange}
|
|
295
|
+
onModeChange={props.onModeChange}
|
|
296
|
+
/>
|
|
297
|
+
)}
|
|
298
|
+
</aside>
|
|
257
299
|
);
|
|
258
300
|
}
|
|
@@ -18,6 +18,7 @@ type NotesWorkspaceProps = {
|
|
|
18
18
|
authoringMode: NotesAuthoringMode;
|
|
19
19
|
wordWrap: boolean;
|
|
20
20
|
treeWidth: number;
|
|
21
|
+
onOpenSidebarSearch: () => void;
|
|
21
22
|
onSelectedPathChange: (path: string | null) => void;
|
|
22
23
|
onDirtyPathChange: (path: string | null) => void;
|
|
23
24
|
onViewModeChange: (mode: NotesViewMode) => void;
|
|
@@ -84,6 +85,18 @@ export function NotesWorkspace(props: NotesWorkspaceProps) {
|
|
|
84
85
|
return () => window.removeEventListener('keydown', handleModeShortcut);
|
|
85
86
|
}, [props.active, props.viewMode, props.authoringMode, props.onViewModeChange, props.onAuthoringModeChange]);
|
|
86
87
|
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!props.active) return;
|
|
90
|
+
function handleSearchShortcut(event: KeyboardEvent): void {
|
|
91
|
+
if (!(event.metaKey || event.ctrlKey) || !event.shiftKey || event.key.toLowerCase() !== 'f') return;
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
props.onOpenSidebarSearch();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
window.addEventListener('keydown', handleSearchShortcut);
|
|
97
|
+
return () => window.removeEventListener('keydown', handleSearchShortcut);
|
|
98
|
+
}, [props.active, props.onOpenSidebarSearch]);
|
|
99
|
+
|
|
87
100
|
async function handleTitleBlur(event: React.FocusEvent<HTMLInputElement>): Promise<void> {
|
|
88
101
|
if (renamingRef.current || !props.selectedPath) return;
|
|
89
102
|
const newTitle = event.currentTarget.value.trim().replace(INVALID_TITLE_CHARS, '');
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
.notes-tree-actions button.is-active {
|
|
2
|
+
border-color: var(--accent-soft);
|
|
3
|
+
background: var(--accent-soft);
|
|
4
|
+
color: var(--text-primary);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.notes-search-sidebar {
|
|
8
|
+
min-height: 0;
|
|
9
|
+
display: grid;
|
|
10
|
+
grid-template-rows: max-content minmax(0, 1fr);
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.notes-search-sidebar-header {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: 8px;
|
|
18
|
+
padding: 8px;
|
|
19
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.notes-search-input {
|
|
23
|
+
min-width: 0;
|
|
24
|
+
flex: 1 1 auto;
|
|
25
|
+
height: 34px;
|
|
26
|
+
border: 1px solid var(--border-subtle);
|
|
27
|
+
border-radius: 7px;
|
|
28
|
+
padding: 0 10px;
|
|
29
|
+
background: var(--bg-panel);
|
|
30
|
+
color: var(--text-primary);
|
|
31
|
+
font: inherit;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.notes-search-sidebar-results {
|
|
35
|
+
min-height: 0;
|
|
36
|
+
overflow: auto;
|
|
37
|
+
padding: 6px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.notes-search-result {
|
|
41
|
+
width: 100%;
|
|
42
|
+
display: grid;
|
|
43
|
+
gap: 3px;
|
|
44
|
+
padding: 9px 10px;
|
|
45
|
+
border: 1px solid transparent;
|
|
46
|
+
border-radius: 7px;
|
|
47
|
+
background: transparent;
|
|
48
|
+
color: var(--text-primary);
|
|
49
|
+
text-align: left;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.notes-search-result:hover,
|
|
54
|
+
.notes-search-result:focus-visible {
|
|
55
|
+
border-color: var(--border-subtle);
|
|
56
|
+
background: var(--canvas-mid);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.notes-search-result-path {
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
text-overflow: ellipsis;
|
|
62
|
+
white-space: nowrap;
|
|
63
|
+
font-size: 13px;
|
|
64
|
+
font-weight: 600;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.notes-search-result-line {
|
|
68
|
+
color: var(--text-tertiary);
|
|
69
|
+
font-size: 11px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.notes-search-result-context {
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
text-overflow: ellipsis;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
color: var(--text-secondary);
|
|
77
|
+
font-size: 12px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.notes-search-loading,
|
|
81
|
+
.notes-search-empty,
|
|
82
|
+
.notes-search-error {
|
|
83
|
+
padding: 14px 10px;
|
|
84
|
+
color: var(--text-secondary);
|
|
85
|
+
font-size: 13px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.notes-search-error {
|
|
89
|
+
color: var(--danger, #c34a4a);
|
|
90
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
DashboardNoteFileResponse,
|
|
3
|
+
DashboardNoteSearchResult,
|
|
3
4
|
DashboardNotesCapabilities,
|
|
4
5
|
DashboardNoteTreeEntry,
|
|
5
6
|
VaultIndexSnapshot,
|
|
@@ -20,5 +21,6 @@ export type NoteConflictState = {
|
|
|
20
21
|
|
|
21
22
|
export type NotesTreeEntry = DashboardNoteTreeEntry;
|
|
22
23
|
export type NoteFile = DashboardNoteFileResponse;
|
|
24
|
+
export type NoteSearchResult = DashboardNoteSearchResult;
|
|
23
25
|
export type NotesVaultIndexSnapshot = VaultIndexSnapshot;
|
|
24
26
|
export type NotesCapabilities = DashboardNotesCapabilities;
|
|
@@ -13,10 +13,29 @@ export type PreviewTransport = 'origin-port' | 'legacy-path' | 'none';
|
|
|
13
13
|
|
|
14
14
|
type PreviewArgument = PreviewTheme | 'proxy' | 'direct' | undefined;
|
|
15
15
|
|
|
16
|
+
function isLoopbackHost(hostname: string): boolean {
|
|
17
|
+
return hostname === '127.0.0.1' || hostname === 'localhost' || hostname === '::1' || hostname === '[::1]';
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
function isPreviewTheme(value: PreviewArgument): value is PreviewTheme {
|
|
17
21
|
return value === 'dark' || value === 'light';
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
export function normalizePreviewUrlForCurrentHost(src: string, currentHref?: string): string {
|
|
25
|
+
const href = currentHref || (typeof window !== 'undefined' ? window.location.href : '');
|
|
26
|
+
if (!href || src.startsWith('/')) return src;
|
|
27
|
+
try {
|
|
28
|
+
const previewUrl = new URL(src);
|
|
29
|
+
const currentUrl = new URL(href);
|
|
30
|
+
if (isLoopbackHost(previewUrl.hostname) && isLoopbackHost(currentUrl.hostname)) {
|
|
31
|
+
previewUrl.hostname = currentUrl.hostname;
|
|
32
|
+
}
|
|
33
|
+
return previewUrl.toString();
|
|
34
|
+
} catch {
|
|
35
|
+
return src;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
20
39
|
export function appendPreviewTheme(src: string, theme: PreviewTheme | null | undefined): string {
|
|
21
40
|
if (!theme) return src;
|
|
22
41
|
const isRelative = src.startsWith('/');
|
|
@@ -51,7 +70,7 @@ export function buildPreviewState(
|
|
|
51
70
|
if (proxy.preview?.enabled && originPreview?.status === 'ready' && originPreview.url) {
|
|
52
71
|
return {
|
|
53
72
|
canPreview: true,
|
|
54
|
-
src: appendPreviewTheme(originPreview.url, theme),
|
|
73
|
+
src: appendPreviewTheme(normalizePreviewUrlForCurrentHost(originPreview.url), theme),
|
|
55
74
|
reason: null,
|
|
56
75
|
transport: 'origin-port',
|
|
57
76
|
warning: 'origin proxy ready',
|
|
@@ -285,6 +285,14 @@ export type DashboardNoteFileResponse = {
|
|
|
285
285
|
size: number;
|
|
286
286
|
};
|
|
287
287
|
|
|
288
|
+
export type DashboardNoteSearchResult = {
|
|
289
|
+
path: string;
|
|
290
|
+
line: number;
|
|
291
|
+
content: string;
|
|
292
|
+
context: string;
|
|
293
|
+
kind: 'path' | 'content';
|
|
294
|
+
};
|
|
295
|
+
|
|
288
296
|
export type DashboardPutNoteRequest = {
|
|
289
297
|
path: string;
|
|
290
298
|
content: string;
|