mulmoclaude 0.6.4 → 0.7.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.
Files changed (302) hide show
  1. package/bin/mulmoclaude.js +1 -1
  2. package/client/assets/{html2canvas-CDGcmOD3-XVrO-eyz.js → html2canvas-CDGcmOD3-CKJ6vKPo.js} +1 -1
  3. package/client/assets/{index-zZIqEbNX.js → index-BG_JJcKI.js} +193 -197
  4. package/client/assets/index-DCoo3kpR.css +2 -0
  5. package/client/assets/{index.es-DqtpmBm8-DHT6q10o.js → index.es-DqtpmBm8-DFXjJgCa.js} +1 -1
  6. package/client/assets/lib-Dpph7PBN.js +114 -0
  7. package/client/assets/marp-cCGismx0.js +3452 -0
  8. package/client/assets/schemas-DuYzyHQc.js +64 -0
  9. package/client/index.html +5 -4
  10. package/package.json +7 -5
  11. package/server/agent/backend/claude-code.ts +44 -10
  12. package/server/agent/backend/fake-echo.ts +5 -1
  13. package/server/agent/config.ts +49 -12
  14. package/server/agent/mcp-server.ts +13 -2
  15. package/server/agent/mcp-tools/handlePermission.ts +115 -0
  16. package/server/agent/mcp-tools/index.ts +2 -1
  17. package/server/agent/mcp-tools/x.ts +18 -2
  18. package/server/agent/prompt.ts +3 -45
  19. package/server/api/csrfGuard.ts +122 -32
  20. package/server/api/routes/collections.ts +320 -0
  21. package/server/api/routes/config.ts +45 -0
  22. package/server/api/routes/feeds.ts +70 -0
  23. package/server/api/routes/files.ts +167 -0
  24. package/server/api/routes/marp-themes.ts +15 -0
  25. package/server/api/routes/mulmo-script.ts +139 -0
  26. package/server/api/routes/pdf.ts +108 -0
  27. package/server/api/routes/plugins.ts +10 -0
  28. package/server/api/routes/shortcuts.ts +49 -0
  29. package/server/api/routes/wiki.ts +35 -0
  30. package/server/build/dispatcher.mjs +40 -22
  31. package/server/events/notifications.ts +14 -9
  32. package/server/index.ts +49 -14
  33. package/server/plugins/preset-list.ts +18 -10
  34. package/server/plugins/preset-loader.ts +11 -4
  35. package/server/prompts/index.ts +1 -3
  36. package/server/prompts/system/system.md +7 -2
  37. package/server/system/env.ts +14 -0
  38. package/server/utils/clientDir.ts +7 -0
  39. package/server/utils/files/index.ts +0 -2
  40. package/server/utils/files/shortcuts-io.ts +63 -0
  41. package/server/utils/slug.ts +3 -4
  42. package/server/workspace/billing-migration.ts +69 -0
  43. package/server/workspace/collections/delete.ts +186 -0
  44. package/server/workspace/collections/discovery.ts +730 -0
  45. package/server/workspace/collections/index.ts +23 -0
  46. package/server/workspace/collections/io.ts +287 -0
  47. package/server/workspace/collections/notifications.ts +404 -0
  48. package/server/workspace/collections/paths.ts +125 -0
  49. package/server/workspace/collections/spawn.ts +213 -0
  50. package/server/workspace/collections/templatePath.ts +36 -0
  51. package/server/workspace/collections/types.ts +334 -0
  52. package/server/workspace/collections/watcher.ts +398 -0
  53. package/server/workspace/feeds/engine.ts +135 -0
  54. package/server/workspace/feeds/fetch/httpClient.ts +127 -0
  55. package/server/workspace/feeds/fetch/rssParser.ts +117 -0
  56. package/server/workspace/feeds/index.ts +8 -0
  57. package/server/workspace/feeds/ingestTypes.ts +66 -0
  58. package/server/workspace/feeds/pathResolver.ts +74 -0
  59. package/server/workspace/feeds/paths.ts +30 -0
  60. package/server/workspace/feeds/projectItem.ts +92 -0
  61. package/server/workspace/feeds/registry.ts +43 -0
  62. package/server/workspace/feeds/retrievers/httpJson.ts +19 -0
  63. package/server/workspace/feeds/retrievers/index.ts +27 -0
  64. package/server/workspace/feeds/retrievers/registerAll.ts +5 -0
  65. package/server/workspace/feeds/retrievers/rss.ts +24 -0
  66. package/server/workspace/feeds/state.ts +55 -0
  67. package/server/workspace/helps/billing-clients-worklog.md +215 -0
  68. package/server/workspace/helps/billing-invoice.md +457 -0
  69. package/server/workspace/helps/collection-skills.md +664 -0
  70. package/server/workspace/helps/feeds.md +110 -0
  71. package/server/workspace/helps/index.md +9 -3
  72. package/server/workspace/helps/portfolio-tracker.md +211 -0
  73. package/server/workspace/helps/presentation-deck.md +828 -0
  74. package/server/workspace/helps/todo-collection.md +140 -0
  75. package/server/workspace/helps/vocabulary.md +106 -0
  76. package/server/workspace/hooks/handlers/skillBridge.ts +101 -40
  77. package/server/workspace/marp-themes.ts +46 -0
  78. package/server/workspace/paths.ts +46 -11
  79. package/server/workspace/skills-preset/mc-manage-skills/SKILL.md +13 -0
  80. package/server/workspace/skills-preset/mc-wiki-deep-lint/SKILL.md +108 -0
  81. package/server/workspace/skills-preset/mc-wiki-health-check/SKILL.md +61 -0
  82. package/server/workspace/skills-preset/mc-wiki-ingest/SKILL.md +182 -0
  83. package/server/workspace/skills-preset/mc-wiki-promote/SKILL.md +175 -0
  84. package/server/workspace/skills-preset.ts +376 -2
  85. package/server/workspace/wiki-pages/io.ts +34 -2
  86. package/server/workspace/workspace.ts +20 -1
  87. package/src/App.vue +70 -41
  88. package/src/components/BackendOfflineBanner.vue +56 -0
  89. package/src/components/ChatInput.vue +72 -6
  90. package/src/components/CollectionCalendarView.vue +243 -0
  91. package/src/components/CollectionDashboardView.vue +181 -0
  92. package/src/components/CollectionDayView.vue +308 -0
  93. package/src/components/CollectionEmbedView.vue +69 -0
  94. package/src/components/CollectionKanbanView.vue +196 -0
  95. package/src/components/CollectionRecordModal.vue +93 -0
  96. package/src/components/CollectionRecordPanel.vue +567 -0
  97. package/src/components/CollectionView.vue +1748 -0
  98. package/src/components/CollectionsIndexView.vue +152 -0
  99. package/src/components/ConfirmModal.vue +344 -0
  100. package/src/components/FeedsView.vue +225 -0
  101. package/src/components/FileContentRenderer.vue +122 -30
  102. package/src/components/FileTree.vue +218 -2
  103. package/src/components/FileTreePane.vue +2 -0
  104. package/src/components/FilesView.vue +95 -17
  105. package/src/components/PinToggle.vue +52 -0
  106. package/src/components/PluginLauncher.vue +97 -37
  107. package/src/components/RightSidebar.vue +74 -3
  108. package/src/components/RolesView.vue +1 -1
  109. package/src/components/SettingsModal.vue +146 -72
  110. package/src/components/SlashCommandMenu.vue +56 -0
  111. package/src/components/StackView.vue +128 -48
  112. package/src/components/collectionEmbed.ts +29 -0
  113. package/src/components/collectionTypes.ts +177 -0
  114. package/src/composables/collections/useCollectionRendering.ts +350 -0
  115. package/src/composables/useConfirm.ts +70 -0
  116. package/src/composables/useFileTree.ts +35 -3
  117. package/src/composables/useImeAwareEnter.ts +14 -0
  118. package/src/composables/usePdfDownload.ts +6 -1
  119. package/src/composables/useShortcuts.ts +163 -0
  120. package/src/composables/useSlashCommandMenu.ts +138 -0
  121. package/src/config/apiRoutes.ts +46 -13
  122. package/src/config/createFilePolicy.ts +82 -0
  123. package/src/config/roles.ts +46 -47
  124. package/src/config/systemFileDescriptors.ts +0 -30
  125. package/src/config/toolNames.ts +1 -5
  126. package/src/config/workspacePaths.ts +4 -9
  127. package/src/lang/de.ts +154 -221
  128. package/src/lang/en.ts +153 -218
  129. package/src/lang/es.ts +154 -219
  130. package/src/lang/fr.ts +155 -221
  131. package/src/lang/index.ts +55 -0
  132. package/src/lang/ja.ts +153 -219
  133. package/src/lang/ko.ts +153 -218
  134. package/src/lang/pt-BR.ts +154 -219
  135. package/src/lang/zh.ts +152 -218
  136. package/src/lib/vue-i18n.ts +15 -45
  137. package/src/lib/wiki-page/graph.ts +108 -0
  138. package/src/main.ts +2 -5
  139. package/src/plugins/_generated/metas.ts +2 -8
  140. package/src/plugins/_generated/registrations.ts +2 -4
  141. package/src/plugins/_generated/server-bindings.ts +5 -12
  142. package/src/plugins/manageSkills/View.vue +36 -68
  143. package/src/plugins/manageSkills/presetDetection.ts +25 -0
  144. package/src/plugins/markdown/MarpView.vue +301 -0
  145. package/src/plugins/markdown/Preview.vue +26 -3
  146. package/src/plugins/markdown/View.vue +230 -1
  147. package/src/plugins/markdown/definition.ts +21 -1
  148. package/src/plugins/presentCollection/Preview.vue +30 -0
  149. package/src/plugins/presentCollection/View.vue +78 -0
  150. package/src/plugins/presentCollection/definition.ts +30 -0
  151. package/src/plugins/presentCollection/index.ts +25 -0
  152. package/src/plugins/presentCollection/meta.ts +15 -0
  153. package/src/plugins/presentCollection/plugin.ts +39 -0
  154. package/src/plugins/presentCollection/types.ts +13 -0
  155. package/src/plugins/presentForm/View.vue +56 -6
  156. package/src/plugins/presentForm/types.ts +3 -3
  157. package/src/plugins/presentMulmoScript/View.vue +252 -5
  158. package/src/plugins/presentMulmoScript/helpers.ts +18 -0
  159. package/src/plugins/presentMulmoScript/meta.ts +11 -0
  160. package/src/plugins/scheduler/AutomationsView.vue +13 -11
  161. package/src/plugins/scheduler/automationsDefinition.ts +7 -10
  162. package/src/plugins/scheduler/automationsMeta.ts +27 -0
  163. package/src/plugins/scheduler/index.ts +19 -38
  164. package/src/plugins/wiki/View.vue +120 -27
  165. package/src/plugins/wiki/components/WikiGraphView.vue +75 -0
  166. package/src/plugins/wiki/components/WikiPageBody.vue +18 -0
  167. package/src/plugins/wiki/index.ts +6 -0
  168. package/src/plugins/wiki/route.ts +8 -1
  169. package/src/router/index.ts +31 -34
  170. package/src/router/pageRoutes.ts +2 -7
  171. package/src/tools/types.ts +4 -3
  172. package/src/types/notification.ts +5 -9
  173. package/src/types/session.ts +11 -0
  174. package/src/types/shortcuts.ts +37 -0
  175. package/src/utils/agent/eventDispatch.ts +4 -0
  176. package/src/utils/api.ts +47 -1
  177. package/src/utils/canvas/stackGrouping.ts +96 -0
  178. package/src/utils/chat/permalink.ts +20 -0
  179. package/src/utils/collections/actionVisible.ts +55 -0
  180. package/src/utils/collections/calendarGrid.ts +328 -0
  181. package/src/utils/collections/collectionViewMode.ts +42 -0
  182. package/src/utils/collections/derivedFormula.ts +364 -0
  183. package/src/utils/collections/draft.ts +160 -0
  184. package/src/utils/collections/enumColors.ts +130 -0
  185. package/src/utils/collections/itemLabel.ts +42 -0
  186. package/src/utils/confirmDelete.ts +13 -0
  187. package/src/utils/markdown/marpAspect.ts +28 -0
  188. package/src/utils/markdown/marpCustomSize.ts +120 -0
  189. package/src/utils/markdown/marpDetect.ts +15 -0
  190. package/src/utils/markdown/marpTheme.ts +109 -0
  191. package/src/utils/markdown/wikiEmbedHandlers.ts +1 -1
  192. package/src/utils/path/workspaceLinkRouter.ts +126 -9
  193. package/src/utils/session/sessionEntries.ts +1 -0
  194. package/src/utils/session/sessionFactory.ts +1 -0
  195. package/src/utils/session/sessionHelpers.ts +6 -1
  196. package/client/assets/index-CyBr8Mkr.css +0 -2
  197. package/server/api/routes/encore.ts +0 -55
  198. package/server/api/routes/news.ts +0 -133
  199. package/server/api/routes/sources.ts +0 -550
  200. package/server/encore/INVARIANTS.md +0 -272
  201. package/server/encore/boot.ts +0 -39
  202. package/server/encore/closure.ts +0 -36
  203. package/server/encore/cycle.ts +0 -276
  204. package/server/encore/dispatch.ts +0 -103
  205. package/server/encore/handlers/amend.ts +0 -99
  206. package/server/encore/handlers/appendNote.ts +0 -74
  207. package/server/encore/handlers/defineEncore.ts +0 -42
  208. package/server/encore/handlers/listTickets.ts +0 -107
  209. package/server/encore/handlers/markStepDone.ts +0 -41
  210. package/server/encore/handlers/markTargetSkipped.ts +0 -33
  211. package/server/encore/handlers/query.ts +0 -138
  212. package/server/encore/handlers/recordValues.ts +0 -44
  213. package/server/encore/handlers/resolveNotification.ts +0 -121
  214. package/server/encore/handlers/setup.ts +0 -81
  215. package/server/encore/handlers/shared.ts +0 -137
  216. package/server/encore/handlers/snooze.ts +0 -87
  217. package/server/encore/handlers/startObligationChat.ts +0 -64
  218. package/server/encore/handlers/startSetupChat.ts +0 -50
  219. package/server/encore/lock.ts +0 -61
  220. package/server/encore/notifier.ts +0 -123
  221. package/server/encore/obligation.ts +0 -25
  222. package/server/encore/paths.ts +0 -78
  223. package/server/encore/reconcile.ts +0 -661
  224. package/server/encore/tick.ts +0 -191
  225. package/server/encore/yaml-fm.ts +0 -63
  226. package/server/prompts/system/news-concierge.md +0 -24
  227. package/server/prompts/system/sources-context.md +0 -16
  228. package/server/utils/files/encore-io.ts +0 -111
  229. package/server/workspace/helps/encore-dsl.md +0 -482
  230. package/server/workspace/helps/sources.md +0 -42
  231. package/server/workspace/news/reader.ts +0 -247
  232. package/server/workspace/skills-preset/mc-manage-sources/SKILL.md +0 -106
  233. package/server/workspace/sources/arxivDiscovery.ts +0 -182
  234. package/server/workspace/sources/classifier.ts +0 -268
  235. package/server/workspace/sources/fetchers/arxiv.ts +0 -170
  236. package/server/workspace/sources/fetchers/github.ts +0 -106
  237. package/server/workspace/sources/fetchers/githubIssues.ts +0 -210
  238. package/server/workspace/sources/fetchers/githubReleases.ts +0 -186
  239. package/server/workspace/sources/fetchers/index.ts +0 -71
  240. package/server/workspace/sources/fetchers/registerAll.ts +0 -15
  241. package/server/workspace/sources/fetchers/rss.ts +0 -141
  242. package/server/workspace/sources/fetchers/rssParser.ts +0 -295
  243. package/server/workspace/sources/httpFetcher.ts +0 -230
  244. package/server/workspace/sources/interests.ts +0 -120
  245. package/server/workspace/sources/paths.ts +0 -110
  246. package/server/workspace/sources/pipeline/dedup.ts +0 -60
  247. package/server/workspace/sources/pipeline/fetch.ts +0 -182
  248. package/server/workspace/sources/pipeline/index.ts +0 -301
  249. package/server/workspace/sources/pipeline/notify.ts +0 -80
  250. package/server/workspace/sources/pipeline/plan.ts +0 -68
  251. package/server/workspace/sources/pipeline/summarize.ts +0 -189
  252. package/server/workspace/sources/pipeline/write.ts +0 -185
  253. package/server/workspace/sources/rateLimiter.ts +0 -148
  254. package/server/workspace/sources/registry.ts +0 -304
  255. package/server/workspace/sources/robots.ts +0 -271
  256. package/server/workspace/sources/sourceState.ts +0 -142
  257. package/server/workspace/sources/taxonomy.ts +0 -74
  258. package/server/workspace/sources/types.ts +0 -153
  259. package/server/workspace/sources/urls.ts +0 -112
  260. package/src/components/NewsView.vue +0 -267
  261. package/src/components/SourcesManager.vue +0 -915
  262. package/src/components/SourcesView.vue +0 -45
  263. package/src/components/TodoExplorer.vue +0 -423
  264. package/src/components/todo/TodoAddDialog.vue +0 -135
  265. package/src/components/todo/TodoEditDialog.vue +0 -51
  266. package/src/components/todo/TodoEditPanel.vue +0 -117
  267. package/src/components/todo/TodoKanbanView.vue +0 -290
  268. package/src/components/todo/TodoListView.vue +0 -88
  269. package/src/components/todo/TodoTableView.vue +0 -210
  270. package/src/composables/useNewsItems.ts +0 -38
  271. package/src/composables/useNewsReadState.ts +0 -75
  272. package/src/plugins/encore/EncoreDashboard.vue +0 -504
  273. package/src/plugins/encore/EncoreRedirect.vue +0 -116
  274. package/src/plugins/encore/View.vue +0 -36
  275. package/src/plugins/encore/defineEncoreDefinition.ts +0 -74
  276. package/src/plugins/encore/defineEncoreMeta.ts +0 -13
  277. package/src/plugins/encore/index.ts +0 -93
  278. package/src/plugins/encore/manageEncoreDefinition.ts +0 -100
  279. package/src/plugins/encore/manageEncoreMeta.ts +0 -36
  280. package/src/plugins/manageSource/Preview.vue +0 -33
  281. package/src/plugins/manageSource/View.vue +0 -13
  282. package/src/plugins/manageSource/definition.ts +0 -66
  283. package/src/plugins/manageSource/index.ts +0 -75
  284. package/src/plugins/manageSource/meta.ts +0 -21
  285. package/src/plugins/scheduler/CalendarView.vue +0 -23
  286. package/src/plugins/scheduler/Preview.vue +0 -73
  287. package/src/plugins/scheduler/View.vue +0 -608
  288. package/src/plugins/scheduler/calendarDefinition.ts +0 -47
  289. package/src/plugins/scheduler/calendarMeta.ts +0 -28
  290. package/src/plugins/scheduler/multiDayHelpers.ts +0 -95
  291. package/src/plugins/scheduler/viewModes.ts +0 -26
  292. package/src/types/encore-dsl/at-expression.ts +0 -120
  293. package/src/types/encore-dsl/at-resolver.ts +0 -32
  294. package/src/types/encore-dsl/cadence.ts +0 -289
  295. package/src/types/encore-dsl/schema.ts +0 -288
  296. package/src/utils/filesPreview/schedulerPreview.ts +0 -44
  297. package/src/utils/filesPreview/todoPreview.ts +0 -51
  298. package/src/utils/sources/filter.ts +0 -69
  299. /package/client/assets/{JsonEditor-D6WBWLoa.js → JsonEditor-C_RDoefj.js} +0 -0
  300. /package/client/assets/{chunk-D8eiyYIV-LcKZGJv5.js → chunk-D8eiyYIV-BY16KEZc.js} +0 -0
  301. /package/client/assets/{purify.es-Fx1Nqyry-Dwtk-9WZ.js → purify.es-Fx1Nqyry-BufT4RJl.js} +0 -0
  302. /package/client/assets/{typeof-DBp4T-Ny-CSr8wx1e.js → typeof-DBp4T-Ny-z2wCIsir.js} +0 -0
