experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(extensions): move tag and tagset commands to tags extension

+6 -372
+2 -6
extensions/cmd/commands/index.js
··· 3 3 * Note: groups commands are now provided by the groups extension 4 4 * Note: sync command is now provided by the sync extension 5 5 * Note: open/modal commands are now provided by the page extension 6 + * Note: tag, tags, untag, tagset commands are now provided by the tags extension 6 7 */ 7 8 import debugCommand from './debug.js'; 8 9 import noteModule from './note.js'; 9 - import tagsetModule from './tagset.js'; 10 10 import urlModule from './url.js'; 11 11 import historyModule from './history.js'; 12 - import tagModule from './tag.js'; 13 12 14 13 // Chaining commands - for command composition pipelines 15 14 import listsCommand from './lists.js'; 16 15 // csv and save commands moved to files extension 17 16 18 - console.log('[cmd:commands/index] tagModule.commands:', tagModule.commands?.map(c => c.name)); 19 - 20 17 // Source commands (commented out as they need browser extension APIs) 21 18 // These modules contain command sources that dynamically generate commands 22 19 import bookmarkletsSource from './bookmarklets.js'; ··· 33 30 // Note: groups commands are dynamically registered by the groups extension 34 31 // Note: sync command is dynamically registered by the sync extension 35 32 // Note: open/modal commands are dynamically registered by the page extension 33 + // Note: tag, tags, untag, tagset commands are dynamically registered by the tags extension 36 34 const activeCommands = [ 37 35 debugCommand, 38 36 ...noteModule.commands, 39 - ...tagsetModule.commands, 40 37 ...urlModule.commands, 41 38 ...historyModule.commands, 42 - ...tagModule.commands, 43 39 44 40 // Chaining commands 45 41 listsCommand
-287
extensions/cmd/commands/tag.js
··· 1 - /** 2 - * Tag command - add tags to the URL of the active window 3 - * Tags are saved using the items table with item_tags join table 4 - * 5 - * Usage: 6 - * tag foo - add tag "foo" to active window's URL 7 - * tag foo bar - add multiple tags 8 - * tag -r foo - remove tag "foo" from active window 9 - * tag - show tags for active window 10 - */ 11 - import api from 'peek://app/api.js'; 12 - 13 - /** 14 - * Get the most recently focused non-internal window 15 - */ 16 - const getActiveWindow = async () => { 17 - const result = await api.window.list({ includeInternal: false }); 18 - if (!result.success || !result.windows.length) { 19 - return null; 20 - } 21 - // Return the first non-internal window 22 - return result.windows[0]; 23 - }; 24 - 25 - /** 26 - * Find item record by URL content 27 - */ 28 - const findItemByUrl = async (url) => { 29 - const result = await api.datastore.queryItems({ type: 'url' }); 30 - if (!result.success) return null; 31 - 32 - return result.data.find(item => item.content === url) || null; 33 - }; 34 - 35 - /** 36 - * Add tags to an item using the join table 37 - */ 38 - const addTagsToItem = async (itemId, tagNames) => { 39 - const results = []; 40 - 41 - for (const tagName of tagNames) { 42 - // Get or create the tag 43 - api.log('[tag] Getting/creating tag:', tagName); 44 - const tagResult = await api.datastore.getOrCreateTag(tagName); 45 - api.log('[tag] getOrCreateTag result:', JSON.stringify(tagResult)); 46 - if (!tagResult.success) { 47 - console.error('Failed to get/create tag:', tagName, tagResult.error); 48 - continue; 49 - } 50 - 51 - const tag = tagResult.data.tag; 52 - api.log('[tag] Tag id:', tag.id, 'name:', tag.name); 53 - 54 - // Link tag to item 55 - api.log('[tag] Linking tag', tag.id, 'to item', itemId); 56 - const linkResult = await api.datastore.tagItem(itemId, tag.id); 57 - api.log('[tag] tagItem result:', JSON.stringify(linkResult)); 58 - if (!linkResult.success) { 59 - console.error('Failed to link tag:', tagName, linkResult.error); 60 - continue; 61 - } 62 - 63 - results.push({ 64 - tag, 65 - alreadyExists: linkResult.alreadyExists 66 - }); 67 - } 68 - 69 - return results; 70 - }; 71 - 72 - /** 73 - * Remove tags from an item 74 - */ 75 - const removeTagsFromItem = async (itemId, tagNames) => { 76 - const results = []; 77 - 78 - // Get current tags for item 79 - const tagsResult = await api.datastore.getItemTags(itemId); 80 - if (!tagsResult.success) { 81 - return results; 82 - } 83 - 84 - for (const tagName of tagNames) { 85 - // Find the tag by name 86 - const tag = tagsResult.data.find(t => t.name.toLowerCase() === tagName.toLowerCase()); 87 - if (!tag) { 88 - console.log('Tag not found on item:', tagName); 89 - continue; 90 - } 91 - 92 - // Unlink tag from item 93 - const unlinkResult = await api.datastore.untagItem(itemId, tag.id); 94 - results.push({ 95 - tag, 96 - removed: unlinkResult.success 97 - }); 98 - } 99 - 100 - return results; 101 - }; 102 - 103 - /** 104 - * Get tags for an item 105 - */ 106 - const getTagsForItem = async (itemId) => { 107 - const result = await api.datastore.getItemTags(itemId); 108 - if (!result.success) return []; 109 - return result.data; 110 - }; 111 - 112 - // Commands 113 - const commands = [ 114 - { 115 - name: 'tag', 116 - description: 'Add tags to the active window URL', 117 - async execute(ctx) { 118 - // Get active window 119 - api.log('tag command execute, ctx:', ctx); 120 - const activeWindow = await getActiveWindow(); 121 - api.log('tag command: activeWindow =', activeWindow); 122 - if (!activeWindow) { 123 - api.log('No active window found'); 124 - return { success: false, error: 'No active window' }; 125 - } 126 - 127 - const url = activeWindow.url; 128 - api.log('Tagging URL:', url); 129 - 130 - // Find item in datastore 131 - let item = await findItemByUrl(url); 132 - 133 - // If no item exists, create one 134 - if (!item) { 135 - api.log('[tag] Creating new item for URL:', url); 136 - const addResult = await api.datastore.addItem('url', { 137 - content: url, 138 - metadata: JSON.stringify({ title: activeWindow.title || '' }) 139 - }); 140 - api.log('[tag] addItem result:', JSON.stringify(addResult)); 141 - if (!addResult.success) { 142 - console.error('Failed to create item:', addResult.error); 143 - return { success: false, error: 'Failed to create item' }; 144 - } 145 - item = { id: addResult.data.id }; 146 - api.log('[tag] Created item with id:', item.id); 147 - } else { 148 - api.log('[tag] Found existing item:', item.id, 'for URL:', url); 149 - } 150 - 151 - // No args - show current tags 152 - if (!ctx.search) { 153 - const tags = await getTagsForItem(item.id); 154 - if (tags.length === 0) { 155 - console.log('No tags for:', url); 156 - } else { 157 - console.log('Tags for', url + ':'); 158 - tags.forEach(t => console.log(' -', t.name, `(frecency: ${t.frecencyScore?.toFixed(1) || 0})`)); 159 - } 160 - return { success: true, tags }; 161 - } 162 - 163 - // Parse args 164 - // If comma present, split on comma; otherwise split on spaces 165 - const input = ctx.search.trim(); 166 - const hasComma = input.includes(','); 167 - let args; 168 - if (hasComma) { 169 - args = input.split(',').map(s => s.trim()).filter(s => s.length > 0); 170 - } else { 171 - args = input.split(/\s+/); 172 - } 173 - const removeMode = args[0] === '-r'; 174 - const tagsToProcess = removeMode ? args.slice(1) : args; 175 - 176 - if (tagsToProcess.length === 0) { 177 - console.log('No tags specified'); 178 - return { success: false, error: 'No tags specified' }; 179 - } 180 - 181 - // Add or remove tags 182 - if (removeMode) { 183 - const results = await removeTagsFromItem(item.id, tagsToProcess); 184 - const removed = results.filter(r => r.removed).map(r => r.tag.name); 185 - if (removed.length > 0) { 186 - console.log('Removed tags:', removed.join(', '), 'from', url); 187 - } 188 - return { success: true, removed }; 189 - } else { 190 - api.log('Adding tags to item:', item.id, 'tags:', tagsToProcess); 191 - const results = await addTagsToItem(item.id, tagsToProcess); 192 - api.log('addTagsToItem results:', results); 193 - const added = results.filter(r => !r.alreadyExists).map(r => r.tag.name); 194 - const existing = results.filter(r => r.alreadyExists).map(r => r.tag.name); 195 - if (added.length > 0) { 196 - console.log('Added tags:', added.join(', '), 'to', url); 197 - } 198 - if (existing.length > 0) { 199 - console.log('Already tagged:', existing.join(', ')); 200 - } 201 - return { success: true, added, existing }; 202 - } 203 - } 204 - }, 205 - { 206 - name: 'tags', 207 - description: 'Show tags for the active window URL (or all tags by frecency)', 208 - async execute(ctx) { 209 - // If search term provided, show all tags matching 210 - if (ctx.search) { 211 - const result = await api.datastore.getTagsByFrecency(); 212 - if (!result.success) { 213 - console.log('Failed to get tags'); 214 - return { success: false }; 215 - } 216 - 217 - const filter = ctx.search.toLowerCase(); 218 - const filtered = result.data.filter(t => t.name.toLowerCase().includes(filter)); 219 - 220 - if (filtered.length === 0) { 221 - console.log('No tags matching:', ctx.search); 222 - } else { 223 - console.log('Tags matching "' + ctx.search + '":'); 224 - filtered.forEach(t => { 225 - console.log(' -', t.name, `(used ${t.frequency}x, frecency: ${t.frecencyScore?.toFixed(1) || 0})`); 226 - }); 227 - } 228 - return { success: true, tags: filtered }; 229 - } 230 - 231 - // No args - show tags for active window 232 - const activeWindow = await getActiveWindow(); 233 - if (!activeWindow) { 234 - // No active window - show all tags by frecency 235 - const result = await api.datastore.getTagsByFrecency(); 236 - if (!result.success) { 237 - console.log('Failed to get tags'); 238 - return { success: false }; 239 - } 240 - 241 - if (result.data.length === 0) { 242 - console.log('No tags yet'); 243 - } else { 244 - console.log('All tags (by frecency):'); 245 - result.data.slice(0, 20).forEach(t => { 246 - console.log(' -', t.name, `(used ${t.frequency}x, frecency: ${t.frecencyScore?.toFixed(1) || 0})`); 247 - }); 248 - } 249 - return { success: true, tags: result.data }; 250 - } 251 - 252 - const item = await findItemByUrl(activeWindow.url); 253 - if (!item) { 254 - console.log('No tags for:', activeWindow.url); 255 - return { success: true, tags: [] }; 256 - } 257 - 258 - const tags = await getTagsForItem(item.id); 259 - 260 - if (tags.length === 0) { 261 - console.log('No tags for:', activeWindow.url); 262 - } else { 263 - console.log('Tags for', activeWindow.url + ':'); 264 - tags.forEach(t => console.log(' -', t.name)); 265 - } 266 - 267 - return { success: true, tags }; 268 - } 269 - }, 270 - { 271 - name: 'untag', 272 - description: 'Remove tags from the active window URL', 273 - async execute(ctx) { 274 - if (!ctx.search) { 275 - console.log('Usage: untag <tag1> [tag2] ...'); 276 - return { success: false, error: 'No tags specified' }; 277 - } 278 - 279 - // Delegate to tag -r 280 - return commands[0].execute({ search: '-r ' + ctx.search }); 281 - } 282 - } 283 - ]; 284 - 285 - export default { 286 - commands 287 - };
-79
extensions/cmd/commands/tagset.js
··· 1 - /** 2 - * Tagset command - creates tagset items in the datastore 3 - * Tagsets are items of type='tagset' that exist solely to hold a combination of tags 4 - * Useful for creating quick reference collections or categorization markers 5 - */ 6 - import api from 'peek://app/api.js'; 7 - 8 - /** 9 - * Create a new tagset with the specified tags 10 - * @param {string} tagsString - Comma-separated list of tag names 11 - * @returns {Promise<string>} The ID of the created tagset 12 - */ 13 - const createTagset = async (tagsString) => { 14 - // Parse tags from comma-separated string 15 - const tagNames = tagsString 16 - .split(',') 17 - .map(t => t.trim()) 18 - .filter(t => t.length > 0); 19 - 20 - if (tagNames.length === 0) { 21 - throw new Error('No valid tags provided'); 22 - } 23 - 24 - // Create the tagset item 25 - const result = await api.datastore.addItem('tagset', { 26 - content: tagNames.join(', ') 27 - }); 28 - 29 - if (!result.success) { 30 - throw new Error(result.error || 'Failed to create tagset'); 31 - } 32 - 33 - const itemId = result.data.id; 34 - 35 - // Add each tag to the tagset 36 - for (const tagName of tagNames) { 37 - const tagResult = await api.datastore.getOrCreateTag(tagName); 38 - if (tagResult.success) { 39 - await api.datastore.tagItem(itemId, tagResult.data.tag.id); 40 - } 41 - } 42 - 43 - // Also add the 'from:cmd' tag to track origin 44 - const fromCmdResult = await api.datastore.getOrCreateTag('from:cmd'); 45 - if (fromCmdResult.success) { 46 - await api.datastore.tagItem(itemId, fromCmdResult.data.tag.id); 47 - } 48 - 49 - return { id: itemId, tags: tagNames }; 50 - }; 51 - 52 - // Commands 53 - const commands = [ 54 - { 55 - name: 'tagset', 56 - description: 'Create a tagset with specified tags', 57 - async execute(ctx) { 58 - if (ctx.search) { 59 - try { 60 - const { id, tags } = await createTagset(ctx.search); 61 - console.log(`Tagset created with ID: ${id}`); 62 - console.log(`Tags: ${tags.join(', ')}`); 63 - api.publish('editor:changed', { action: 'add', itemId: id }, api.scopes.GLOBAL); 64 - return { success: true, message: `Tagset created with tags: ${tags.join(', ')}` }; 65 - } catch (error) { 66 - console.error('Failed to create tagset:', error); 67 - return { success: false, message: error.message }; 68 - } 69 - } else { 70 - api.publish('editor:add', { type: 'tagset' }, api.scopes.GLOBAL); 71 - return { success: true, message: 'Opening editor' }; 72 - } 73 - } 74 - } 75 - ]; 76 - 77 - export default { 78 - commands 79 - };
+4
extensions/tags/background.js
··· 489 489 if (hasPeekAPI) { 490 490 api.commands.unregister('open tags'); 491 491 api.commands.unregister('list tags'); 492 + api.commands.unregister('tag'); 493 + api.commands.unregister('tags'); 494 + api.commands.unregister('untag'); 495 + api.commands.unregister('tagset'); 492 496 api.shortcuts.unregister('Option+t'); 493 497 } 494 498 }