Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/catalog-rest-and-skill-toggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": minor
---

Add server endpoints that list installed plugins, enable or disable one, and list subagent profiles, and let a named skill be turned off so it is hidden from the model, the slash menu and the API.
5 changes: 5 additions & 0 deletions .changeset/inline-settings-route.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": minor
---

Open the web settings inside the app shell instead of over it, and add pages for plugins, skills, subagents, connectors, hooks and usage statistics.
94 changes: 94 additions & 0 deletions .github/workflows/desktop-download-badges.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Refreshes the per-platform desktop download counters behind the README badges.
#
# shields.io cannot do this on its own: its asset wildcards silently report 0
# (a tag whose .dmg had 39,877 downloads returns 0 for `*.dmg`), exact asset
# names embed the version so they break on every release, and `dynamic/json`
# rejects every filter expression. So we sum the counts here and publish them
# as shields `endpoint` documents.
#
# The documents land on the orphan `badges` branch, never on `main` — `main` is
# protected and rejects pushes from everyone, CI included.
name: Desktop Download Badges

on:
schedule:
# Once a day is plenty; the counters move slowly and the API is rate-limited.
- cron: '17 4 * * *'
workflow_dispatch: {}

permissions:
contents: write

concurrency:
group: desktop-download-badges
cancel-in-progress: true

jobs:
refresh:
runs-on: ubuntu-latest
steps:
- name: Checkout the badges branch
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # pinned from v4
with:
ref: badges
persist-credentials: true

- name: Sum the .dmg and .exe download counts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Desktop installers shipped from this repo up to v0.1.0 and move to
# pythinker-desktop-releases from the next release on. Summing both
# keeps the counters continuous across that move.
RELEASES_REPOS: PyModel/pythinker-code PyModel/pythinker-desktop-releases
run: |
set -euo pipefail

# --paginate walks every release, so the totals cover the whole
# history rather than just the latest tag. jq -s concatenates the one
# array each repo produces into a single list of releases.
for repo in ${RELEASES_REPOS}; do
gh api --paginate "repos/${repo}/releases"
done | jq -s 'add' > releases.json

# An empty suffix matches every asset, which is what the combined
# counter wants; `endswith("")` is true for any string.
write_badge() {
local suffix="$1" label="$2" out="$3"
local total
total="$(jq --arg s "$suffix" '
[ .[].assets[]
| select(.name | ascii_downcase | endswith($s))
| .download_count
] | add // 0
' releases.json)"
jq -n --arg label "$label" --arg message "$total" '{
schemaVersion: 1,
label: $label,
message: $message,
color: "4D6BFE"
}' > "$out"
echo "${label}: ${total}"
}

write_badge '.dmg' 'macOS .dmg' desktop-dmg.json
write_badge '.exe' 'Windows .exe' desktop-exe.json
# The shields `github/downloads/.../total` route reads one repository,
# so the combined counter has to be summed here like the other two.
write_badge '' 'desktop' desktop-total.json

rm -f releases.json

- name: Publish if the counts moved
run: |
set -euo pipefail
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
# Stage before comparing: a document added in this run is untracked,
# and `git diff` on an untracked path reports no change at all.
git add desktop-dmg.json desktop-exe.json desktop-total.json
if git diff --cached --quiet; then
echo 'Counts unchanged; nothing to publish.'
exit 0
fi
git commit -m 'chore: refresh desktop download counts'
git push origin badges
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