@@ -5,6 +5,7 @@
5
5
  class="w-full flex items-center gap-1 px-2 py-1 text-left text-sm hover:bg-gray-100 rounded"
6
6
  :data-testid="`file-tree-dir-${node.name || 'root'}`"
7
7
  @click="onToggle"
8
+ @contextmenu="onFolderContextMenu"
8
9
  >
9
10
  <span class="material-icons text-sm text-gray-400 shrink-0">{{ expanded ? "folder_open" : "folder" }}</span>
10
11
  <span class="text-gray-700 truncate">{{ node.name || t("fileTree.workspace") }}</span>
@@ -14,6 +15,7 @@
14
15
  class="w-full flex items-center gap-1 px-2 py-1 text-left text-sm rounded transition-colors"
15
16
  :class="selectedPath === node.path ? 'bg-blue-100 text-blue-700' : 'text-gray-700 hover:bg-gray-100'"
16
17
  :data-testid="`file-tree-file-${node.name}`"
18
+ :data-selected="selectedPath === node.path ? 'true' : undefined"
17
19
  :title="node.path"
18
20
  @click="emit('select', node.path)"
19
21
  >
@@ -22,6 +24,28 @@
22
24
  <span v-if="isRecent" class="ml-auto w-1.5 h-1.5 rounded-full bg-green-500 shrink-0" :title="t('fileTree.recentlyChanged')" />
