From 0da349f6acffe4cacc89aab90991f0a2f85980ab Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 28 Jul 2026 22:59:36 +0100 Subject: [PATCH 1/2] feat: group shell actions and bundle default skills --- CMakeLists.txt | 17 +- .../src/components/chat/modelOptions.test.ts | 24 +- UI-V2/src/components/chat/modelOptions.ts | 8 +- .../settings/MarkdownStoreModal.test.tsx | 12 +- .../settings/MarkdownStoreModal.tsx | 16 +- .../src/components/settings/SettingsModal.tsx | 22 +- .../settings/ShellActionsSettings.test.tsx | 5 + .../settings/ShellActionsSettings.tsx | 40 +- UI-V2/src/components/views/ChatView.test.tsx | 2 +- UI-V2/src/store/cpp/sanitizers.ts | 5 +- UI-V2/src/store/cpp/types.ts | 1 + UI-V2/src/store/slices/foldersSlice.ts | 2 +- UI-V2/src/store/useAppStore.test.ts | 1 + UI-V2/src/types/markdownStore.ts | 1 + UI-V2/src/utils/providerMetadata.ts | 8 +- markdown-store/bundled/Coding/bug-fix.uam | 44 ++ markdown-store/bundled/Coding/code-review.uam | 30 + .../bundled/Coding/learn-repository.uam | 31 + .../bundled/Coding/log-analysis.uam | 88 +++ .../bundled/Coding/project-summary.uam | 37 + .../bundled/Coding/security-audit.uam | 127 +++ .../bundled/Coding/skill-builder.uam | 85 ++ .../bundled/Coding/warm-project.uam | 27 + .../{ => bundled/GitHub}/github-create-pr.uam | 0 .../{ => bundled/GitHub}/github-fix-issue.uam | 0 .../GitHub}/github-issue-creator.uam | 0 .../GitHub}/github-repo-cleanup.uam | 0 .../GitHub}/github-version-tag-release.uam | 0 .../bundled/SVN/svn-create-review.uam | 43 ++ .../bundled/SVN/svn-draft-issue.uam | 41 + markdown-store/bundled/SVN/svn-fix-issue.uam | 44 ++ .../bundled/SVN/svn-repo-cleanup.uam | 43 ++ .../bundled/SVN/svn-version-tag-release.uam | 48 ++ markdown-store/security-audit.uam | 724 ------------------ markdown-store/skill-builder.uam | 78 -- src/app/application.cpp | 18 +- src/app/markdown_store_service.cpp | 82 +- src/app/markdown_store_service.h | 2 + src/app/shell_action_service.cpp | 352 ++++++++- src/app/shell_action_service.h | 2 +- src/cef/shell_action_handlers.cpp | 7 + src/cef/state_serializer.cpp | 1 + src/common/models/app_models.h | 1 + .../platform/platform_process_windows.cpp | 19 +- src/common/provider/codex/codex_options.h | 1 + tests/core_tests.cpp | 168 +++- 46 files changed, 1402 insertions(+), 905 deletions(-) create mode 100644 markdown-store/bundled/Coding/bug-fix.uam create mode 100644 markdown-store/bundled/Coding/code-review.uam create mode 100644 markdown-store/bundled/Coding/learn-repository.uam create mode 100644 markdown-store/bundled/Coding/log-analysis.uam create mode 100644 markdown-store/bundled/Coding/project-summary.uam create mode 100644 markdown-store/bundled/Coding/security-audit.uam create mode 100644 markdown-store/bundled/Coding/skill-builder.uam create mode 100644 markdown-store/bundled/Coding/warm-project.uam rename markdown-store/{ => bundled/GitHub}/github-create-pr.uam (100%) rename markdown-store/{ => bundled/GitHub}/github-fix-issue.uam (100%) rename markdown-store/{ => bundled/GitHub}/github-issue-creator.uam (100%) rename markdown-store/{ => bundled/GitHub}/github-repo-cleanup.uam (100%) rename markdown-store/{ => bundled/GitHub}/github-version-tag-release.uam (100%) create mode 100644 markdown-store/bundled/SVN/svn-create-review.uam create mode 100644 markdown-store/bundled/SVN/svn-draft-issue.uam create mode 100644 markdown-store/bundled/SVN/svn-fix-issue.uam create mode 100644 markdown-store/bundled/SVN/svn-repo-cleanup.uam create mode 100644 markdown-store/bundled/SVN/svn-version-tag-release.uam delete mode 100644 markdown-store/security-audit.uam delete mode 100644 markdown-store/skill-builder.uam diff --git a/CMakeLists.txt b/CMakeLists.txt index 07516d40..26c5c8c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -663,9 +663,13 @@ if(APPLE AND UAM_MACOS_BUNDLE) "$/Resources/UI-V2/dist" COMMAND ${CMAKE_COMMAND} -E make_directory "$/Resources/markdown-store" - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${CMAKE_CURRENT_SOURCE_DIR}/markdown-store/skill-builder.uam" + COMMAND ${CMAKE_COMMAND} -E rm -f "$/Resources/markdown-store/skill-builder.uam" + COMMAND ${CMAKE_COMMAND} -E remove_directory + "$/Resources/markdown-store/bundled" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/markdown-store/bundled" + "$/Resources/markdown-store/bundled" COMMAND ${CMAKE_COMMAND} -E echo "Bundled UI-V2 build id: ${UAM_UI_BUILD_ID}" COMMENT "Copying UI-V2/dist and bundled Markdown Store entries to Contents/Resources/" ) @@ -678,9 +682,13 @@ else() "$/UI-V2/dist" COMMAND ${CMAKE_COMMAND} -E make_directory "$/markdown-store" - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${CMAKE_CURRENT_SOURCE_DIR}/markdown-store/skill-builder.uam" + COMMAND ${CMAKE_COMMAND} -E rm -f "$/markdown-store/skill-builder.uam" + COMMAND ${CMAKE_COMMAND} -E remove_directory + "$/markdown-store/bundled" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/markdown-store/bundled" + "$/markdown-store/bundled" COMMAND ${CMAKE_COMMAND} -E echo "Bundled UI-V2 build id: ${UAM_UI_BUILD_ID}" COMMENT "Copying UI-V2/dist and bundled Markdown Store entries to build output" ) @@ -892,6 +900,7 @@ if(UAM_BUILD_TESTS) libcef_dll_wrapper "${cef_binary_SOURCE_DIR}/Release/libcef.lib" advapi32 + shell32 windowsapp ) endif() diff --git a/UI-V2/src/components/chat/modelOptions.test.ts b/UI-V2/src/components/chat/modelOptions.test.ts index 0dc4a0d9..300d787d 100644 --- a/UI-V2/src/components/chat/modelOptions.test.ts +++ b/UI-V2/src/components/chat/modelOptions.test.ts @@ -1,21 +1,22 @@ import { describe, expect, it } from 'vitest' import type { AcpBinding } from '../../store/cpp/types' -import { buildModelOptions, reasoningEffortForModel } from './modelOptions' +import { buildCodexReasoningOptions, buildModelOptions, reasoningEffortForModel } from './modelOptions' describe('reasoningEffortForModel', () => { it('defaults invalid or empty effort to the runtime model default', () => { const acp = { availableModels: [{ - id: 'gpt-5.4', - name: 'GPT-5.4', + id: 'gpt-5.6', + name: 'GPT-5.6', defaultReasoningEffort: 'medium', - supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh'], + supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'ultra'], }], } as AcpBinding - expect(reasoningEffortForModel(acp, 'gpt-5.4')).toBe('medium') - expect(reasoningEffortForModel(acp, 'gpt-5.4', 'unknown')).toBe('medium') - expect(reasoningEffortForModel(acp, 'gpt-5.4', 'low')).toBe('low') + expect(reasoningEffortForModel(acp, 'gpt-5.6')).toBe('medium') + expect(reasoningEffortForModel(acp, 'gpt-5.6', 'unknown')).toBe('medium') + expect(reasoningEffortForModel(acp, 'gpt-5.6', 'low')).toBe('low') + expect(reasoningEffortForModel(acp, 'gpt-5.6', 'ultra')).toBe('ultra') }) it('keeps provider-default model selection explicit', () => { @@ -23,4 +24,13 @@ describe('reasoningEffortForModel', () => { expect(options[0]).toMatchObject({ id: '', label: 'Default' }) }) + + it('offers ultra reasoning when live model metadata is unavailable', () => { + expect(buildCodexReasoningOptions(undefined, 'gpt-5.6').at(-1)).toMatchObject({ + id: 'ultra', + label: 'Ultra', + }) + expect(buildCodexReasoningOptions(undefined, 'gpt-5.4').some((option) => option.id === 'ultra')).toBe(false) + expect(reasoningEffortForModel(undefined, 'gpt-5.4', 'ultra')).toBe('') + }) }) diff --git a/UI-V2/src/components/chat/modelOptions.ts b/UI-V2/src/components/chat/modelOptions.ts index b229d4d6..7a8b5bc5 100644 --- a/UI-V2/src/components/chat/modelOptions.ts +++ b/UI-V2/src/components/chat/modelOptions.ts @@ -143,15 +143,17 @@ export function buildCodexReasoningOptions(acp: AcpBinding | undefined, modelId: const runtimeModel = selectedRuntimeModel(acp, modelId) const runtimeEfforts = runtimeModel?.supportedReasoningEfforts ?? [] if (runtimeModel && runtimeEfforts.length === 0) return [] - const base = runtimeEfforts.length > 0 ? runtimeEfforts : ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'] + const fallbackEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'] + if (/^gpt-5\.6(?:$|-)/i.test(modelId.trim())) fallbackEfforts.push('ultra') + const base = runtimeEfforts.length > 0 ? runtimeEfforts : fallbackEfforts const ids = runtimeModel ? [...base] : ['', ...base] - if (!runtimeModel && selectedReasoningEffort && !ids.includes(selectedReasoningEffort)) ids.push(selectedReasoningEffort) + if (!runtimeModel && selectedReasoningEffort && selectedReasoningEffort !== 'ultra' && !ids.includes(selectedReasoningEffort)) ids.push(selectedReasoningEffort) return Array.from(new Set(ids)).map((id) => labeledOption(id, CODEX_REASONING_LABELS)) } export function reasoningEffortForModel(acp: AcpBinding | undefined, modelId: string, currentEffort = '') { const model = selectedRuntimeModel(acp, modelId) - if (!model) return currentEffort + if (!model) return currentEffort === 'ultra' && !/^gpt-5\.6(?:$|-)/i.test(modelId.trim()) ? '' : currentEffort const supported = model.supportedReasoningEfforts ?? [] if (supported.length === 0) return '' if (supported.includes(currentEffort)) return currentEffort diff --git a/UI-V2/src/components/settings/MarkdownStoreModal.test.tsx b/UI-V2/src/components/settings/MarkdownStoreModal.test.tsx index c9f41b17..a1abd423 100644 --- a/UI-V2/src/components/settings/MarkdownStoreModal.test.tsx +++ b/UI-V2/src/components/settings/MarkdownStoreModal.test.tsx @@ -15,7 +15,7 @@ describe('MarkdownStoreModal', () => { markdownStoreLoading: false, markdownStoreError: '', markdownStoreEntries: [ - { id: 'review', title: 'Review code', maker: 'David', review: '', dateCreated: '', dateUpdated: '', preview: 'Find regressions', body: '# Review\n\nFind regressions', favorite: false, sourceProvider: 'codex', sourcePath: '/tmp/codex/review.md', commandName: 'review-code', filePath: '/tmp/store/review.uam' }, + { id: 'review', title: 'Review code', maker: 'David', review: '', dateCreated: '', dateUpdated: '', preview: 'Find regressions', body: '# Review\n\nFind regressions', favorite: false, sourceProvider: 'codex', sourcePath: '/tmp/codex/review.md', commandName: 'review-code', group: 'Coding', filePath: '/tmp/store/review.uam' }, { id: 'notes', title: 'Release notes', maker: 'Sam', review: '', dateCreated: '', dateUpdated: '', preview: 'Summarize changes', body: '# Notes', favorite: true, sourceProvider: 'gemini-cli', sourcePath: '/tmp/gemini/notes.md', commandName: 'release-notes', filePath: '/tmp/store/notes.uam' }, ], closeMarkdownStore: vi.fn(), @@ -113,6 +113,9 @@ describe('MarkdownStoreModal', () => { act(() => { filter.value = 'source:codex'; filter.dispatchEvent(new Event('change', { bubbles: true })) }) expect(host.textContent).toContain('Review code') expect(host.textContent).not.toContain('Release notes') + act(() => { filter.value = 'group:Coding'; filter.dispatchEvent(new Event('change', { bubbles: true })) }) + expect(host.textContent).toContain('Review code') + expect(host.textContent).not.toContain('Release notes') const favorite = host.querySelector('[aria-label="Add Review code to favorites"]') as HTMLElement await act(async () => { favorite.dispatchEvent(new MouseEvent('click', { bubbles: true })); await Promise.resolve() }) @@ -136,11 +139,16 @@ describe('MarkdownStoreModal', () => { Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set?.call(title, 'Review carefully') title.dispatchEvent(new Event('input', { bubbles: true })) }) + const group = host.querySelector('input[aria-label="Entry group"]') as HTMLInputElement + act(() => { + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set?.call(group, 'Coding / Review') + group.dispatchEvent(new Event('input', { bubbles: true })) + }) act(() => Array.from(host.querySelectorAll('button')).find((button) => button.textContent === 'Preview Markdown')?.click()) expect(host.textContent).toContain('Find regressions') await act(async () => { Array.from(host.querySelectorAll('button')).find((button) => button.textContent === 'Save')?.click(); await Promise.resolve() }) - expect(update).toHaveBeenCalledWith(expect.objectContaining({ id: 'review' }), expect.objectContaining({ title: 'Review carefully' })) + expect(update).toHaveBeenCalledWith(expect.objectContaining({ id: 'review' }), expect.objectContaining({ title: 'Review carefully', group: 'Coding / Review' })) expect(host.querySelector('input[aria-label="Entry title"]')).toBeTruthy() await act(async () => { Array.from(host.querySelectorAll('button')).find((button) => button.textContent === 'Save')?.click(); await Promise.resolve() }) expect(host.querySelector('input[aria-label="Entry title"]')).toBeFalsy() diff --git a/UI-V2/src/components/settings/MarkdownStoreModal.tsx b/UI-V2/src/components/settings/MarkdownStoreModal.tsx index da2fb46d..9b52956f 100644 --- a/UI-V2/src/components/settings/MarkdownStoreModal.tsx +++ b/UI-V2/src/components/settings/MarkdownStoreModal.tsx @@ -6,7 +6,7 @@ import { MarkdownContent } from '../markdown/Markdown' import type { MarkdownStoreConflictAction, MarkdownStoreDraft, MarkdownStoreEntry, MarkdownStoreImportCandidate, MarkdownStoreImportResult } from '../../types/markdownStore' import { BookOpen, ExternalLink, FileInput, FolderInput, Paperclip, Pencil, Plus, RefreshCw, Search, Star, Users, X } from 'lucide-react' -const EMPTY_DRAFT: MarkdownStoreDraft = { title: '', maker: '', review: '', body: '' } +const EMPTY_DRAFT: MarkdownStoreDraft = { title: '', maker: '', review: '', body: '', group: '' } export function MarkdownStoreModal() { const activeSessionId = useAppStore((s) => s.activeSessionId) @@ -54,12 +54,14 @@ export function MarkdownStoreModal() { }, [entries, selectedPath]) const sourceProviders = useMemo(() => Array.from(new Set(entries.map((entry) => entry.sourceProvider).filter(Boolean))).sort(), [entries]) + const groups = useMemo(() => Array.from(new Set(entries.map((entry) => entry.group?.trim()).filter(Boolean))).sort(), [entries]) const filtered = useMemo(() => { const query = search.trim().toLowerCase() return entries.filter((entry) => { if (filter === 'favorites' && !entry.favorite) return false if (filter.startsWith('source:') && entry.sourceProvider !== filter.slice(7)) return false - return !query || [entry.title, entry.maker, entry.review, entry.preview, entry.sourceProvider ?? '', entry.filePath] + if (filter.startsWith('group:') && entry.group !== filter.slice(6)) return false + return !query || [entry.title, entry.maker, entry.review, entry.preview, entry.sourceProvider ?? '', entry.group ?? '', entry.filePath] .some((value) => value.toLowerCase().includes(query)) }) }, [entries, filter, search]) @@ -68,7 +70,7 @@ export function MarkdownStoreModal() { const beginNew = () => { setDraft(EMPTY_DRAFT); setEditing('new'); setShowEditorPreview(false) } const beginEdit = (entry: MarkdownStoreEntry) => { setSelectedPath(entry.filePath) - setDraft({ title: entry.title, maker: entry.maker, review: entry.review, body: entry.body ?? entry.preview }) + setDraft({ title: entry.title, maker: entry.maker, review: entry.review, body: entry.body ?? entry.preview, group: entry.group ?? '' }) setEditing('existing') setShowEditorPreview(false) } @@ -77,7 +79,7 @@ export function MarkdownStoreModal() { if (submittingRef.current || !draft.title.trim() || !draft.body.trim()) return submittingRef.current = true setSubmitting(true) - const clean = { title: draft.title.trim(), maker: draft.maker.trim(), review: draft.review.trim(), body: draft.body.trim() } + const clean = { title: draft.title.trim(), maker: draft.maker.trim(), review: draft.review.trim(), body: draft.body.trim(), group: draft.group.trim() } let ok = false try { ok = editing === 'existing' && selected ? await updateEntry(selected, clean) : await createEntry(clean) @@ -144,6 +146,7 @@ export function MarkdownStoreModal() { {filtered.length} of {entries.length} @@ -161,7 +164,8 @@ export function MarkdownStoreModal() { {editing &&
{editing === 'new' ? 'New entry' : 'Edit entry'}
setDraft({ ...draft, title: event.target.value })} placeholder="Title" className="text-sm" style={{ border: '1px solid var(--border)', borderRadius: 6, background: 'var(--surface)', color: 'var(--text)', padding: '8px 10px' }} /> -
setDraft({ ...draft, maker: event.target.value })} placeholder="Maker" className="text-sm" style={{ border: '1px solid var(--border)', borderRadius: 6, background: 'var(--surface)', color: 'var(--text)', padding: '8px 10px' }} /> setDraft({ ...draft, review: event.target.value })} placeholder="Summary" className="text-sm" style={{ border: '1px solid var(--border)', borderRadius: 6, background: 'var(--surface)', color: 'var(--text)', padding: '8px 10px' }} />
+
setDraft({ ...draft, maker: event.target.value })} placeholder="Maker" className="text-sm" style={{ border: '1px solid var(--border)', borderRadius: 6, background: 'var(--surface)', color: 'var(--text)', padding: '8px 10px' }} /> setDraft({ ...draft, group: event.target.value })} placeholder="Group, for example Coding / Safety" className="text-sm" style={{ border: '1px solid var(--border)', borderRadius: 6, background: 'var(--surface)', color: 'var(--text)', padding: '8px 10px' }} />
+ setDraft({ ...draft, review: event.target.value })} placeholder="Summary" className="text-sm" style={{ border: '1px solid var(--border)', borderRadius: 6, background: 'var(--surface)', color: 'var(--text)', padding: '8px 10px' }} /> {showEditorPreview ?
: