From c7060c147d88c922ee687f9cab7fef15cec31b6e Mon Sep 17 00:00:00 2001 From: zhayujie Date: Thu, 16 Jul 2026 17:00:21 +0800 Subject: [PATCH] fix: windows desktop workflow --- .github/workflows/release-win7.yml | 47 +++++++++++++++------ desktop/src/renderer/src/index.css | 22 +++++----- desktop/src/renderer/src/pages/ChatPage.tsx | 20 +++++---- desktop/src/renderer/src/store/chatStore.ts | 42 ++++++++++++++++++ desktop/src/renderer/src/store/uiStore.ts | 5 +++ desktop/src/renderer/src/types.ts | 6 +-- 6 files changed, 107 insertions(+), 35 deletions(-) diff --git a/.github/workflows/release-win7.yml b/.github/workflows/release-win7.yml index 6fff1930..549c9646 100644 --- a/.github/workflows/release-win7.yml +++ b/.github/workflows/release-win7.yml @@ -117,27 +117,48 @@ jobs: if-no-files-found: warn retention-days: 7 - # Mirror the installer to R2 so it downloads fast (GitHub artifacts are - # painfully slow from China). Reuses the same cow-skills bucket + - # cdn.cowagent.ai domain as the main pipeline, but under a dedicated - # desktop/win7// prefix. NO D1 write: this is an unpublished test - # build. Skips cleanly when Cloudflare secrets aren't configured. + # Mirror the installer to R2 so it downloads fast (GitHub artifacts are + # painfully slow from China). Mirrors the MAIN pipeline's publish-r2 job: + # a SEPARATE ubuntu-latest job with NO setup-node, so it uses the runner's + # built-in Node 22+ and wrangler@latest works (the build job runs Node 20 for + # Electron, where wrangler@latest — which needs Node >=22 — would abort). + # Reuses the same cow-skills bucket + cdn.cowagent.ai domain, under a + # dedicated desktop/win7// prefix. NO D1 write: this is an + # unpublished test build. Skips cleanly when Cloudflare secrets are absent. + publish-r2: + name: Publish to R2 + needs: build + runs-on: ubuntu-latest + steps: + - name: Guard on Cloudflare secrets + id: guard + env: + CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + run: | + if [ -n "$CF_TOKEN" ]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "::notice::CLOUDFLARE_API_TOKEN not set — skipping R2 upload (use the artifact instead)." + fi + + - name: Download build artifact + if: steps.guard.outputs.enabled == 'true' + uses: actions/download-artifact@v4 + with: + name: cowagent-win7-x64 + path: dist + - name: Upload installer to R2 - shell: bash + if: steps.guard.outputs.enabled == 'true' env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} R2_BUCKET: ${{ vars.R2_BUCKET != '' && vars.R2_BUCKET || 'cow-skills' }} VER: ${{ github.event.inputs.version }} run: | - # Skip cleanly when Cloudflare isn't configured (secrets are only - # readable inside run:, so we gate here rather than in the step `if`). - if [ -z "$CLOUDFLARE_API_TOKEN" ]; then - echo "::notice::CLOUDFLARE_API_TOKEN not set — skipping R2 upload (use the artifact instead)." - exit 0 - fi shopt -s nullglob - for f in desktop/release/*.exe; do + for f in dist/*.exe; do base="$(basename "$f")" key="desktop/win7/v${VER}/${base}" echo "==> Uploading $base -> r2://${R2_BUCKET}/${key}" diff --git a/desktop/src/renderer/src/index.css b/desktop/src/renderer/src/index.css index b290cf93..43aaaa2b 100644 --- a/desktop/src/renderer/src/index.css +++ b/desktop/src/renderer/src/index.css @@ -27,21 +27,21 @@ --danger-border: rgba(239, 68, 68, 0.3); --info: #3b82f6; - /* Light theme — layered neutral surfaces */ - --bg-base: #fafafa; /* app background */ + /* Light theme — aligned with the web console (Tailwind gray/slate palette) */ + --bg-base: #f9fafb; /* app background (gray-50) */ --bg-surface: #ffffff; /* panels, cards */ - --bg-surface-2: #f4f4f5; /* nested surfaces, hover fills */ + --bg-surface-2: #f1f5f9; /* nested surfaces, hover fills (slate-100) */ --bg-elevated: #ffffff; /* popovers, menus, modals */ - --bg-inset: #f4f4f5; /* inputs, code blocks */ + --bg-inset: #f1f5f9; /* inputs, code blocks (slate-100) */ - --text-primary: #18181b; /* headings, primary text (contrast > 4.5:1) */ - --text-secondary: #52525b; /* body, labels */ - --text-tertiary: #71717a; /* hints, captions */ - --text-disabled: #a1a1aa; + --text-primary: #1e293b; /* headings, primary text (slate-800) */ + --text-secondary: #475569; /* body, labels (slate-600) */ + --text-tertiary: #64748b; /* hints, captions (slate-500) */ + --text-disabled: #94a3b8; /* slate-400 */ - --border-default: #e4e4e7; - --border-strong: #d4d4d8; - --border-subtle: #f0f0f1; + --border-default: #e2e8f0; /* slate-200 */ + --border-strong: #cbd5e1; /* slate-300 */ + --border-subtle: #f1f5f9; /* slate-100 */ --overlay: rgba(0, 0, 0, 0.4); --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.06); diff --git a/desktop/src/renderer/src/pages/ChatPage.tsx b/desktop/src/renderer/src/pages/ChatPage.tsx index 837e5138..b27d1b10 100644 --- a/desktop/src/renderer/src/pages/ChatPage.tsx +++ b/desktop/src/renderer/src/pages/ChatPage.tsx @@ -17,6 +17,7 @@ import apiClient from '../api/client' import type { Attachment, ChatMessage } from '../types' import { useChatStore } from '../store/chatStore' import { useSessionStore } from '../store/sessionStore' +import { useUIStore } from '../store/uiStore' interface ChatPageProps { baseUrl: string @@ -54,6 +55,8 @@ const ChatPage: React.FC = ({ baseUrl }) => { const deleteMessage = useChatStore((s) => s.deleteMessage) const loadHistory = useChatStore((s) => s.loadHistory) const ensureSession = useChatStore((s) => s.ensureSession) + const clearContext = useChatStore((s) => s.clearContext) + const setSessionsCollapsed = useUIStore((s) => s.setSessionsCollapsed) const messages = session?.messages ?? [] const isStreaming = session?.isStreaming ?? false @@ -170,15 +173,14 @@ const ChatPage: React.FC = ({ baseUrl }) => { const id = newSession() ensureSession(id) loadHistory(id, 1) - }, [newSession, ensureSession, loadHistory]) + // Auto-expand the session list so the user sees the new/switched session. + setSessionsCollapsed(false) + }, [newSession, ensureSession, loadHistory, setSessionsCollapsed]) const handleClearContext = useCallback(async () => { - try { - await apiClient.clearContext(activeId) - } catch { - /* ignore */ - } - }, [activeId]) + await clearContext(activeId) + scrollToBottom(true) + }, [clearContext, activeId, scrollToBottom]) const handleStop = useCallback(() => cancel(activeId), [cancel, activeId]) @@ -242,7 +244,9 @@ const ChatPage: React.FC = ({ baseUrl }) => {
CowAgent

