granola-toolkit 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/cli.js +141 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -319,6 +319,7 @@ That keeps the current single-package repo simple, while making a future split i
|
|
|
319
319
|
|
|
320
320
|
The initial terminal workspace includes:
|
|
321
321
|
|
|
322
|
+
- a folder scope inside the navigation pane, including an explicit All meetings view
|
|
322
323
|
- a meeting list pane with keyboard navigation
|
|
323
324
|
- a detail pane with notes, transcript, metadata, and raw views
|
|
324
325
|
- an auth session overlay for import, refresh, source switching, and sign-out
|
|
@@ -327,7 +328,8 @@ The initial terminal workspace includes:
|
|
|
327
328
|
|
|
328
329
|
The main keyboard controls are:
|
|
329
330
|
|
|
330
|
-
- `
|
|
331
|
+
- `h` / `l`, left / right, or `Tab` to switch between folders and meetings
|
|
332
|
+
- `j` / `k` or arrow keys to move within the active folder or meeting list
|
|
331
333
|
- `/` or `Ctrl+P` to open quick open
|
|
332
334
|
- `a` to open auth session actions
|
|
333
335
|
- `1`-`4` to switch detail tabs
|
package/dist/cli.js
CHANGED
|
@@ -1345,6 +1345,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
|
|
|
1345
1345
|
`ID: ${summary.id}`,
|
|
1346
1346
|
`Created: ${summary.createdAt}`,
|
|
1347
1347
|
`Updated: ${summary.updatedAt}`,
|
|
1348
|
+
`Folders: ${summary.folders.length > 0 ? summary.folders.map((folder) => folder.name).join(", ") : "none"}`,
|
|
1348
1349
|
`Tags: ${summary.tags.length > 0 ? summary.tags.join(", ") : "none"}`,
|
|
1349
1350
|
`Notes source: ${summary.noteContentSource}`,
|
|
1350
1351
|
`Transcript loaded: ${summary.transcriptLoaded ? "yes" : "no"}`,
|
|
@@ -1360,7 +1361,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
|
|
|
1360
1361
|
}
|
|
1361
1362
|
}
|
|
1362
1363
|
function buildGranolaTuiSummary(state, meetingSource) {
|
|
1363
|
-
return `auth ${state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | list ${meetingSource}`;
|
|
1364
|
+
return `auth ${state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.folders.loaded ? `${state.folders.count} folders` : "folders pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | list ${meetingSource}`;
|
|
1364
1365
|
}
|
|
1365
1366
|
//#endregion
|
|
1366
1367
|
//#region src/tui/theme.ts
|
|
@@ -1657,9 +1658,13 @@ var GranolaTuiWorkspace = class {
|
|
|
1657
1658
|
focused = false;
|
|
1658
1659
|
#maxMeetings;
|
|
1659
1660
|
#appState;
|
|
1661
|
+
#activePane = "meetings";
|
|
1660
1662
|
#detailError = "";
|
|
1661
1663
|
#detailScroll = 0;
|
|
1662
1664
|
#detailToken = 0;
|
|
1665
|
+
#folderError = "";
|
|
1666
|
+
#folderToken = 0;
|
|
1667
|
+
#folders = [];
|
|
1663
1668
|
#listError = "";
|
|
1664
1669
|
#listToken = 0;
|
|
1665
1670
|
#loadingDetail = false;
|
|
@@ -1667,6 +1672,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1667
1672
|
#meetingSource = "live";
|
|
1668
1673
|
#meetings = [];
|
|
1669
1674
|
#overlay;
|
|
1675
|
+
#selectedFolderId;
|
|
1670
1676
|
#selectedMeeting;
|
|
1671
1677
|
#selectedMeetingId;
|
|
1672
1678
|
#statusMessage = "Loading meetings…";
|
|
@@ -1679,11 +1685,13 @@ var GranolaTuiWorkspace = class {
|
|
|
1679
1685
|
this.options = options;
|
|
1680
1686
|
this.#appState = app.getState();
|
|
1681
1687
|
this.#maxMeetings = options.maxMeetings ?? 200;
|
|
1688
|
+
this.#selectedFolderId = this.#appState.ui.selectedFolderId;
|
|
1682
1689
|
}
|
|
1683
1690
|
async initialise() {
|
|
1684
1691
|
this.#unsubscribe = this.app.subscribe((event) => {
|
|
1685
1692
|
this.handleAppUpdate(event);
|
|
1686
1693
|
});
|
|
1694
|
+
await this.loadFolders({ setStatus: false });
|
|
1687
1695
|
await this.loadMeetings({
|
|
1688
1696
|
preferredMeetingId: this.options.initialMeetingId,
|
|
1689
1697
|
setStatus: true
|
|
@@ -1699,7 +1707,12 @@ var GranolaTuiWorkspace = class {
|
|
|
1699
1707
|
handleAppUpdate(event) {
|
|
1700
1708
|
const previousDocumentsLoadedAt = this.#appState.documents.loadedAt;
|
|
1701
1709
|
this.#appState = event.state;
|
|
1702
|
-
|
|
1710
|
+
this.#selectedFolderId = event.state.ui.selectedFolderId;
|
|
1711
|
+
this.#selectedMeetingId = event.state.ui.selectedMeetingId ?? this.#selectedMeetingId;
|
|
1712
|
+
if (this.#meetingSource === "index" && event.state.documents.loadedAt && event.state.documents.loadedAt !== previousDocumentsLoadedAt && !this.#loadingMeetings) (async () => {
|
|
1713
|
+
await this.loadFolders({ setStatus: false });
|
|
1714
|
+
await this.loadMeetings({ preferredMeetingId: this.#selectedMeetingId });
|
|
1715
|
+
})();
|
|
1703
1716
|
this.tui.requestRender();
|
|
1704
1717
|
}
|
|
1705
1718
|
setStatus(message, tone = "info") {
|
|
@@ -1712,6 +1725,11 @@ var GranolaTuiWorkspace = class {
|
|
|
1712
1725
|
const selectedIndex = this.#selectedMeetingId ? this.#meetings.findIndex((meeting) => meeting.id === this.#selectedMeetingId) : -1;
|
|
1713
1726
|
return selectedIndex >= 0 ? selectedIndex : 0;
|
|
1714
1727
|
}
|
|
1728
|
+
normaliseSelectedFolderIndex() {
|
|
1729
|
+
if (!this.#selectedFolderId) return 0;
|
|
1730
|
+
const selectedIndex = this.#folders.findIndex((folder) => folder.id === this.#selectedFolderId);
|
|
1731
|
+
return selectedIndex >= 0 ? selectedIndex + 1 : 0;
|
|
1732
|
+
}
|
|
1715
1733
|
ensureMeetingVisible(meeting) {
|
|
1716
1734
|
const existingIndex = this.#meetings.findIndex((item) => item.id === meeting.id);
|
|
1717
1735
|
if (existingIndex >= 0) this.#meetings[existingIndex] = meeting;
|
|
@@ -1721,6 +1739,30 @@ var GranolaTuiWorkspace = class {
|
|
|
1721
1739
|
return left.title.localeCompare(right.title);
|
|
1722
1740
|
});
|
|
1723
1741
|
}
|
|
1742
|
+
async loadFolders(options = {}) {
|
|
1743
|
+
const token = ++this.#folderToken;
|
|
1744
|
+
this.#folderError = "";
|
|
1745
|
+
if (options.setStatus) this.setStatus(options.forceRefresh ? "Refreshing folders…" : "Loading folders…");
|
|
1746
|
+
try {
|
|
1747
|
+
const result = await this.app.listFolders({
|
|
1748
|
+
forceRefresh: options.forceRefresh,
|
|
1749
|
+
limit: 100
|
|
1750
|
+
});
|
|
1751
|
+
if (token !== this.#folderToken) return;
|
|
1752
|
+
this.#folders = result.folders;
|
|
1753
|
+
if (this.#selectedFolderId && !this.#folders.some((folder) => folder.id === this.#selectedFolderId)) this.#selectedFolderId = void 0;
|
|
1754
|
+
this.#folderError = "";
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
if (token !== this.#folderToken) return;
|
|
1757
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1758
|
+
this.#folderError = message;
|
|
1759
|
+
this.#folders = [];
|
|
1760
|
+
this.#selectedFolderId = void 0;
|
|
1761
|
+
this.setStatus(message, "error");
|
|
1762
|
+
} finally {
|
|
1763
|
+
if (token === this.#folderToken) this.tui.requestRender();
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1724
1766
|
async loadMeetings(options = {}) {
|
|
1725
1767
|
const token = ++this.#listToken;
|
|
1726
1768
|
this.#loadingMeetings = true;
|
|
@@ -1728,6 +1770,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1728
1770
|
if (options.setStatus !== false) this.setStatus(options.forceRefresh ? "Refreshing meetings…" : "Loading meetings…");
|
|
1729
1771
|
try {
|
|
1730
1772
|
const result = await this.app.listMeetings({
|
|
1773
|
+
folderId: this.#selectedFolderId,
|
|
1731
1774
|
forceRefresh: options.forceRefresh,
|
|
1732
1775
|
limit: this.#maxMeetings,
|
|
1733
1776
|
preferIndex: true
|
|
@@ -1736,8 +1779,13 @@ var GranolaTuiWorkspace = class {
|
|
|
1736
1779
|
this.#meetings = result.meetings;
|
|
1737
1780
|
this.#meetingSource = result.source;
|
|
1738
1781
|
this.#selectedMeetingId = options.preferredMeetingId && this.#meetings.some((meeting) => meeting.id === options.preferredMeetingId) ? options.preferredMeetingId : this.#selectedMeetingId && this.#meetings.some((meeting) => meeting.id === this.#selectedMeetingId) ? this.#selectedMeetingId : this.#meetings[0]?.id;
|
|
1782
|
+
if (!this.#selectedMeetingId) {
|
|
1783
|
+
this.#selectedMeeting = void 0;
|
|
1784
|
+
this.#detailError = "";
|
|
1785
|
+
this.#detailScroll = 0;
|
|
1786
|
+
}
|
|
1739
1787
|
this.#listError = "";
|
|
1740
|
-
this.setStatus(result.source === "index" ? "Loaded meetings from the local index" : "Connected to Granola");
|
|
1788
|
+
this.setStatus(result.source === "index" ? "Loaded meetings from the local index" : this.#selectedFolderId ? "Connected to Granola (folder scope)" : "Connected to Granola");
|
|
1741
1789
|
} catch (error) {
|
|
1742
1790
|
if (token !== this.#listToken) return;
|
|
1743
1791
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1780,6 +1828,10 @@ var GranolaTuiWorkspace = class {
|
|
|
1780
1828
|
}
|
|
1781
1829
|
async refresh(forceRefresh) {
|
|
1782
1830
|
try {
|
|
1831
|
+
await this.loadFolders({
|
|
1832
|
+
forceRefresh,
|
|
1833
|
+
setStatus: false
|
|
1834
|
+
});
|
|
1783
1835
|
await this.loadMeetings({
|
|
1784
1836
|
forceRefresh,
|
|
1785
1837
|
preferredMeetingId: this.#selectedMeetingId
|
|
@@ -1787,7 +1839,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1787
1839
|
if (this.#selectedMeetingId) await this.loadMeeting(this.#selectedMeetingId, { ensureMeetingVisible: true });
|
|
1788
1840
|
} catch {}
|
|
1789
1841
|
}
|
|
1790
|
-
async
|
|
1842
|
+
async moveMeetingSelection(delta) {
|
|
1791
1843
|
if (this.#meetings.length === 0) return;
|
|
1792
1844
|
const currentIndex = this.normaliseSelectedIndex();
|
|
1793
1845
|
const nextIndex = Math.max(0, Math.min(this.#meetings.length - 1, currentIndex + delta));
|
|
@@ -1795,6 +1847,33 @@ var GranolaTuiWorkspace = class {
|
|
|
1795
1847
|
if (!nextMeeting || nextMeeting.id === this.#selectedMeetingId) return;
|
|
1796
1848
|
await this.loadMeeting(nextMeeting.id);
|
|
1797
1849
|
}
|
|
1850
|
+
async moveFolderSelection(delta) {
|
|
1851
|
+
const total = this.#folders.length + 1;
|
|
1852
|
+
const currentIndex = this.normaliseSelectedFolderIndex();
|
|
1853
|
+
const nextIndex = Math.max(0, Math.min(total - 1, currentIndex + delta));
|
|
1854
|
+
const nextFolderId = nextIndex === 0 ? void 0 : this.#folders[nextIndex - 1]?.id;
|
|
1855
|
+
if (nextFolderId === this.#selectedFolderId) return;
|
|
1856
|
+
this.#selectedFolderId = nextFolderId;
|
|
1857
|
+
this.#selectedMeeting = void 0;
|
|
1858
|
+
this.#detailError = "";
|
|
1859
|
+
this.#detailScroll = 0;
|
|
1860
|
+
await this.loadMeetings({
|
|
1861
|
+
preferredMeetingId: this.#selectedMeetingId,
|
|
1862
|
+
setStatus: false
|
|
1863
|
+
});
|
|
1864
|
+
if (this.#selectedMeetingId) {
|
|
1865
|
+
await this.loadMeeting(this.#selectedMeetingId, { ensureMeetingVisible: true });
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
this.tui.requestRender();
|
|
1869
|
+
}
|
|
1870
|
+
async moveSelection(delta) {
|
|
1871
|
+
if (this.#activePane === "folders") {
|
|
1872
|
+
await this.moveFolderSelection(delta);
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
await this.moveMeetingSelection(delta);
|
|
1876
|
+
}
|
|
1798
1877
|
currentDetailBody(width) {
|
|
1799
1878
|
if (this.#detailError) return wrapBlock(this.#detailError, width);
|
|
1800
1879
|
if (this.#loadingDetail && !this.#selectedMeeting) return wrapBlock("Loading meeting details…", width);
|
|
@@ -1832,6 +1911,10 @@ var GranolaTuiWorkspace = class {
|
|
|
1832
1911
|
async reloadAfterAuthChange() {
|
|
1833
1912
|
const preferredMeetingId = this.#selectedMeeting?.document.id ?? this.#selectedMeetingId;
|
|
1834
1913
|
try {
|
|
1914
|
+
await this.loadFolders({
|
|
1915
|
+
forceRefresh: true,
|
|
1916
|
+
setStatus: false
|
|
1917
|
+
});
|
|
1835
1918
|
await this.loadMeetings({
|
|
1836
1919
|
forceRefresh: true,
|
|
1837
1920
|
preferredMeetingId,
|
|
@@ -1956,6 +2039,21 @@ var GranolaTuiWorkspace = class {
|
|
|
1956
2039
|
this.openAuthPanel();
|
|
1957
2040
|
return;
|
|
1958
2041
|
}
|
|
2042
|
+
if (matchesKey(data, "tab")) {
|
|
2043
|
+
this.#activePane = this.#activePane === "folders" ? "meetings" : "folders";
|
|
2044
|
+
this.tui.requestRender();
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
if (matchesKey(data, "left") || matchesKey(data, "h")) {
|
|
2048
|
+
this.#activePane = "folders";
|
|
2049
|
+
this.tui.requestRender();
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
if (matchesKey(data, "right") || matchesKey(data, "l")) {
|
|
2053
|
+
this.#activePane = "meetings";
|
|
2054
|
+
this.tui.requestRender();
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
1959
2057
|
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
1960
2058
|
this.moveSelection(-1);
|
|
1961
2059
|
return;
|
|
@@ -2021,22 +2119,53 @@ var GranolaTuiWorkspace = class {
|
|
|
2021
2119
|
renderListPane(width, height) {
|
|
2022
2120
|
const lines = [];
|
|
2023
2121
|
const innerWidth = Math.max(1, width - 2);
|
|
2024
|
-
const
|
|
2025
|
-
|
|
2122
|
+
const folderEntries = [{
|
|
2123
|
+
id: void 0,
|
|
2124
|
+
label: "All meetings",
|
|
2125
|
+
meta: this.#folders.length > 0 ? `${this.#folders.length} folders` : "global scope"
|
|
2126
|
+
}, ...this.#folders.map((folder) => ({
|
|
2127
|
+
id: folder.id,
|
|
2128
|
+
label: `${folder.isFavourite ? "★ " : ""}${folder.name || folder.id}`,
|
|
2129
|
+
meta: `${folder.documentCount} meetings`
|
|
2130
|
+
}))];
|
|
2131
|
+
const availableRows = Math.max(2, height - 3);
|
|
2132
|
+
const folderWindowSize = Math.min(Math.max(3, Math.min(8, Math.floor(availableRows * .35))), Math.max(1, availableRows - 1));
|
|
2133
|
+
const meetingWindowSize = Math.max(1, availableRows - folderWindowSize);
|
|
2134
|
+
const folderHeader = `${this.#activePane === "folders" ? granolaTuiTheme.accent("Folders") : granolaTuiTheme.strong("Folders")} ${granolaTuiTheme.dim(`(${this.#folders.length})`)}`;
|
|
2135
|
+
lines.push(padLine(folderHeader, innerWidth));
|
|
2136
|
+
if (this.#folderError) {
|
|
2137
|
+
lines.push(...wrapBlock(granolaTuiTheme.error(this.#folderError), innerWidth).slice(0, folderWindowSize));
|
|
2138
|
+
while (lines.length < 1 + folderWindowSize) lines.push(" ".repeat(innerWidth));
|
|
2139
|
+
} else {
|
|
2140
|
+
const selectedFolderIndex = this.normaliseSelectedFolderIndex();
|
|
2141
|
+
const folderStartIndex = Math.max(0, Math.min(selectedFolderIndex - Math.floor(folderWindowSize / 2), folderEntries.length - folderWindowSize));
|
|
2142
|
+
const visibleFolders = folderEntries.slice(folderStartIndex, folderStartIndex + folderWindowSize);
|
|
2143
|
+
for (const [offset, folder] of visibleFolders.entries()) {
|
|
2144
|
+
const selected = folderStartIndex + offset === selectedFolderIndex;
|
|
2145
|
+
const prefix = selected ? "> " : " ";
|
|
2146
|
+
const maxLabelWidth = Math.max(6, innerWidth - visibleWidth(prefix) - visibleWidth(folder.meta) - 1);
|
|
2147
|
+
const labelBlock = `${prefix}${truncateToWidth(folder.label, maxLabelWidth, "")}`;
|
|
2148
|
+
const line = `${labelBlock}${" ".repeat(Math.max(1, innerWidth - visibleWidth(labelBlock) - visibleWidth(folder.meta)))}${granolaTuiTheme.dim(folder.meta)}`;
|
|
2149
|
+
lines.push(selected ? padLine(granolaTuiTheme.selected(line), innerWidth) : padLine(line, innerWidth));
|
|
2150
|
+
}
|
|
2151
|
+
while (lines.length < 1 + folderWindowSize) lines.push(" ".repeat(innerWidth));
|
|
2152
|
+
}
|
|
2153
|
+
lines.push(padLine(granolaTuiTheme.dim(""), innerWidth));
|
|
2154
|
+
const meetingsHeader = `${this.#activePane === "meetings" ? granolaTuiTheme.accent("Meetings") : granolaTuiTheme.strong("Meetings")} ${granolaTuiTheme.dim(`(${this.#meetings.length})`)}`;
|
|
2155
|
+
lines.push(padLine(meetingsHeader, innerWidth));
|
|
2026
2156
|
if (this.#listError) {
|
|
2027
|
-
lines.push(...wrapBlock(granolaTuiTheme.error(this.#listError), innerWidth).slice(0,
|
|
2157
|
+
lines.push(...wrapBlock(granolaTuiTheme.error(this.#listError), innerWidth).slice(0, meetingWindowSize));
|
|
2028
2158
|
while (lines.length < height) lines.push(" ".repeat(innerWidth));
|
|
2029
2159
|
return lines;
|
|
2030
2160
|
}
|
|
2031
2161
|
if (this.#meetings.length === 0) {
|
|
2032
|
-
lines.push(...wrapBlock("No meetings available yet.", innerWidth).slice(0,
|
|
2162
|
+
lines.push(...wrapBlock("No meetings available yet.", innerWidth).slice(0, meetingWindowSize));
|
|
2033
2163
|
while (lines.length < height) lines.push(" ".repeat(innerWidth));
|
|
2034
2164
|
return lines;
|
|
2035
2165
|
}
|
|
2036
2166
|
const selectedIndex = this.normaliseSelectedIndex();
|
|
2037
|
-
const
|
|
2038
|
-
const
|
|
2039
|
-
const visibleMeetings = this.#meetings.slice(startIndex, startIndex + windowSize);
|
|
2167
|
+
const startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(meetingWindowSize / 2), this.#meetings.length - meetingWindowSize));
|
|
2168
|
+
const visibleMeetings = this.#meetings.slice(startIndex, startIndex + meetingWindowSize);
|
|
2040
2169
|
for (const [offset, meeting] of visibleMeetings.entries()) {
|
|
2041
2170
|
const selected = startIndex + offset === selectedIndex;
|
|
2042
2171
|
const dateLabel = meeting.updatedAt.slice(0, 10);
|
|
@@ -2094,7 +2223,7 @@ var GranolaTuiWorkspace = class {
|
|
|
2094
2223
|
const bodyLines = [];
|
|
2095
2224
|
for (let index = 0; index < bodyHeight; index += 1) bodyLines.push(`${padLine(listLines[index] ?? "", listWidth)} | ${padLine(detailLines[index] ?? "", detailWidth)}`);
|
|
2096
2225
|
const footerStatus = padLine(toneText(this.#statusTone, this.#statusMessage), width);
|
|
2097
|
-
const footerHints = padLine(granolaTuiTheme.dim("/ quick open a auth r refresh 1-4 tabs PgUp/PgDn scroll q quit"), width);
|
|
2226
|
+
const footerHints = padLine(granolaTuiTheme.dim("h/l or Tab pane j/k move / quick open a auth r refresh 1-4 tabs PgUp/PgDn scroll q quit"), width);
|
|
2098
2227
|
return [
|
|
2099
2228
|
headerTitle,
|
|
2100
2229
|
headerSummary,
|