[![npm version](https://img.shields.io/npm/v/@pymodel/pythinker-code?style=for-the-badge&logo=npm&logoColor=white&color=CB3837&label=pythinker-code)](https://www.npmjs.com/package/@pymodel/pythinker-code)
[![Downloads](https://img.shields.io/npm/dm/@pymodel/pythinker-code?style=for-the-badge&logo=npm&logoColor=white&color=2b89ff&label=downloads)](https://www.npmjs.com/package/@pymodel/pythinker-code)
[![Desktop downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FPyModel%2Fpythinker-code%2Fbadges%2Fdesktop-total.json&style=for-the-badge&logo=github&logoColor=white)](https://github.com/PyModel/pythinker-desktop-releases/releases)
[![macOS .dmg](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FPyModel%2Fpythinker-code%2Fbadges%2Fdesktop-dmg.json&style=for-the-badge&logo=apple&logoColor=white)](https://github.com/PyModel/pythinker-desktop-releases/releases/latest)
[![Windows .exe](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FPyModel%2Fpythinker-code%2Fbadges%2Fdesktop-exe.json&style=for-the-badge&logo=windows&logoColor=white)](https://github.com/PyModel/pythinker-desktop-releases/releases/latest)
[![Node.js](https://img.shields.io/badge/Node.js-26%2B-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://github.com/PyModel/pythinker-code/blob/main/package.json)
[![License: MIT](https://img.shields.io/badge/License-MIT-16a34a.svg?style=for-the-badge)](https://github.com/PyModel/pythinker-code/blob/main/LICENSE)
[![CI](https://img.shields.io/github/actions/workflow/status/PyModel/pythinker-code/ci.yml?branch=main&label=CI&style=for-the-badge&logo=githubactions&logoColor=white)](https://github.com/PyModel/pythinker-code/actions/workflows/ci.yml?query=branch%3Amain)
Expand Down
111 changes: 82 additions & 29 deletions apps/pythinker-web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { AgentMember } from './types';
import ModelPicker from './components/ModelPicker.vue';
import ProviderManager from './components/ProviderManager.vue';
import NewSessionDialog from './components/NewSessionDialog.vue';
import SettingsDialog from './components/SettingsDialog.vue';
import SettingsPane from './components/settings/SettingsPane.vue';
import SessionsDialog from './components/SessionsDialog.vue';
import AddWorkspaceDialog from './components/AddWorkspaceDialog.vue';
import StatusPanel from './components/StatusPanel.vue';
Expand All @@ -30,6 +30,7 @@ import { isTraceEnabled } from './debug/trace';
import { usePythinkerWebClient } from './composables/usePythinkerWebClient';
import { useIsMobile } from './composables/useIsMobile';
import { useIsDark } from './composables/useIsDark';
import { useSettingsNav } from './composables/useSettingsNav';
import type { AppConfig, ThinkingLevel } from './api/types';
import type { FilePreviewRequest, ToolMedia } from './types';

Expand Down Expand Up @@ -610,6 +611,43 @@ const showAddWorkspace = ref(false);
const showStatusPanel = ref(false);
const showSettings = ref(false);

const {
activeTab: activeSettingsTab,
setTab: selectSettingsTab,
refreshActiveTab: refreshSettingsTab,
} = useSettingsNav({
counts: {
connectors: () => client.connectors.value.length,
plugins: () => client.plugins.value.length,
subagents: () => client.subagents.value.length,
},
onLoadConnectors: () => { void client.loadConnectors(); },
onLoadPlugins: () => { void client.loadPlugins(); },
onLoadSubagents: () => { void client.loadSubagents(); },
});

function openSettings(): void {
showSettings.value = true;
// The active tab persists across visits, so its data has to be refetched on
// open — the session it was loaded for may no longer be the active one.
refreshSettingsTab();
}

function toggleSettings(): void {
if (showSettings.value) showSettings.value = false;
else openSettings();
}

function loginFromSettings(): void {
showSettings.value = false;
openLogin();
}

function openOnboardingFromSettings(): void {
showSettings.value = false;
openOnboarding();
}

type SubmitPayload = {
text: string;
attachments: { fileId: string; kind: 'image' | 'video' }[];
Expand Down Expand Up @@ -854,6 +892,9 @@ function handleCloseAddWorkspace(): void {
// right pane shows the onboarding composer. The session is only created when
// the user sends the first message.
function handleCreateSession(): void {
// Starting a session leaves the settings route — the new draft has to be
// visible, and the content area can only show one of the two.
showSettings.value = false;
const wsId = client.activeWorkspaceId.value;
if (wsId) {
client.openWorkspaceDraft(wsId);
Expand All @@ -866,6 +907,7 @@ function handleCreateSession(): void {
// state in the chosen workspace. No backend session is created until the user
// actually sends a message.
function handleCreateSessionInWorkspace(workspaceId: string): void {
showSettings.value = false;
client.openWorkspaceDraft(workspaceId);
}

Expand Down Expand Up @@ -918,6 +960,8 @@ function openPr(url: string): void {
:attention-by-session="client.attentionBySession.value"
:pending-by-session="client.pendingBySession.value"
:unread-by-session="client.unreadBySession.value"
:mode="showSettings ? 'settings' : 'sessions'"
:active-settings-tab="activeSettingsTab"
@select="client.selectSession($event)"
@create="handleCreateSession"
@create-in-workspace="handleCreateSessionInWorkspace($event)"
Expand All @@ -929,7 +973,9 @@ function openPr(url: string): void {
@rename-workspace="(id, name) => client.renameWorkspace(id, name)"
@delete-workspace="(id) => client.deleteWorkspace(id)"
@select-workspaces="handleSelectWorkspaces"
@open-settings="showSettings = true"
@open-settings="openSettings"
@close-settings="showSettings = false"
@select-settings-tab="selectSettingsTab($event)"
@collapse="toggleSidebarCollapse"
/>
<ResizeHandle
Expand Down Expand Up @@ -992,7 +1038,7 @@ function openPr(url: string): void {
class="rail-btn"
:title="t('settings.title')"
:aria-label="t('settings.title')"
@click="showSettings = true"
@click="toggleSettings"
>
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="3" />
Expand All @@ -1014,8 +1060,40 @@ function openPr(url: string): void {
@open-settings="showMobileSettings = true"
/>

<SettingsPane
v-if="showSettings && !isMobile"
:active-tab="activeSettingsTab"
:theme="client.theme.value"
:color-scheme="client.colorScheme.value"
:ui-font-size="client.uiFontSize.value"
:auth-ready="client.authReady.value"
:account-model="client.defaultModel.value"
:notify="client.notifyOnComplete.value"
:notify-permission="client.notifyPermission.value"
:beta-toc="client.betaToc.value"
:config="client.config.value"
:models="client.models.value"
:config-saving="configSaving"
:skills="client.skills.value"
:connectors="client.connectors.value"
:connectors-loading="client.connectorsLoading.value"
:sessions="client.sessionsWithUsage.value"
:plugins="client.plugins.value"
:subagents="client.subagents.value"
@set-plugin-enabled="client.setPluginEnabled($event.pluginId, $event.enabled)"
@restart-connector="client.restartConnector($event)"
@set-theme="client.setTheme($event)"
@set-color-scheme="client.setColorScheme($event)"
@set-ui-font-size="client.setUiFontSize($event)"
@set-notify="client.setNotifyOnComplete($event)"
@set-beta-toc="client.setBetaToc($event)"
@update-config="handleUpdateConfig($event)"
@login="loginFromSettings"
@open-onboarding="openOnboardingFromSettings"
/>

<ConversationPane
v-if="!hasMultiSelect"
v-else-if="!hasMultiSelect"
ref="conversationPaneRef"
:mobile="isMobile"
:modern="client.theme.value === 'modern' || client.theme.value === 'pythinker'"
Expand Down Expand Up @@ -1188,31 +1266,6 @@ function openPr(url: string): void {
@close="showModelPicker = false"
/>

<!-- Settings page (modal) -->
<SettingsDialog
v-if="showSettings"
:theme="client.theme.value"
:color-scheme="client.colorScheme.value"
:ui-font-size="client.uiFontSize.value"
:auth-ready="client.authReady.value"
:account-model="client.defaultModel.value"
:notify="client.notifyOnComplete.value"
:notify-permission="client.notifyPermission.value"
:beta-toc="client.betaToc.value"
:config="client.config.value"
:models="client.models.value"
:config-saving="configSaving"
@set-theme="client.setTheme($event)"
@set-color-scheme="client.setColorScheme($event)"
@set-ui-font-size="client.setUiFontSize($event)"
@set-notify="client.setNotifyOnComplete($event)"
@set-beta-toc="client.setBetaToc($event)"
@update-config="handleUpdateConfig($event)"
@login="() => { showSettings = false; openLogin(); }"
@open-onboarding="() => { showSettings = false; openOnboarding(); }"
@close="showSettings = false"
/>

<!-- Provider Manager overlay -->
<ProviderManager
v-if="showProviders"
Expand Down
Loading
Loading