23
25
  </button>
24
26
  <div v-if="node.type === 'dir' && expanded" class="pl-4">
27
+ <!-- New-file inline input (#1598). Shown when the user picked
28
+ "New file" from the folder's context menu. Mounted as the
29
+ first child so the new entry sits where the user expects
30
+ it (top of the folder). Esc / blur close; Enter submits. -->
31
+ <div v-if="createPending" class="flex items-center gap-1 px-2 py-1 text-sm" :data-testid="`file-tree-new-file-input-${node.name || 'root'}`">
32
+ <span class="material-icons text-sm text-gray-400 shrink-0">description</span>
33
+ <input
34
+ ref="newFileInputRef"
35
+ v-model="newFileSlug"
36
+ type="text"
37
+ class="flex-1 min-w-0 px-1 py-0.5 text-sm border border-blue-400 rounded focus:outline-none focus:ring-1 focus:ring-blue-400"
38
+ :placeholder="placeholderText"
39
+ :aria-label="t('fileTree.newFileInputAria')"
40
+ data-testid="file-tree-new-file-input"
41
+ @keydown="onInputKeydown"
42
+ @compositionstart="onCompositionStart"
43
+ @compositionend="onCompositionEnd"
44
+ @blur="onInputBlur"
45
+ />
46
+ <span class="text-xs text-gray-400 font-mono shrink-0 select-none">{{ createPolicy?.extension }}</span>
47
+ </div>
48
+ <div v-if="createError" class="px-2 py-1 text-xs text-red-600" data-testid="file-tree-new-file-error">{{ createError }}</div>
25
49
  <!-- Loading state: children not in the cache yet. Rendered
26
50
  once per dir so a slow network shows where the wait is,
27
51
  not as a global overlay. -->
@@ -36,17 +60,42 @@
36
60
  :sort-mode="sortMode"
37
61
  @select="(p) => emit('select', p)"
38
62
  @load-children="(p) => emit('loadChildren', p)"
63
+ @create-file="(args) => emit('createFile', args)"
39
64
  />
40
65
  </div>
66
+ <!-- Floating context menu (#1598). Position-fixed near the
67
+ click point so it floats above the tree row regardless of
68
+ the scrollable pane. One-shot; clicking the option starts
69
+ the inline input flow above. -->
70
+ <Teleport to="body">
71
+ <div
72
+ v-if="menuOpen"
73
+ class="fixed z-50 min-w-32 bg-white border border-gray-200 rounded shadow-md py-1 text-sm"
74
+ :style="{ top: `${menuY}px`, left: `${menuX}px` }"
75
+ data-testid="file-tree-context-menu"
76
+ @click.stop
77
+ >
78
+ <button
79
+ type="button"
80
+ class="w-full text-left px-3 py-1.5 hover:bg-gray-100 flex items-center gap-2"
81
+ data-testid="file-tree-context-new-file"
82
+ @click="onContextNewFile"
83
+ >
84
+ <span class="material-icons text-sm text-gray-400">note_add</span>
85
+ {{ t("fileTree.newFileMenuItem") }}
86
+ </button>
87
+ </div>
88
+ </Teleport>
41
89
  </div>
42
90
  </template>
43
91
 
44
92
  <script setup lang="ts">
45
- import { computed, watch } from "vue";
93
+ import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
46
94
  import { useI18n } from "vue-i18n";
47
95
  import { useExpandedDirs } from "../composables/useExpandedDirs";
48
96
  import { sortChildren } from "../utils/files/sortChildren";
49
97
  import { descriptorForPath, EDIT_POLICY_ICON_COLOR } from "../config/systemFileDescriptors";
98
+ import { normaliseNewFileSlug, policyForFolder } from "../config/createFilePolicy";
50
99
  import type { FileSortMode } from "../composables/useFileSortMode";
51
100
  import type { TreeNode } from "../types/fileTree";
52
101
 
@@ -74,13 +123,18 @@ const props = defineProps<{
74
123
  const emit = defineEmits<{
75
124
  select: [path: string];
76
125
  loadChildren: [path: string];
126
+ // Bubbled up to FilesView, which performs the PUT and refreshes
127
+ // the tree. Per-instance FileTree state is reset by the parent
128
+ // setting `childrenByPath` for this folder; the inline input here
129
+ // closes itself when its commit resolves.
130
+ createFile: [args: { folder: string; filename: string; resolve: (ok: boolean, error?: string) => void }];
77
131
  }>();
78
132
 
79
133
  // Expand/collapse state lives in a module-level singleton so every
80
134
  // recursive FileTree instance shares it, and survives remounts (e.g.
81
135
  // the agent-run refresh that bumps filesRefreshToken in FilesView).
82
136
  // Default on first run: only the workspace root ("") is expanded.
83
- const { isExpanded, toggle } = useExpandedDirs();
137
+ const { isExpanded, toggle, expand } = useExpandedDirs();
84
138
  const expanded = computed(() => isExpanded(props.node.path));
85
139
 
86
140
  const cached = computed(() => props.childrenByPath.get(props.node.path));
@@ -142,4 +196,166 @@ const iconColorClass = computed(() => {
142
196
  const descriptor = descriptorForPath(props.node.path);
143
197
  return descriptor ? EDIT_POLICY_ICON_COLOR[descriptor.editPolicy] : DEFAULT_FILE_ICON_COLOR;
144
198
  });
199
+
200
+ // --- Context menu + inline new-file input (#1598) -------------------
201
+
202
+ const createPolicy = computed(() => (props.node.type === "dir" ? policyForFolder(props.node.path) : null));
203
+ const placeholderText = computed(() => (createPolicy.value ? t(createPolicy.value.placeholderKey) : ""));
204
+
205
+ const menuOpen = ref(false);
206
+ const menuX = ref(0);
207
+ const menuY = ref(0);
208
+ const createPending = ref(false);
209
+ const newFileSlug = ref("");
210
+ const createError = ref<string | null>(null);
211
+ const newFileInputRef = ref<HTMLInputElement | null>(null);
212
+ // `blur` would close the input on every focus change. We mute it for
213
+ // a single tick when the user clicks the trailing extension label or
214
+ // re-focuses the field programmatically, so the cancel-on-blur path
215
+ // doesn't fight legitimate re-focus.
216
+ let suppressBlur = false;
217
+
218
+ function onFolderContextMenu(event: MouseEvent): void {
219
+ if (!createPolicy.value) return;
220
+ event.preventDefault();
221
+ menuX.value = event.clientX;
222
+ menuY.value = event.clientY;
223
+ menuOpen.value = true;
224
+ }
225
+
226
+ function closeMenu(): void {
227
+ menuOpen.value = false;
228
+ }
229
+
230
+ function onContextNewFile(): void {
231
+ closeMenu();
232
+ if (!createPolicy.value) return;
233
+ // Make sure the folder is open so the inline input is visible.
234
+ if (!isExpanded(props.node.path)) expand(props.node.path);
235
+ newFileSlug.value = "";
236
+ createError.value = null;
237
+ createPending.value = true;
238
+ void nextTick(() => {
239
+ newFileInputRef.value?.focus();
240
+ });
241
+ }
242
+
243
+ function cancelCreate(): void {
244
+ createPending.value = false;
245
+ newFileSlug.value = "";
246
+ createError.value = null;
247
+ }
248
+
249
+ function onInputBlur(): void {
250
+ if (suppressBlur) {
251
+ suppressBlur = false;
252
+ return;
253
+ }
254
+ // Cancel on blur — matches Finder/VSCode's "click away to discard"
255
+ // behaviour. Submit still happens on Enter explicitly.
256
+ cancelCreate();
257
+ }
258
+
259
+ const composingFlag = ref(false);
260
+ function onCompositionStart(): void {
261
+ composingFlag.value = true;
262
+ }
263
+ function onCompositionEnd(): void {
264
+ // Defer clearing so a keydown Enter that fires immediately after
265
+ // compositionend (Chromium IME-commit behaviour) still sees the
266
+ // flag true and is suppressed.
267
+ setTimeout(() => {
268
+ composingFlag.value = false;
269
+ }, 0);
270
+ }
271
+
272
+ function onInputKeydown(event: KeyboardEvent): void {
273
+ // Enter / Escape are wired here (not via Vue's `.enter` / `.esc`
274
+ // modifiers) because those modifiers were not firing for the user
275
+ // in #1598. Reading `event.key` directly matches the spec.
276
+ //
277
+ // IME guard: the Enter that commits a Japanese / Chinese / Korean
278
+ // composition fires keydown with either `isComposing === true`
279
+ // (Firefox) or `keyCode === 229` (Chrome / Safari). Without the
280
+ // 229 fallback the user gets an empty-filename error the moment
281
+ // they confirm an IME candidate. `composingFlag` covers the
282
+ // "compositionend just fired but the trailing keyup hasn't" gap
283
+ // some browsers expose.
284
+ if (event.key === "Enter") {
285
+ if (composingFlag.value || event.isComposing || event.keyCode === 229) return;
286
+ event.preventDefault();
287
+ onNewFileSubmit();
288
+ return;
289
+ }
290
+ if (event.key === "Escape") {
291
+ event.preventDefault();
292
+ cancelCreate();
293
+ }
294
+ }
295
+
296
+ function refocusInput(): void {
297
+ // Arm `suppressBlur` for the SINGLE blur that the focus() call
298
+ // may produce before settling, then clear it so the next real
299
+ // click-away triggers cancel-on-blur as expected. Without the
300
+ // post-focus clear the flag stays armed after a validation /
301
+ // save failure and silently swallows the user's next blur
302
+ // (CodeRabbit review on #1608).
303
+ suppressBlur = true;
304
+ void nextTick(() => {
305
+ newFileInputRef.value?.focus();
306
+ setTimeout(() => {
307
+ suppressBlur = false;
308
+ }, 0);
309
+ });
310
+ }
311
+
312
+ function onNewFileSubmit(): void {
313
+ if (!createPolicy.value) return;
314
+ const result = normaliseNewFileSlug(newFileSlug.value, createPolicy.value);
315
+ if (!result.ok) {
316
+ createError.value = result.reason === "empty" ? t("fileTree.newFileError.empty") : t("fileTree.newFileError.unsafe");
317
+ refocusInput();
318
+ return;
319
+ }
320
+ // Keep the input open until the parent's PUT resolves so a save
321
+ // failure leaves the user where they were typing. The parent
322
+ // hands back ok / error via the `resolve` callback.
323
+ suppressBlur = true;
324
+ emit("createFile", {
325
+ folder: props.node.path,
326
+ filename: result.filename,
327
+ resolve: (ok, error) => {
328
+ if (ok) {
329
+ cancelCreate();
330
+ return;
331
+ }
332
+ createError.value = error ?? t("fileTree.newFileError.saveFailed");
333
+ refocusInput();
334
+ },
335
+ });
336
+ }
337
+
338
+ // Global click closes the menu — Teleport puts it on body, so a
339
+ // click outside the menu but inside the tree pane will still trigger
340
+ // this. The Teleport's @click.stop above keeps clicks inside the
341
+ // menu from being treated as outside.
342
+ function onWindowClick(): void {
343
+ if (menuOpen.value) closeMenu();
344
+ }
345
+ function onWindowKeydown(event: KeyboardEvent): void {
346
+ if (event.key === "Escape" && menuOpen.value) closeMenu();
347
+ }
348
+ watch(menuOpen, (open) => {
349
+ if (open) {
350
+ window.addEventListener("click", onWindowClick);
351
+ window.addEventListener("keydown", onWindowKeydown);
352
+ } else {
353
+ window.removeEventListener("click", onWindowClick);
354
+ window.removeEventListener("keydown", onWindowKeydown);
355
+ }
356
+ });
357
+ onBeforeUnmount(() => {
358
+ window.removeEventListener("click", onWindowClick);
359
+ window.removeEventListener("keydown", onWindowKeydown);
360
+ });
145
361
  </script>
@@ -40,6 +40,7 @@
40
40
  :sort-mode="sortMode"
41
41
  @select="emit('select', $event)"
42
42
  @load-children="emit('loadChildren', $event)"
43
+ @create-file="emit('createFile', $event)"
43
44
  />
44
45
  <template v-if="refRoots.length > 0">
45
46
  <div class="mt-2 pt-2 border-t border-gray-200 px-1 mb-1 flex items-center gap-1">
@@ -83,6 +84,7 @@ const emit = defineEmits<{
83
84
  select: [path: string];
84
85
  loadChildren: [path: string];
85
86
  "update:sortMode": [mode: FileSortMode];
87
+ createFile: [args: { folder: string; filename: string; resolve: (ok: boolean, error?: string) => void }];
86
88
  }>();
87
89
 
88
90
  // Shared empty set for reference roots (they don't highlight recents).
@@ -1,6 +1,7 @@
1
1
  <template>
2
- <div class="h-full flex bg-white">
2
+ <div class="h-full flex bg-white" data-testid="files-view-root">
3
3
  <FileTreePane
4
+ ref="treePaneRef"
4
5
  :root-node="rootNode"
5
6
  :ref-roots="refRoots"
6
7
  :children-by-path="childrenByPath"
@@ -11,6 +12,7 @@
11
12
  @select="selectFile"
12
13
  @load-children="loadDirChildren"
13
14
  @update:sort-mode="setSortMode"
15
+ @create-file="handleCreateFile"
14
16
  />
15
17
  <!-- Content pane -->
16
18
  <div class="flex-1 flex flex-col min-w-0 overflow-hidden">
@@ -28,8 +30,6 @@
28
30
  :content="content"
29
31
  :content-error="contentError"
30
32
  :content-loading="contentLoading"
31
- :scheduler-result="schedulerResult"
32
- :todo-explorer-result="todoExplorerResult"
33
33
  :is-markdown="isMarkdown"
34
34
  :is-html="isHtml"
35
35
  :is-json="isJson"
@@ -74,10 +74,8 @@ import { useMarkdownMode } from "../composables/useMarkdownMode";
74
74
  import { useFileSortMode } from "../composables/useFileSortMode";
75
75
  import { useContentDisplay } from "../composables/useContentDisplay";
76
76
  import { useMarkdownLinkHandler } from "../composables/useMarkdownLinkHandler";
77
- import { apiPut } from "../utils/api";
77
+ import { apiPost, apiPut } from "../utils/api";
78
78
  import { API_ROUTES } from "../config/apiRoutes";
79
- import { toSchedulerResult } from "../utils/filesPreview/schedulerPreview";
80
- import { toTodoExplorerResult } from "../utils/filesPreview/todoPreview";
81
79
 
82
80
  const RECENT_THRESHOLD_MS = 60 * 1000;
83
81
 
@@ -95,7 +93,7 @@ const emit = defineEmits<{
95
93
  loadSession: [sessionId: string];
96
94
  }>();
97
95
 
98
- const { rootNode, refRoots, childrenByPath, treeError, loadDirChildren, ensureAncestorsLoaded, reloadRoot, loadRefRoots } = useFileTree();
96
+ const { rootNode, refRoots, childrenByPath, treeError, loadDirChildren, ensureAncestorsLoaded, reloadDirChildren, reloadRoot, loadRefRoots } = useFileTree();
99
97
 
100
98
  const { selectedPath, content, contentLoading, contentError, loadContent, selectFile, deselectFile, abortContent } = useFileSelection();
101
99
 
@@ -149,9 +147,47 @@ watch(content, () => {
149
147
  rawSaveError.value = null;
150
148
  });
151
149
 
152
- const schedulerResult = computed(() => toSchedulerResult(selectedPath.value, content.value?.kind === "text" ? content.value.content : null));
153
-
154
- const todoExplorerResult = computed(() => toTodoExplorerResult(selectedPath.value, content.value?.kind === "text" ? content.value.content : null));
150
+ // #1598 folder-row context menu "New file" inline flow. The
151
+ // FileTree component handles input + slug validation; here we
152
+ // only do the conflict check + PUT + refresh. The callback shape
153
+ // lets the inline input close itself on success and stay open
154
+ // (with the error label) on failure.
155
+ async function handleCreateFile(args: { folder: string; filename: string; resolve: (ok: boolean, error?: string) => void }): Promise<void> {
156
+ const { folder, filename, resolve } = args;
157
+ const targetPath = folder ? `${folder}/${filename}` : filename;
158
+ // Client-side conflict pre-check via the local cache — cheap and
159
+ // matches what the user sees. The server's create endpoint also
160
+ // refuses on conflict (#1598), so a tab racing with another that
161
+ // already won would still get a 409 below — this just turns the
162
+ // common case into a localised inline error without a round-trip.
163
+ const cached = childrenByPath.value.get(folder);
164
+ if (Array.isArray(cached) && cached.some((child) => child.name === filename)) {
165
+ resolve(false, t("fileTree.newFileError.exists", { filename }));
166
+ return;
167
+ }
168
+ const result = await apiPost<{ path: string; size: number; modifiedMs: number }>(API_ROUTES.files.create, {
169
+ path: targetPath,
170
+ content: "",
171
+ });
172
+ if (!result.ok) {
173
+ // Map HTTP status to a localised message so the inline error
174
+ // matches the rest of the menu's language. 409 = a race lost
175
+ // to another tab/agent that just created the same file.
176
+ if (result.status === 409) {
177
+ resolve(false, t("fileTree.newFileError.exists", { filename }));
178
+ await reloadDirChildren(folder);
179
+ return;
180
+ }
181
+ resolve(false, t("fileTree.newFileError.saveFailed"));
182
+ return;
183
+ }
184
+ resolve(true);
185
+ await reloadDirChildren(folder);
186
+ // Reveal the new file in the right-hand content pane so the user
187
+ // can start editing immediately. selectFile() also drives the URL
188
+ // bar + ancestor expansion via the existing watcher.
189
+ selectFile(result.data.path);
190
+ }
155
191
 
156
192
  const recentPaths = computed(() => {
157
193
  const set = new Set<string>();
@@ -197,6 +233,50 @@ watch(
197
233
  },
198
234
  );
199
235
 
236
+ // Keep the tree expanded down to the active selection regardless of
237
+ // how the selection changed (URL bar, back/forward, selectFile from a
238
+ // markdown link). selectFile() updates selectedPath synchronously
239
+ // before pushing the route, so a guard on the route watcher would
240
+ // miss in-app file→file navigation — we watch the source of truth
241
+ // directly. `immediate: true` covers the deep-link mount case, so
242
+ // onMounted doesn't need its own ensureAncestorsLoaded call.
243
+ // Idempotent: loadDirChildren and expand() both short-circuit when
244
+ // the cache/expand-state already has the ancestor.
245
+ watch(
246
+ selectedPath,
247
+ (newPath) => {
248
+ if (newPath) ensureAncestorsLoaded(newPath);
249
+ },
250
+ { immediate: true },
251
+ );
252
+
253
+ // Reveal the selected file row in the tree pane. The tree grows
254
+ // incrementally on deep-link mount: ensureAncestorsLoaded fetches the
255
+ // direct ancestors, but sibling dirs whose `expanded` state was
256
+ // restored from localStorage lazy-load their children later via each
257
+ // FileTree's own watcher. Each of those loads pushes the selected
258
+ // row further down, so scrollIntoView must re-run whenever the tree
259
+ // grows, not just once on selection change. A pending-rAF guard
260
+ // coalesces a burst of childrenByPath updates into a single scroll.
261
+ // Scope the query to the FileTreePane's root via a template ref so
262
+ // it survives data-testid / DOM-structure changes elsewhere in
263
+ // FilesView.
264
+ const treePaneRef = ref<InstanceType<typeof FileTreePane> | null>(null);
265
+ let pendingRevealRaf = 0;
266
+ function revealSelectedInTree(): void {
267
+ if (!selectedPath.value) return;
268
+ if (pendingRevealRaf !== 0) return;
269
+ pendingRevealRaf = requestAnimationFrame(() => {
270
+ pendingRevealRaf = 0;
271
+ const paneRoot = treePaneRef.value?.$el as HTMLElement | undefined;
272
+ const button = paneRoot?.querySelector<HTMLElement>('button[data-selected="true"]');
273
+ button?.scrollIntoView({ block: "nearest" });
274
+ });
275
+ }
276
+
277
+ watch(selectedPath, revealSelectedInTree);
278
+ watch(childrenByPath, revealSelectedInTree);
279
+
200
280
  watch(
201
281
  () => props.refreshToken,
202
282
  () => {
@@ -209,16 +289,14 @@ onMounted(async () => {
209
289
  await loadDirChildren("");
210
290
  await loadRefRoots();
211
291
 
212
- // Deep-link: if the URL has a selected path, reveal its ancestors
213
- // by fetching each dir in sequence so the tree auto-expands to
214
- // the selection.
215
- if (selectedPath.value) {
216
- await ensureAncestorsLoaded(selectedPath.value);
217
- loadContent(selectedPath.value);
218
- }
292
+ // Deep-link content load. The ancestor expansion + scroll reveal are
293
+ // handled by the selectedPath watchers above (the ensureAncestorsLoaded
294
+ // watcher runs with immediate: true).
295
+ if (selectedPath.value) loadContent(selectedPath.value);
219
296
  });
220
297
 
221
298
  onUnmounted(() => {
222
299
  abortContent();
300
+ if (pendingRevealRaf !== 0) cancelAnimationFrame(pendingRevealRaf);
223
301
  });
224
302
  </script>
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ :class="[
5
+ 'h-8 w-8 flex items-center justify-center rounded transition-colors',
6
+ pinned ? 'text-amber-500 hover:bg-amber-50' : 'text-slate-400 hover:bg-slate-100 hover:text-slate-600',
7
+ ]"
8
+ :title="pinned ? t('shortcuts.unpin') : t('shortcuts.pin')"
9
+ :aria-label="pinned ? t('shortcuts.unpin') : t('shortcuts.pin')"
10
+ :aria-pressed="pinned"
11
+ :data-testid="`pin-toggle-${kind}-${slug}`"
12
+ @click.stop="toggle"
13
+ @keydown.enter.stop
14
+ @keydown.space.stop
15
+ >
16
+ <span class="material-icons text-lg">{{ pinned ? "star" : "star_border" }}</span>
17
+ </button>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import { computed } from "vue";
22
+ import { useI18n } from "vue-i18n";
23
+ import { useShortcuts } from "../composables/useShortcuts";
24
+ import type { ShortcutKind } from "../types/shortcuts";
25
+
26
+ // Shared ★ toggle used by the collections / feeds index cards and the
27
+ // individual view header. Talks to the `useShortcuts` singleton itself,
28
+ // so a parent only supplies the target's identity + cached label/icon.
29
+ // Click + keyboard activation are stopped so toggling the star never
30
+ // also opens the underlying card.
31
+
32
+ const props = defineProps<{
33
+ kind: ShortcutKind;
34
+ slug: string;
35
+ /** Cached at pin time so the launcher renders without re-fetching. */
36
+ title: string;
37
+ icon: string;
38
+ }>();
39
+
40
+ const { t } = useI18n();
41
+ const { isPinned, pin, unpin } = useShortcuts();
42
+
43
+ const pinned = computed(() => isPinned(props.kind, props.slug));
44
+
45
+ function toggle(): void {
46
+ if (pinned.value) {
47
+ void unpin(props.kind, props.slug);
48
+ } else {
49
+ void pin({ kind: props.kind, slug: props.slug, title: props.title, icon: props.icon });
50
+ }
51
+ }
52
+ </script>