Skip to content

Commit 190bffd

Browse files
committed
Fix UI mode sync to respect manual top-bar toggles and make cron tab always visible
- Change Layout mode sync effect to only trigger on session switch, not on every mode change, preventing override of manual top-bar mode toggles - Remove `mode` from Layout useEffect dependency array to break reactive loop - Remove conditional cron tab visibility logic in RightPanel (tab now always visible regardless of job count) - Add console.log to TopBar mode switch click handler for debugging - Add `pluginId` and `pluginC
1 parent 1800050 commit 190bffd

9 files changed

Lines changed: 77 additions & 15 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
All notable changes to **OpenCowork** will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/).
55

6+
## [0.2.0] - 2026-02-21
7+
8+
### Fixed
9+
- **Feishu DM title mismatch** — added `getUserProfile` and cached user display names inside the Feishu service so P2P sessions fall back to the sender's real name whenever `chatName` is missing, keeping private chat titles aligned with Feishu.
10+
611
## [0.1.9] - 2026-02-20
712

813
### Fixed

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "open-cowork",
3-
"version": "0.1.9",
3+
"version": "0.2.0",
44
"description": "An Electron application with React and TypeScript",
55
"main": "./out/main/index.js",
66
"author": "example.com",

‎src/main/plugins/providers/feishu/feishu-api.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,30 @@ export class FeishuApi {
172172
}
173173
}
174174

175+
/** Get user profile info (name) by ID */
176+
async getUserProfile(
177+
userId: string,
178+
idType: 'open_id' | 'user_id' | 'union_id' = 'open_id'
179+
): Promise<{ name: string } | null> {
180+
if (!userId) return null
181+
try {
182+
const headers = await this.authHeaders()
183+
const encodedId = encodeURIComponent(userId)
184+
const res = await request(
185+
'GET',
186+
`/open-apis/contact/v3/users/${encodedId}?user_id_type=${idType}`,
187+
headers
188+
)
189+
const data = JSON.parse(res.body)
190+
if (data.code !== 0) return null
191+
return {
192+
name: data.data?.user?.name ?? '',
193+
}
194+
} catch {
195+
return null
196+
}
197+
}
198+
175199
/** List chats/groups the bot is in */
176200
async listChats(): Promise<
177201
Array<{ chat_id: string; name: string; member_count?: number; raw: unknown }>

‎src/main/plugins/providers/feishu/feishu-service.ts‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ export class FeishuService implements MessagingPluginService {
6363
private _processedMsgIds = new Set<string>()
6464
/** Cache: chat_id → chat name */
6565
private _chatNameCache = new Map<string, string>()
66+
/** Cache: user_id/open_id → user display name */
67+
private _userNameCache = new Map<string, string>()
6668
/** Bot's own open_id — fetched once at startup for reliable @mention detection */
6769
private _botOpenId = ''
6870

@@ -118,7 +120,11 @@ export class FeishuService implements MessagingPluginService {
118120
const messageId = msg.message?.message_id ?? ''
119121
const msgType = msg.message?.message_type ?? 'text'
120122
const chatType = (msg.message?.chat_type ?? 'p2p') as 'p2p' | 'group'
121-
const senderId = msg.sender?.sender_id?.open_id ?? msg.sender?.sender_id?.user_id ?? ''
123+
const senderIds = msg.sender?.sender_id ?? {}
124+
const senderOpenId = senderIds.open_id ?? ''
125+
const senderUserId = senderIds.user_id ?? ''
126+
const senderIdType: 'open_id' | 'user_id' = senderOpenId ? 'open_id' : 'user_id'
127+
const senderId = senderOpenId || senderUserId || ''
122128
const mentions = msg.message?.mentions ?? []
123129

124130
// Group chat filter: only respond when the bot is @mentioned
@@ -180,6 +186,16 @@ export class FeishuService implements MessagingPluginService {
180186

181187
console.log(`[Feishu] ${msgType} [${chatType}] in ${chatId}: ${content.slice(0, 60)}${images ? ` [+${images.length} image(s)]` : ''}`)
182188

189+
// Resolve sender display name (cached) so P2P chats have proper titles
190+
let senderName = senderId ? this._userNameCache.get(senderId) ?? '' : ''
191+
if (!senderName && senderId) {
192+
try {
193+
const profile = await this.api.getUserProfile(senderId, senderIdType)
194+
senderName = profile?.name?.trim() ?? ''
195+
if (senderName) this._userNameCache.set(senderId, senderName)
196+
} catch { /* ignore */ }
197+
}
198+
183199
// Resolve chat name (cached)
184200
let chatName = this._chatNameCache.get(chatId)
185201
if (!chatName) {
@@ -190,14 +206,18 @@ export class FeishuService implements MessagingPluginService {
190206
} catch { /* ignore */ }
191207
}
192208

209+
if (chatType === 'p2p' && !chatName && senderName) {
210+
chatName = senderName
211+
}
212+
193213
this._notify({
194214
type: 'incoming_message',
195215
pluginId: this.pluginId,
196216
pluginType: this.pluginType,
197217
data: {
198218
chatId,
199219
senderId,
200-
senderName: '',
220+
senderName: senderName || senderId,
201221
content,
202222
messageId,
203223
images,

‎src/renderer/src/components/layout/Layout.tsx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,14 @@ export function Layout(): React.JSX.Element {
8080
document.title = `${prefix}${base}`
8181
}, [activeSessionTitle, pendingToolCalls.length, streamingMessageId, runningSubAgents])
8282

83-
// Sync UI mode when switching to a session with a different mode
83+
// Sync UI mode only when session info changes, so manual top-bar toggles are respected
8484
useEffect(() => {
85-
if (activeSessionMode && activeSessionMode !== mode) {
85+
if (!activeSessionMode) return
86+
const currentMode = useUIStore.getState().mode
87+
if (currentMode !== activeSessionMode) {
8688
useUIStore.getState().setMode(activeSessionMode)
8789
}
88-
}, [activeSessionId, activeSessionMode, mode])
90+
}, [activeSessionId, activeSessionMode])
8991

9092
// Close detail/preview panels when switching sessions (they are session-specific)
9193
const prevActiveSessionRef = useRef<string | null>(null)

‎src/renderer/src/components/layout/RightPanel.tsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ export function RightPanel({ compact = false }: { compact?: boolean }): React.JS
6464
const visibleTabs = tabDefs
6565
.filter((t) => teamToolsEnabled || t.value !== 'team')
6666
.filter((t) => (hasPlan || planMode) || t.value !== 'plan')
67-
.filter((t) => t.value !== 'cron' || cronJobCount > 0 || tab === 'cron')
68-
// Note: cronJobCount includes ALL jobs (enabled + disabled) so the tab stays visible
67+
// Cron tab is always visible
6968

7069
const badgeCounts: Partial<Record<RightPanelTab, number>> = {
7170
steps: todos.length,

‎src/renderer/src/components/layout/TopBar.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export function TopBar(): React.JSX.Element {
8686
? 'bg-background shadow-sm ring-1 ring-border/50'
8787
: 'text-muted-foreground hover:text-foreground'
8888
)}
89-
onClick={() => setMode(m.value)}
89+
onClick={() => { console.log('[TopBar] Mode switch clicked:', m.value); setMode(m.value) }}
9090
>
9191
{m.icon}
9292
{tCommon(m.labelKey)}

‎src/renderer/src/lib/tools/cron-tool.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ const cronAddHandler: ToolHandler = {
2727
' - "daily at 9am" → { kind: "cron", expr: "0 9 * * *" }\n' +
2828
' - "every 15 min" → { kind: "cron", expr: "*/15 * * * *" }\n' +
2929
' - "weekdays at 6pm" → { kind: "cron", expr: "0 18 * * 1-5" }\n\n' +
30-
'IMPORTANT: For "in X minutes/hours" requests, ALWAYS use kind="at" with relative offset like "+10m". NEVER use ISO timestamps.',
30+
'IMPORTANT: For "in X minutes/hours" requests, ALWAYS use kind="at" with relative offset like "+10m". NEVER use ISO timestamps.\n\n' +
31+
'Delivery: by default, CronAgent will Notify on desktop. To force delivery through a messaging plugin (Feishu, WhatsApp, etc.), provide pluginId + pluginChatId explicitly when creating the job.',
3132
inputSchema: {
3233
type: 'object',
3334
properties: {
@@ -87,6 +88,14 @@ const cronAddHandler: ToolHandler = {
8788
type: 'number',
8889
description: 'Max agent loop iterations. Default: 15.',
8990
},
91+
pluginId: {
92+
type: 'string',
93+
description: 'Optional messaging plugin ID to deliver the results through (e.g. cron reminders to WhatsApp).',
94+
},
95+
pluginChatId: {
96+
type: 'string',
97+
description: 'Chat/channel ID for the messaging plugin. Required when pluginId is provided.',
98+
},
9099
},
91100
required: ['name', 'schedule', 'prompt'],
92101
},
@@ -127,6 +136,9 @@ const cronAddHandler: ToolHandler = {
127136
}
128137
}
129138

139+
const pluginId = input.pluginId ? String(input.pluginId) : ctx.pluginId
140+
const pluginChatId = input.pluginChatId ? String(input.pluginChatId) : ctx.pluginChatId
141+
130142
const result = await ctx.ipc.invoke(IPC.CRON_ADD, {
131143
name,
132144
sessionId: ctx.sessionId ?? null,
@@ -139,9 +151,9 @@ const cronAddHandler: ToolHandler = {
139151
deliveryTarget: input.deliveryTarget ? String(input.deliveryTarget) : ctx.sessionId,
140152
deleteAfterRun: input.deleteAfterRun,
141153
maxIterations: input.maxIterations,
142-
// Auto-inject plugin channel info from context so cron can reply through the same plugin
143-
pluginId: ctx.pluginId,
144-
pluginChatId: ctx.pluginChatId,
154+
// Allow explicit overrides, falling back to current plugin context when available
155+
pluginId: pluginId ?? undefined,
156+
pluginChatId: pluginChatId ?? undefined,
145157
}) as { error?: string; jobId?: string; success?: boolean }
146158

147159
if (result.error) return JSON.stringify({ error: result.error })

0 commit comments

Comments
 (0)