myrlin-workbook 0.9.27 → 0.9.28
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/logs/server.pid +1 -1
- package/package.json +1 -1
- package/src/core/td-adapter.js +2 -1
- package/src/web/public/app.js +281 -21
- package/src/web/public/index.html +6 -0
- package/src/web/public/styles.css +70 -0
- package/src/web/server.js +178 -0
package/logs/server.pid
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
55976
|
package/package.json
CHANGED
package/src/core/td-adapter.js
CHANGED
|
@@ -82,7 +82,8 @@ async function listIssues(cwd, filters = {}, binary = DEFAULT_TD_BINARY) {
|
|
|
82
82
|
if (filters.status) args.push('--status', filters.status);
|
|
83
83
|
const { stdout } = await runTd(binary, args, cwd);
|
|
84
84
|
const parsed = JSON.parse(stdout);
|
|
85
|
-
// td --json returns either an array
|
|
85
|
+
// td --json returns either an array, { issues: [...] }, or null (empty repo)
|
|
86
|
+
if (!parsed) return [];
|
|
86
87
|
return Array.isArray(parsed) ? parsed : (parsed.issues || []);
|
|
87
88
|
}
|
|
88
89
|
|
package/src/web/public/app.js
CHANGED
|
@@ -4763,13 +4763,83 @@ class CWMApp {
|
|
|
4763
4763
|
if (name === 'files') this.renderTasksFilesPanel();
|
|
4764
4764
|
}
|
|
4765
4765
|
|
|
4766
|
-
|
|
4767
|
-
|
|
4766
|
+
/**
|
|
4767
|
+
* Render the project switcher toolbar inside a td panel toolbar element.
|
|
4768
|
+
* Populates a <select> from this._tdProjects (loaded async).
|
|
4769
|
+
* @param {HTMLElement} toolbar
|
|
4770
|
+
* @param {string} currentDir - the currently shown repo dir
|
|
4771
|
+
*/
|
|
4772
|
+
_renderTdToolbar(toolbar, currentDir) {
|
|
4773
|
+
toolbar.textContent = '';
|
|
4774
|
+
const label = document.createElement('span');
|
|
4775
|
+
label.className = 'tasks-td-toolbar-label';
|
|
4776
|
+
label.textContent = 'Project';
|
|
4777
|
+
toolbar.appendChild(label);
|
|
4778
|
+
|
|
4779
|
+
const sel = document.createElement('select');
|
|
4780
|
+
sel.className = 'tasks-td-project-select';
|
|
4781
|
+
|
|
4782
|
+
const projects = this._tdProjects || [];
|
|
4783
|
+
|
|
4784
|
+
// Always include the current dir even if not yet in projects list
|
|
4785
|
+
const allDirs = new Map();
|
|
4786
|
+
allDirs.set(currentDir, this._tdProjectName(currentDir));
|
|
4787
|
+
for (const p of projects) allDirs.set(p.repoDir, p.name || this._tdProjectName(p.repoDir));
|
|
4788
|
+
|
|
4789
|
+
for (const [dir, name] of allDirs) {
|
|
4790
|
+
const opt = document.createElement('option');
|
|
4791
|
+
opt.value = dir;
|
|
4792
|
+
opt.textContent = name;
|
|
4793
|
+
opt.selected = dir === currentDir;
|
|
4794
|
+
sel.appendChild(opt);
|
|
4795
|
+
}
|
|
4796
|
+
|
|
4797
|
+
sel.addEventListener('change', () => {
|
|
4798
|
+
this._tdPanelDir = sel.value;
|
|
4799
|
+
// Mark as manually pinned so pane focus changes don't override the selection
|
|
4800
|
+
this._tdPanelDirPinned = (sel.value !== this._getTdPanelDir());
|
|
4801
|
+
this.renderTasksTdPanel();
|
|
4802
|
+
});
|
|
4803
|
+
|
|
4804
|
+
toolbar.appendChild(sel);
|
|
4805
|
+
|
|
4806
|
+
// Refresh button
|
|
4807
|
+
const refreshBtn = document.createElement('button');
|
|
4808
|
+
refreshBtn.className = 'btn btn-ghost btn-icon btn-sm tasks-td-refresh';
|
|
4809
|
+
refreshBtn.title = 'Refresh';
|
|
4810
|
+
refreshBtn.innerHTML = '↻';
|
|
4811
|
+
refreshBtn.addEventListener('click', () => this.renderTasksTdPanel());
|
|
4812
|
+
toolbar.appendChild(refreshBtn);
|
|
4813
|
+
}
|
|
4814
|
+
|
|
4815
|
+
/** Extract a short project name from a directory path */
|
|
4816
|
+
_tdProjectName(dir) {
|
|
4817
|
+
if (!dir) return '(unknown)';
|
|
4818
|
+
return dir.split('/').filter(Boolean).pop() || dir;
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
/**
|
|
4822
|
+
* Resolve the td repo dir for the currently focused terminal pane.
|
|
4823
|
+
* Falls back to the active workspace's resolved dir.
|
|
4824
|
+
* Returns null if nothing useful found.
|
|
4825
|
+
*/
|
|
4826
|
+
_getTdPanelDir() {
|
|
4827
|
+
// 1. Prefer the focused terminal pane's working dir
|
|
4828
|
+
const slot = this._activeTerminalSlot;
|
|
4829
|
+
const tp = slot !== null ? this.terminalPanes[slot] : null;
|
|
4830
|
+
if (tp && tp.spawnOpts && tp.spawnOpts.cwd) return tp.spawnOpts.cwd;
|
|
4831
|
+
// 2. Fall back to any open pane's cwd
|
|
4832
|
+
for (const p of this.terminalPanes) {
|
|
4833
|
+
if (p && p.spawnOpts && p.spawnOpts.cwd) return p.spawnOpts.cwd;
|
|
4834
|
+
}
|
|
4835
|
+
return null;
|
|
4836
|
+
}
|
|
4837
|
+
|
|
4838
|
+
async renderTasksTdPanel(container = null) {
|
|
4839
|
+
const panel = container || document.getElementById('tasks-td-panel');
|
|
4768
4840
|
if (!panel) return;
|
|
4769
4841
|
if (!this.getSetting('enableTd')) return;
|
|
4770
4842
|
|
|
4771
|
-
const ws = this.state.activeWorkspace;
|
|
4772
|
-
|
|
4773
4843
|
const showPlaceholder = (msg, isError) => {
|
|
4774
4844
|
panel.textContent = '';
|
|
4775
4845
|
const el = document.createElement('div');
|
|
@@ -4778,24 +4848,44 @@ class CWMApp {
|
|
|
4778
4848
|
panel.appendChild(el);
|
|
4779
4849
|
};
|
|
4780
4850
|
|
|
4781
|
-
|
|
4782
|
-
|
|
4851
|
+
// Determine which dir to show: manually selected > active pane > nothing
|
|
4852
|
+
const autoDir = this._getTdPanelDir();
|
|
4853
|
+
if (!this._tdPanelDir && autoDir) this._tdPanelDir = autoDir;
|
|
4854
|
+
const dir = this._tdPanelDir;
|
|
4855
|
+
|
|
4856
|
+
if (!dir) {
|
|
4857
|
+
showPlaceholder('Open a project in a terminal pane to see its td issues', false);
|
|
4783
4858
|
return;
|
|
4784
4859
|
}
|
|
4785
4860
|
|
|
4786
4861
|
showPlaceholder('Loading td issues\u2026', false);
|
|
4787
4862
|
|
|
4863
|
+
// Load projects list for dropdown (non-blocking, updates after issues load)
|
|
4864
|
+
this.api('GET', '/api/td/projects').then(projectsData => {
|
|
4865
|
+
this._tdProjects = projectsData.projects || [];
|
|
4866
|
+
// Re-render toolbar if panel is still showing the td view
|
|
4867
|
+
const toolbar = panel.querySelector('.tasks-td-toolbar');
|
|
4868
|
+
if (toolbar) this._renderTdToolbar(toolbar, dir);
|
|
4869
|
+
}).catch(() => {});
|
|
4870
|
+
|
|
4788
4871
|
try {
|
|
4789
|
-
const data = await this.api('GET', `/api/
|
|
4872
|
+
const data = await this.api('GET', `/api/td/issues?dir=${encodeURIComponent(dir)}`);
|
|
4790
4873
|
const issues = data.issues || [];
|
|
4791
4874
|
|
|
4875
|
+
panel.textContent = '';
|
|
4876
|
+
const toolbar = document.createElement('div');
|
|
4877
|
+
toolbar.className = 'tasks-td-toolbar';
|
|
4878
|
+
this._renderTdToolbar(toolbar, dir);
|
|
4879
|
+
panel.appendChild(toolbar);
|
|
4880
|
+
|
|
4792
4881
|
if (issues.length === 0) {
|
|
4793
|
-
|
|
4882
|
+
const empty = document.createElement('div');
|
|
4883
|
+
empty.className = 'tasks-placeholder';
|
|
4884
|
+
empty.textContent = 'No open td issues for this project';
|
|
4885
|
+
panel.appendChild(empty);
|
|
4794
4886
|
return;
|
|
4795
4887
|
}
|
|
4796
4888
|
|
|
4797
|
-
panel.textContent = '';
|
|
4798
|
-
|
|
4799
4889
|
// Group by status in display order
|
|
4800
4890
|
const STATUS_ORDER = ['in_progress', 'in_review', 'blocked', 'open'];
|
|
4801
4891
|
const STATUS_LABELS = {
|
|
@@ -4869,6 +4959,10 @@ class CWMApp {
|
|
|
4869
4959
|
if (msg.includes('not initialized')) {
|
|
4870
4960
|
// td isn't initialized — show a helpful prompt rather than a raw error
|
|
4871
4961
|
panel.textContent = '';
|
|
4962
|
+
const toolbar = document.createElement('div');
|
|
4963
|
+
toolbar.className = 'tasks-td-toolbar';
|
|
4964
|
+
this._renderTdToolbar(toolbar, dir);
|
|
4965
|
+
panel.appendChild(toolbar);
|
|
4872
4966
|
const wrap = document.createElement('div');
|
|
4873
4967
|
wrap.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:12px;padding:32px 24px;';
|
|
4874
4968
|
const info = document.createElement('div');
|
|
@@ -4877,7 +4971,7 @@ class CWMApp {
|
|
|
4877
4971
|
info.textContent = 'td is not initialized for this project.';
|
|
4878
4972
|
const hint = document.createElement('div');
|
|
4879
4973
|
hint.style.cssText = 'font-size:12px;color:var(--subtext0);text-align:center;';
|
|
4880
|
-
hint.textContent = 'Run td init in the project root to enable task tracking.
|
|
4974
|
+
hint.textContent = 'Run td init in the project root to enable task tracking.';
|
|
4881
4975
|
const initBtn = document.createElement('button');
|
|
4882
4976
|
initBtn.className = 'btn btn-primary btn-sm';
|
|
4883
4977
|
initBtn.textContent = 'Run td init';
|
|
@@ -4885,7 +4979,14 @@ class CWMApp {
|
|
|
4885
4979
|
initBtn.disabled = true;
|
|
4886
4980
|
initBtn.textContent = 'Initializing\u2026';
|
|
4887
4981
|
try {
|
|
4888
|
-
|
|
4982
|
+
// Use the workspace-scoped init if we can find the ws, otherwise use the dir directly
|
|
4983
|
+
const ws = this.state.activeWorkspace;
|
|
4984
|
+
if (ws) {
|
|
4985
|
+
await this.api('POST', `/api/workspaces/${ws.id}/td/init`, { repoDir: dir });
|
|
4986
|
+
} else {
|
|
4987
|
+
// Fallback: run td init via a generic endpoint if workspace isn't known
|
|
4988
|
+
throw new Error('No active workspace to run td init against. Run `td init` manually in ' + dir);
|
|
4989
|
+
}
|
|
4889
4990
|
this.renderTasksTdPanel();
|
|
4890
4991
|
} catch (e) {
|
|
4891
4992
|
initBtn.disabled = false;
|
|
@@ -9978,6 +10079,24 @@ class CWMApp {
|
|
|
9978
10079
|
});
|
|
9979
10080
|
}
|
|
9980
10081
|
|
|
10082
|
+
// Pinned notes (bookmark) button — shows modal of all pinned notes for this pane's session
|
|
10083
|
+
const pinDocBtn = pane.querySelector('.terminal-pane-pinnedoc');
|
|
10084
|
+
if (pinDocBtn) {
|
|
10085
|
+
pinDocBtn.addEventListener('click', (e) => {
|
|
10086
|
+
e.stopPropagation();
|
|
10087
|
+
this._showPinnedNotesModal(slotIdx);
|
|
10088
|
+
});
|
|
10089
|
+
}
|
|
10090
|
+
|
|
10091
|
+
// Pane view back button — restores terminal after a non-terminal view (E003)
|
|
10092
|
+
const backBtn = pane.querySelector('.pane-view-back');
|
|
10093
|
+
if (backBtn) {
|
|
10094
|
+
backBtn.addEventListener('click', (e) => {
|
|
10095
|
+
e.stopPropagation();
|
|
10096
|
+
if (this.restoreTerminalInPane) this.restoreTerminalInPane(slotIdx);
|
|
10097
|
+
});
|
|
10098
|
+
}
|
|
10099
|
+
|
|
9981
10100
|
// Drag-to-reposition: make pane header draggable to swap panes
|
|
9982
10101
|
const header = pane.querySelector('.terminal-pane-header');
|
|
9983
10102
|
if (header) {
|
|
@@ -10166,6 +10285,9 @@ class CWMApp {
|
|
|
10166
10285
|
if (this.state.settings.paneColorHighlights) {
|
|
10167
10286
|
this.renderWorkspaces();
|
|
10168
10287
|
}
|
|
10288
|
+
|
|
10289
|
+
// Refresh pinned-notes badge for this pane now that a session is loaded
|
|
10290
|
+
this._refreshPanePin(slotIdx);
|
|
10169
10291
|
}
|
|
10170
10292
|
|
|
10171
10293
|
/**
|
|
@@ -10206,6 +10328,31 @@ class CWMApp {
|
|
|
10206
10328
|
el.innerHTML = `<span class="activity-dot ${dotClass}"></span>${label}${detail}`;
|
|
10207
10329
|
}
|
|
10208
10330
|
|
|
10331
|
+
/**
|
|
10332
|
+
* Show a modal listing all pinned notes for the session currently open in the given pane slot.
|
|
10333
|
+
* Fetches notes from the backend and displays them with timestamps.
|
|
10334
|
+
* If there are no pinned notes, shows an info toast instead.
|
|
10335
|
+
* @param {number} slotIdx - The terminal pane slot index
|
|
10336
|
+
*/
|
|
10337
|
+
async _showPinnedNotesModal(slotIdx) {
|
|
10338
|
+
const tp = this.terminalPanes[slotIdx];
|
|
10339
|
+
if (!tp || !tp.sessionId) return;
|
|
10340
|
+
const ws = this.state.activeWorkspace;
|
|
10341
|
+
if (!ws) return;
|
|
10342
|
+
const data = await this.api('GET', `/api/workspaces/${ws.id}/pinned-notes/${tp.sessionId}`);
|
|
10343
|
+
const notes = data.notes || [];
|
|
10344
|
+
if (notes.length === 0) {
|
|
10345
|
+
this.showToast('No pinned notes for this session', 'info');
|
|
10346
|
+
return;
|
|
10347
|
+
}
|
|
10348
|
+
const body = notes.map(n => `[${n.timestamp}]\n${n.text}`).join('\n\n---\n\n');
|
|
10349
|
+
await this.showPromptModal({
|
|
10350
|
+
title: 'Pinned Notes',
|
|
10351
|
+
fields: [{ key: 'body', type: 'textarea', value: body }],
|
|
10352
|
+
confirmText: 'Close',
|
|
10353
|
+
});
|
|
10354
|
+
}
|
|
10355
|
+
|
|
10209
10356
|
showTerminalContextMenu(slotIdx, x, y) {
|
|
10210
10357
|
const tp = this.terminalPanes[slotIdx];
|
|
10211
10358
|
if (!tp) return;
|
|
@@ -10227,6 +10374,26 @@ class CWMApp {
|
|
|
10227
10374
|
});
|
|
10228
10375
|
}
|
|
10229
10376
|
|
|
10377
|
+
// Save to Notes (only show when there's a selection)
|
|
10378
|
+
if (tp.term && tp.term.hasSelection()) {
|
|
10379
|
+
items.push({
|
|
10380
|
+
label: 'Save to Notes', icon: '📝', action: async () => {
|
|
10381
|
+
const selected = tp.term.getSelection();
|
|
10382
|
+
const ws = this.state.activeWorkspace;
|
|
10383
|
+
if (!ws) { this.showToast('No active workspace', 'error'); return; }
|
|
10384
|
+
const result = await this.showPromptModal({
|
|
10385
|
+
title: 'Save to Notes',
|
|
10386
|
+
fields: [{ key: 'text', type: 'textarea', value: selected }],
|
|
10387
|
+
confirmText: 'Save Note'
|
|
10388
|
+
});
|
|
10389
|
+
if (!result) return;
|
|
10390
|
+
await this.api('POST', '/api/workspaces/' + ws.id + '/docs/notes', { text: result.text.trim() });
|
|
10391
|
+
this.loadDocs();
|
|
10392
|
+
this.showToast('Saved to Notes', 'success');
|
|
10393
|
+
},
|
|
10394
|
+
});
|
|
10395
|
+
}
|
|
10396
|
+
|
|
10230
10397
|
// Paste from clipboard
|
|
10231
10398
|
items.push({
|
|
10232
10399
|
label: 'Paste', icon: '📋', action: () => {
|
|
@@ -10992,6 +11159,15 @@ class CWMApp {
|
|
|
10992
11159
|
tp.setFocused(true);
|
|
10993
11160
|
tp.focus();
|
|
10994
11161
|
}
|
|
11162
|
+
|
|
11163
|
+
// If Tasks > td tab is visible and not manually pinned, update to this pane's project
|
|
11164
|
+
if (this._activeTasksTab === 'td' && !this._tdPanelDirPinned) {
|
|
11165
|
+
const newDir = tp && tp.spawnOpts && tp.spawnOpts.cwd ? tp.spawnOpts.cwd : null;
|
|
11166
|
+
if (newDir && newDir !== this._tdPanelDir) {
|
|
11167
|
+
this._tdPanelDir = newDir;
|
|
11168
|
+
this.renderTasksTdPanel();
|
|
11169
|
+
}
|
|
11170
|
+
}
|
|
10995
11171
|
}
|
|
10996
11172
|
|
|
10997
11173
|
/**
|
|
@@ -11810,16 +11986,54 @@ class CWMApp {
|
|
|
11810
11986
|
if (this.els.docsRoadmapCount) this.els.docsRoadmapCount.textContent = (docs.roadmap || []).length;
|
|
11811
11987
|
if (this.els.docsRulesCount) this.els.docsRulesCount.textContent = (docs.rules || []).length;
|
|
11812
11988
|
|
|
11813
|
-
// Notes
|
|
11989
|
+
// Notes — built with DOM APIs to support pin buttons safely (no user HTML injected)
|
|
11814
11990
|
if (this.els.docsNotesList) {
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
|
|
11991
|
+
const notes = docs.notes || [];
|
|
11992
|
+
while (this.els.docsNotesList.firstChild) {
|
|
11993
|
+
this.els.docsNotesList.removeChild(this.els.docsNotesList.firstChild);
|
|
11994
|
+
}
|
|
11995
|
+
if (notes.length === 0) {
|
|
11996
|
+
const empty = document.createElement('div');
|
|
11997
|
+
empty.className = 'docs-empty';
|
|
11998
|
+
empty.textContent = 'No notes yet. Click + to add one.';
|
|
11999
|
+
this.els.docsNotesList.appendChild(empty);
|
|
12000
|
+
} else {
|
|
12001
|
+
notes.forEach((n, noteIndex) => {
|
|
12002
|
+
const noteRow = document.createElement('div');
|
|
12003
|
+
noteRow.className = 'docs-item';
|
|
12004
|
+
noteRow.dataset.index = noteIndex;
|
|
12005
|
+
|
|
12006
|
+
const timeSpan = document.createElement('span');
|
|
12007
|
+
timeSpan.className = 'docs-note-time';
|
|
12008
|
+
timeSpan.textContent = n.timestamp || '';
|
|
12009
|
+
noteRow.appendChild(timeSpan);
|
|
12010
|
+
|
|
12011
|
+
const textSpan = document.createElement('span');
|
|
12012
|
+
textSpan.className = 'docs-note-text';
|
|
12013
|
+
textSpan.textContent = n.text;
|
|
12014
|
+
noteRow.appendChild(textSpan);
|
|
12015
|
+
|
|
12016
|
+
const pinBtn = document.createElement('button');
|
|
12017
|
+
pinBtn.className = 'doc-pin-btn btn btn-ghost btn-icon btn-sm';
|
|
12018
|
+
pinBtn.textContent = '📌';
|
|
12019
|
+
pinBtn.title = 'Pin to focused terminal session';
|
|
12020
|
+
pinBtn.addEventListener('click', (e) => {
|
|
12021
|
+
e.stopPropagation();
|
|
12022
|
+
this._toggleNotePin(noteIndex, pinBtn);
|
|
12023
|
+
});
|
|
12024
|
+
noteRow.appendChild(pinBtn);
|
|
12025
|
+
|
|
12026
|
+
const delBtn = document.createElement('button');
|
|
12027
|
+
delBtn.className = 'docs-item-delete btn btn-ghost btn-icon btn-sm';
|
|
12028
|
+
delBtn.dataset.section = 'notes';
|
|
12029
|
+
delBtn.dataset.index = noteIndex;
|
|
12030
|
+
delBtn.title = 'Remove';
|
|
12031
|
+
delBtn.textContent = '×';
|
|
12032
|
+
noteRow.appendChild(delBtn);
|
|
12033
|
+
|
|
12034
|
+
this.els.docsNotesList.appendChild(noteRow);
|
|
12035
|
+
});
|
|
12036
|
+
}
|
|
11823
12037
|
}
|
|
11824
12038
|
|
|
11825
12039
|
// Goals
|
|
@@ -11971,6 +12185,52 @@ class CWMApp {
|
|
|
11971
12185
|
}
|
|
11972
12186
|
}
|
|
11973
12187
|
|
|
12188
|
+
/**
|
|
12189
|
+
* Toggle a pinned note on the currently focused terminal pane session.
|
|
12190
|
+
* @param {number} noteIndex - 0-based index of the note in the workspace docs.notes array
|
|
12191
|
+
* @param {HTMLButtonElement} buttonEl - The pin button element to update visually
|
|
12192
|
+
*/
|
|
12193
|
+
async _toggleNotePin(noteIndex, buttonEl) {
|
|
12194
|
+
const slot = this._activeTerminalSlot;
|
|
12195
|
+
const tp = (slot !== null && slot !== undefined) ? this.terminalPanes[slot] : null;
|
|
12196
|
+
if (!tp || !tp.sessionId) {
|
|
12197
|
+
this.showToast('Focus a terminal pane first', 'error');
|
|
12198
|
+
return;
|
|
12199
|
+
}
|
|
12200
|
+
const ws = this.state.activeWorkspace;
|
|
12201
|
+
if (!ws) return;
|
|
12202
|
+
const isPinned = buttonEl.classList.contains('pinned');
|
|
12203
|
+
const action = isPinned ? 'unpin' : 'pin';
|
|
12204
|
+
await this.api('POST', `/api/workspaces/${ws.id}/pinned-notes`, {
|
|
12205
|
+
sessionId: tp.sessionId,
|
|
12206
|
+
noteIndex,
|
|
12207
|
+
action
|
|
12208
|
+
});
|
|
12209
|
+
buttonEl.classList.toggle('pinned', !isPinned);
|
|
12210
|
+
await this._refreshPanePin(slot);
|
|
12211
|
+
}
|
|
12212
|
+
|
|
12213
|
+
/**
|
|
12214
|
+
* Refresh the pinned-notes badge on a terminal pane header.
|
|
12215
|
+
* Shows/hides the badge button and updates the count label.
|
|
12216
|
+
* @param {number} slotIdx - Terminal pane slot index (0-based)
|
|
12217
|
+
*/
|
|
12218
|
+
async _refreshPanePin(slotIdx) {
|
|
12219
|
+
const tp = this.terminalPanes[slotIdx];
|
|
12220
|
+
if (!tp || !tp.sessionId) return;
|
|
12221
|
+
const ws = this.state.activeWorkspace;
|
|
12222
|
+
if (!ws) return;
|
|
12223
|
+
const data = await this.api('GET', `/api/workspaces/${ws.id}/pinned-notes`);
|
|
12224
|
+
const pins = data[tp.sessionId] || [];
|
|
12225
|
+
const paneEl = document.getElementById(`term-pane-${slotIdx}`);
|
|
12226
|
+
if (!paneEl) return;
|
|
12227
|
+
const pinDocBtn = paneEl.querySelector('.terminal-pane-pinnedoc');
|
|
12228
|
+
if (!pinDocBtn) return;
|
|
12229
|
+
pinDocBtn.hidden = pins.length === 0;
|
|
12230
|
+
const countEl = pinDocBtn.querySelector('.pane-pin-count');
|
|
12231
|
+
if (countEl) countEl.textContent = pins.length > 0 ? pins.length : '';
|
|
12232
|
+
}
|
|
12233
|
+
|
|
11974
12234
|
|
|
11975
12235
|
/* ═══════════════════════════════════════════════════════════
|
|
11976
12236
|
TD ISSUES — docs panel integration
|
|
@@ -558,6 +558,7 @@
|
|
|
558
558
|
<path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z"/>
|
|
559
559
|
</svg>
|
|
560
560
|
</button>
|
|
561
|
+
<button class="terminal-pane-pinnedoc btn btn-ghost btn-icon btn-sm" title="Pinned notes" hidden>📌<span class="pane-pin-count"></span></button>
|
|
561
562
|
<button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
|
|
562
563
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>
|
|
563
564
|
</button>
|
|
@@ -604,6 +605,7 @@
|
|
|
604
605
|
<path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z"/>
|
|
605
606
|
</svg>
|
|
606
607
|
</button>
|
|
608
|
+
<button class="terminal-pane-pinnedoc btn btn-ghost btn-icon btn-sm" title="Pinned notes" hidden>📌<span class="pane-pin-count"></span></button>
|
|
607
609
|
<button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
|
|
608
610
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>
|
|
609
611
|
</button>
|
|
@@ -650,6 +652,7 @@
|
|
|
650
652
|
<path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z"/>
|
|
651
653
|
</svg>
|
|
652
654
|
</button>
|
|
655
|
+
<button class="terminal-pane-pinnedoc btn btn-ghost btn-icon btn-sm" title="Pinned notes" hidden>📌<span class="pane-pin-count"></span></button>
|
|
653
656
|
<button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
|
|
654
657
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>
|
|
655
658
|
</button>
|
|
@@ -696,6 +699,7 @@
|
|
|
696
699
|
<path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z"/>
|
|
697
700
|
</svg>
|
|
698
701
|
</button>
|
|
702
|
+
<button class="terminal-pane-pinnedoc btn btn-ghost btn-icon btn-sm" title="Pinned notes" hidden>📌<span class="pane-pin-count"></span></button>
|
|
699
703
|
<button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
|
|
700
704
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>
|
|
701
705
|
</button>
|
|
@@ -742,6 +746,7 @@
|
|
|
742
746
|
<path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z"/>
|
|
743
747
|
</svg>
|
|
744
748
|
</button>
|
|
749
|
+
<button class="terminal-pane-pinnedoc btn btn-ghost btn-icon btn-sm" title="Pinned notes" hidden>📌<span class="pane-pin-count"></span></button>
|
|
745
750
|
<button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
|
|
746
751
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>
|
|
747
752
|
</button>
|
|
@@ -788,6 +793,7 @@
|
|
|
788
793
|
<path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z"/>
|
|
789
794
|
</svg>
|
|
790
795
|
</button>
|
|
796
|
+
<button class="terminal-pane-pinnedoc btn btn-ghost btn-icon btn-sm" title="Pinned notes" hidden>📌<span class="pane-pin-count"></span></button>
|
|
791
797
|
<button class="terminal-pane-close btn btn-ghost btn-icon btn-sm" hidden>
|
|
792
798
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/></svg>
|
|
793
799
|
</button>
|
|
@@ -4313,6 +4313,19 @@ textarea.input {
|
|
|
4313
4313
|
color: var(--red);
|
|
4314
4314
|
}
|
|
4315
4315
|
|
|
4316
|
+
.doc-pin-btn {
|
|
4317
|
+
opacity: 0.4;
|
|
4318
|
+
transition: opacity 0.15s;
|
|
4319
|
+
flex-shrink: 0;
|
|
4320
|
+
cursor: pointer;
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4323
|
+
.doc-pin-btn:hover,
|
|
4324
|
+
.doc-pin-btn.pinned {
|
|
4325
|
+
opacity: 1;
|
|
4326
|
+
color: var(--mauve);
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4316
4329
|
/* Roadmap items */
|
|
4317
4330
|
.roadmap-status-dot {
|
|
4318
4331
|
background: none;
|
|
@@ -5031,6 +5044,15 @@ textarea.input {
|
|
|
5031
5044
|
@media (prefers-reduced-motion: reduce) {
|
|
5032
5045
|
.terminal-pane-mic.mic-active { animation: none; }
|
|
5033
5046
|
}
|
|
5047
|
+
.pane-pin-count {
|
|
5048
|
+
font-size: 10px;
|
|
5049
|
+
background: var(--mauve);
|
|
5050
|
+
color: var(--base);
|
|
5051
|
+
border-radius: 8px;
|
|
5052
|
+
padding: 0 4px;
|
|
5053
|
+
margin-left: 2px;
|
|
5054
|
+
font-weight: 600;
|
|
5055
|
+
}
|
|
5034
5056
|
/* Voice interim transcript overlay - shown at bottom of terminal pane during recording */
|
|
5035
5057
|
.voice-interim-overlay {
|
|
5036
5058
|
position: absolute;
|
|
@@ -6321,6 +6343,20 @@ html.activity-indicators-disabled .terminal-pane-activity {
|
|
|
6321
6343
|
flex: 1;
|
|
6322
6344
|
overflow-y: auto;
|
|
6323
6345
|
min-height: 0;
|
|
6346
|
+
display: flex;
|
|
6347
|
+
flex-direction: column;
|
|
6348
|
+
}
|
|
6349
|
+
/* td panel needs its toolbar fixed at top, list scrolls below */
|
|
6350
|
+
#tasks-td-panel {
|
|
6351
|
+
display: flex;
|
|
6352
|
+
flex-direction: column;
|
|
6353
|
+
overflow: hidden;
|
|
6354
|
+
}
|
|
6355
|
+
#tasks-td-panel .tasks-td-toolbar { flex-shrink: 0; }
|
|
6356
|
+
#tasks-td-panel .tasks-td-group-header,
|
|
6357
|
+
#tasks-td-panel .tasks-td-row,
|
|
6358
|
+
#tasks-td-panel .tasks-placeholder {
|
|
6359
|
+
flex-shrink: 0;
|
|
6324
6360
|
}
|
|
6325
6361
|
|
|
6326
6362
|
/* ── Placeholder / empty states ──────────────────────── */
|
|
@@ -6394,6 +6430,40 @@ html.activity-indicators-disabled .terminal-pane-activity {
|
|
|
6394
6430
|
.td-priority-badge.priority-p2 { background: var(--yellow); color: var(--base); }
|
|
6395
6431
|
.td-priority-badge.priority-p3 { background: var(--surface2); color: var(--text); }
|
|
6396
6432
|
|
|
6433
|
+
/* ── td panel project switcher toolbar ───────────────── */
|
|
6434
|
+
.tasks-td-toolbar {
|
|
6435
|
+
display: flex;
|
|
6436
|
+
align-items: center;
|
|
6437
|
+
gap: 8px;
|
|
6438
|
+
padding: 6px 12px;
|
|
6439
|
+
border-bottom: 1px solid var(--surface1);
|
|
6440
|
+
background: var(--mantle);
|
|
6441
|
+
flex-shrink: 0;
|
|
6442
|
+
}
|
|
6443
|
+
.tasks-td-toolbar-label {
|
|
6444
|
+
font-size: 11px;
|
|
6445
|
+
color: var(--subtext0);
|
|
6446
|
+
font-weight: 600;
|
|
6447
|
+
white-space: nowrap;
|
|
6448
|
+
}
|
|
6449
|
+
.tasks-td-project-select {
|
|
6450
|
+
flex: 1;
|
|
6451
|
+
font-size: 12px;
|
|
6452
|
+
background: var(--surface0);
|
|
6453
|
+
color: var(--text);
|
|
6454
|
+
border: 1px solid var(--surface2);
|
|
6455
|
+
border-radius: 4px;
|
|
6456
|
+
padding: 2px 6px;
|
|
6457
|
+
min-width: 0;
|
|
6458
|
+
cursor: pointer;
|
|
6459
|
+
}
|
|
6460
|
+
.tasks-td-project-select:focus { outline: none; border-color: var(--mauve); }
|
|
6461
|
+
.tasks-td-refresh {
|
|
6462
|
+
font-size: 14px;
|
|
6463
|
+
padding: 2px 6px;
|
|
6464
|
+
flex-shrink: 0;
|
|
6465
|
+
}
|
|
6466
|
+
|
|
6397
6467
|
/* ─── New Task Dialog form elements ──────────────────── */
|
|
6398
6468
|
.form-group {
|
|
6399
6469
|
margin-bottom: 14px;
|
package/src/web/server.js
CHANGED
|
@@ -750,6 +750,151 @@ app.delete('/api/workspaces/:id/docs/:section/:index', requireAuth, (req, res) =
|
|
|
750
750
|
return res.json({ success: true });
|
|
751
751
|
});
|
|
752
752
|
|
|
753
|
+
// ──────────────────────────────────────────────────────────
|
|
754
|
+
// PINNED NOTES
|
|
755
|
+
// Per-session pinned note indices, persisted as JSON files
|
|
756
|
+
// under ~/.myrlin/pinned-notes/{workspaceId}.json
|
|
757
|
+
// Format: { [sessionId]: [noteIndex, ...] }
|
|
758
|
+
// ──────────────────────────────────────────────────────────
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Return the directory used to store pinned-note files.
|
|
762
|
+
* Creates it if it does not already exist.
|
|
763
|
+
* @returns {string}
|
|
764
|
+
*/
|
|
765
|
+
function getPinnedNotesDir() {
|
|
766
|
+
const dir = path.join(getDataDir(), 'pinned-notes');
|
|
767
|
+
require('fs').mkdirSync(dir, { recursive: true });
|
|
768
|
+
return dir;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Read the pinned-notes map for a workspace from disk.
|
|
773
|
+
* Returns {} if the file does not exist or cannot be parsed.
|
|
774
|
+
* @param {string} workspaceId
|
|
775
|
+
* @returns {{ [sessionId: string]: number[] }}
|
|
776
|
+
*/
|
|
777
|
+
function getPinnedNotes(workspaceId) {
|
|
778
|
+
const file = path.join(getPinnedNotesDir(), `${workspaceId}.json`);
|
|
779
|
+
try {
|
|
780
|
+
const raw = require('fs').readFileSync(file, 'utf-8');
|
|
781
|
+
return JSON.parse(raw);
|
|
782
|
+
} catch (_) {
|
|
783
|
+
return {};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Atomically write the pinned-notes map for a workspace.
|
|
789
|
+
* Writes to a temp file then renames to ensure no partial reads.
|
|
790
|
+
* @param {string} workspaceId
|
|
791
|
+
* @param {{ [sessionId: string]: number[] }} data
|
|
792
|
+
*/
|
|
793
|
+
function savePinnedNotes(workspaceId, data) {
|
|
794
|
+
const fs = require('fs');
|
|
795
|
+
const dir = getPinnedNotesDir();
|
|
796
|
+
const file = path.join(dir, `${workspaceId}.json`);
|
|
797
|
+
const tmp = path.join(dir, `.tmp.${Date.now()}.${workspaceId}.json`);
|
|
798
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
799
|
+
fs.renameSync(tmp, file);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Add noteIndex to the pinned list for sessionId, deduplicating.
|
|
804
|
+
* @param {string} wsId
|
|
805
|
+
* @param {string} sessionId
|
|
806
|
+
* @param {number} noteIndex
|
|
807
|
+
*/
|
|
808
|
+
function pin(wsId, sessionId, noteIndex) {
|
|
809
|
+
const data = getPinnedNotes(wsId);
|
|
810
|
+
const existing = data[sessionId] || [];
|
|
811
|
+
if (!existing.includes(noteIndex)) {
|
|
812
|
+
existing.push(noteIndex);
|
|
813
|
+
}
|
|
814
|
+
data[sessionId] = existing;
|
|
815
|
+
savePinnedNotes(wsId, data);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Remove noteIndex from the pinned list for sessionId.
|
|
820
|
+
* @param {string} wsId
|
|
821
|
+
* @param {string} sessionId
|
|
822
|
+
* @param {number} noteIndex
|
|
823
|
+
*/
|
|
824
|
+
function unpin(wsId, sessionId, noteIndex) {
|
|
825
|
+
const data = getPinnedNotes(wsId);
|
|
826
|
+
if (!data[sessionId]) return;
|
|
827
|
+
data[sessionId] = data[sessionId].filter(i => i !== noteIndex);
|
|
828
|
+
savePinnedNotes(wsId, data);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* GET /api/workspaces/:id/pinned-notes
|
|
833
|
+
* Returns the full pinned-notes map: { [sessionId]: [noteIndex, ...] }
|
|
834
|
+
*/
|
|
835
|
+
app.get('/api/workspaces/:id/pinned-notes', requireAuth, (req, res) => {
|
|
836
|
+
const store = getStore();
|
|
837
|
+
const ws = store.getWorkspace(req.params.id);
|
|
838
|
+
if (!ws) return res.status(404).json({ error: 'Workspace not found.' });
|
|
839
|
+
|
|
840
|
+
const data = getPinnedNotes(req.params.id);
|
|
841
|
+
return res.json(data);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* POST /api/workspaces/:id/pinned-notes
|
|
846
|
+
* Body: { sessionId, noteIndex, action: 'pin' | 'unpin' }
|
|
847
|
+
* Pins or unpins a note index for a session.
|
|
848
|
+
*/
|
|
849
|
+
app.post('/api/workspaces/:id/pinned-notes', requireAuth, (req, res) => {
|
|
850
|
+
const store = getStore();
|
|
851
|
+
const ws = store.getWorkspace(req.params.id);
|
|
852
|
+
if (!ws) return res.status(404).json({ error: 'Workspace not found.' });
|
|
853
|
+
|
|
854
|
+
const { sessionId, noteIndex, action } = req.body || {};
|
|
855
|
+
if (!sessionId || noteIndex === undefined || noteIndex === null) {
|
|
856
|
+
return res.status(400).json({ error: 'sessionId and noteIndex are required.' });
|
|
857
|
+
}
|
|
858
|
+
const idx = parseInt(noteIndex, 10);
|
|
859
|
+
if (isNaN(idx) || idx < 0) {
|
|
860
|
+
return res.status(400).json({ error: 'noteIndex must be a non-negative integer.' });
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (action === 'unpin') {
|
|
864
|
+
unpin(req.params.id, sessionId, idx);
|
|
865
|
+
} else {
|
|
866
|
+
pin(req.params.id, sessionId, idx);
|
|
867
|
+
}
|
|
868
|
+
return res.json({ success: true });
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* GET /api/workspaces/:id/pinned-notes/:sessionId
|
|
873
|
+
* Returns resolved note objects for the session's pinned indices.
|
|
874
|
+
* Response: { notes: [{ text, timestamp }, ...] }
|
|
875
|
+
* Returns empty array (not 404) if the session has no pins.
|
|
876
|
+
*/
|
|
877
|
+
app.get('/api/workspaces/:id/pinned-notes/:sessionId', requireAuth, (req, res) => {
|
|
878
|
+
const store = getStore();
|
|
879
|
+
const ws = store.getWorkspace(req.params.id);
|
|
880
|
+
if (!ws) return res.status(404).json({ error: 'Workspace not found.' });
|
|
881
|
+
|
|
882
|
+
const data = getPinnedNotes(req.params.id);
|
|
883
|
+
const indices = data[req.params.sessionId] || [];
|
|
884
|
+
|
|
885
|
+
if (indices.length === 0) {
|
|
886
|
+
return res.json({ notes: [] });
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const docs = store.getWorkspaceDocs(req.params.id);
|
|
890
|
+
const allNotes = (docs && docs.notes) ? docs.notes : [];
|
|
891
|
+
const notes = indices
|
|
892
|
+
.filter(i => i >= 0 && i < allNotes.length)
|
|
893
|
+
.map(i => ({ text: allNotes[i].text, timestamp: allNotes[i].timestamp }));
|
|
894
|
+
|
|
895
|
+
return res.json({ notes });
|
|
896
|
+
});
|
|
897
|
+
|
|
753
898
|
// ──────────────────────────────────────────────────────────
|
|
754
899
|
// TD TASK INTEGRATION
|
|
755
900
|
// These endpoints bridge myrlin's docs panel to the `td` CLI
|
|
@@ -858,6 +1003,39 @@ app.put('/api/workspaces/:id/td/repodir', requireAuth, (req, res) => {
|
|
|
858
1003
|
return res.json({ success: true, repoDir });
|
|
859
1004
|
});
|
|
860
1005
|
|
|
1006
|
+
/**
|
|
1007
|
+
* GET /api/td/projects
|
|
1008
|
+
* Return all workspaces that have td initialized, with their resolved repo dirs.
|
|
1009
|
+
* Used by the Tasks > td panel dropdown.
|
|
1010
|
+
*/
|
|
1011
|
+
app.get('/api/td/projects', requireAuth, (req, res) => {
|
|
1012
|
+
const store = getStore();
|
|
1013
|
+
const allWorkspaces = store.getAllWorkspacesList();
|
|
1014
|
+
const projects = [];
|
|
1015
|
+
for (const ws of allWorkspaces) {
|
|
1016
|
+
const repoDir = resolveTdRepoDir(store, ws.id);
|
|
1017
|
+
if (repoDir && td.isInitialized(repoDir)) {
|
|
1018
|
+
projects.push({ name: ws.name, repoDir, workspaceId: ws.id });
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return res.json({ projects });
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* GET /api/td/issues?dir=<path>
|
|
1026
|
+
* List td issues for an explicit repo directory (not workspace-scoped).
|
|
1027
|
+
* Allows the Tasks panel to show issues for whatever dir is focused.
|
|
1028
|
+
* Query: ?dir=<absolute-path>, ?status=open|in_progress|...
|
|
1029
|
+
*/
|
|
1030
|
+
app.get('/api/td/issues', requireAuth, async (req, res) => {
|
|
1031
|
+
const dir = req.query.dir ? decodeURIComponent(req.query.dir) : null;
|
|
1032
|
+
if (!dir) return res.status(400).json({ error: 'dir query parameter required.' });
|
|
1033
|
+
if (!td.isInitialized(dir)) return res.status(400).json({ error: 'td is not initialized in this directory.' });
|
|
1034
|
+
const filters = req.query.status ? { status: req.query.status } : {};
|
|
1035
|
+
const issues = await td.listIssues(dir, filters, getTdBinary());
|
|
1036
|
+
return res.json({ issues, repoDir: dir });
|
|
1037
|
+
});
|
|
1038
|
+
|
|
861
1039
|
/**
|
|
862
1040
|
* GET /api/workspaces/:id/td/issues
|
|
863
1041
|
* List td issues for this workspace's repo.
|