{t('chat_welcome')}

-

{t('chat_empty_hint')}

+

+ {t('welcome_subtitle')} +

{SUGGESTIONS.map(({ key, send, icon: Icon, iconClass, bgClass }) => ( diff --git a/desktop/src/renderer/src/store/chatStore.ts b/desktop/src/renderer/src/store/chatStore.ts index c904c6e6..2281fd9b 100644 --- a/desktop/src/renderer/src/store/chatStore.ts +++ b/desktop/src/renderer/src/store/chatStore.ts @@ -31,6 +31,7 @@ interface ChatState { deleteMessage: (sid: string, userSeq: number, cascade: boolean) => Promise loadHistory: (sid: string, page?: number) => Promise + clearContext: (sid: string) => Promise clearLocal: (sid: string) => void } @@ -397,6 +398,25 @@ export const useChatStore = create((set, get) => { cancel: async (sid) => { const s = get().sessions[sid] if (!s?.requestId) return + // Optimistically stop the UI right away: mark the last assistant bubble + // cancelled, free the input, and tear down the local SSE stream so no + // further deltas render after the user hit stop. The backend still gets + // the cancel request to abort the running agent task. + patchMessages(sid, (msgs) => { + for (let i = msgs.length - 1; i >= 0; i--) { + if (msgs[i].role === 'assistant') { + msgs[i] = { ...msgs[i], isCancelled: true, isStreaming: false } + break + } + } + return [...msgs] + }) + patchSession(sid, { isStreaming: false, requestId: null }) + const es = streams[sid] + if (es) { + es.close() + delete streams[sid] + } try { await apiClient.cancel({ requestId: s.requestId, sessionId: sid }) } catch { @@ -476,6 +496,28 @@ export const useChatStore = create((set, get) => { } }, + clearContext: async (sid) => { + try { + const res = await apiClient.clearContext(sid) + if (res.status !== 'success') return false + // Append a visual divider so the user sees the context was cleared + // (mirrors the web console's context-divider). + patchMessages(sid, (msgs) => [ + ...msgs, + { + id: uid('divider'), + role: 'system', + kind: 'divider', + content: '', + timestamp: Date.now() / 1000, + }, + ]) + return true + } catch { + return false + } + }, + clearLocal: (sid) => { const es = streams[sid] if (es) { diff --git a/desktop/src/renderer/src/store/uiStore.ts b/desktop/src/renderer/src/store/uiStore.ts index 8360e206..91b7a60b 100644 --- a/desktop/src/renderer/src/store/uiStore.ts +++ b/desktop/src/renderer/src/store/uiStore.ts @@ -12,6 +12,7 @@ interface UIState { /** Session list panel collapsed (hidden) vs expanded. */ sessionsCollapsed: boolean toggleSessions: () => void + setSessionsCollapsed: (v: boolean) => void /** Currently active session id (Chat page). */ activeSessionId: string | null @@ -42,6 +43,10 @@ export const useUIStore = create((set) => ({ localStorage.setItem(SESSIONS_KEY, next ? '1' : '0') return { sessionsCollapsed: next } }), + setSessionsCollapsed: (v) => { + localStorage.setItem(SESSIONS_KEY, v ? '1' : '0') + set({ sessionsCollapsed: v }) + }, activeSessionId: null, setActiveSessionId: (id) => set({ activeSessionId: id }), diff --git a/desktop/src/renderer/src/types.ts b/desktop/src/renderer/src/types.ts index 1fa7592d..c3a02d69 100644 --- a/desktop/src/renderer/src/types.ts +++ b/desktop/src/renderer/src/types.ts @@ -50,7 +50,7 @@ export interface BackendStatusEvent { // Chat / messages / streaming // ============================================================ -export type Role = 'user' | 'assistant' +export type Role = 'user' | 'assistant' | 'system' /** A single ordered step inside an assistant turn (matches backend history). */ export interface MessageStep { @@ -83,8 +83,8 @@ export interface ChatMessage { /** Sequence numbers from backend (for delete/regenerate). */ userSeq?: number botSeq?: number - /** Self-evolution bubble flag. */ - kind?: 'evolution' + /** Self-evolution bubble flag; 'divider' renders a context-cleared separator. */ + kind?: 'evolution' | 'divider' extras?: Record isStreaming?: boolean isCancelled?: boolean