Repository navigation
feat(assistant): coordinate work through a durable inbox bot #6124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
aae6420
712d531
b4ce59d
f1a5fcb
d6ff911
619d936
01546d7
5bb4956
bac660d
788bbb1
e781f3f
af03f0d
9cba5c2
db11126
f471bce
c59ff5a
df549e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,7 +99,7 @@ export function registerBrowserIpc(deps: BrowserIpcDeps): BrowserIpcController { | |
| const owner = ownerForSession(sessionId); | ||
| if (!owner || !browserParentForRenderer(owner)) return false; | ||
| const ref = parseDesktopSessionResourceKey(sessionId); | ||
| return isWorkHubCoordinationSessionId(ref.sessionId) && deps.isHostActive(ref); | ||
| return deps.isHostActive(ref); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: With hidden ordinary Sessions now allowed to act, the previous Session keeps this access until the renderer's next |
||
| }; | ||
| const revokeHiddenActions = (): void => { | ||
| for (const sessionId of views.sessionIds()) views.get(sessionId)?.refreshRendering(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1305,6 +1305,7 @@ function AppShellContent({ | |
| onOpenWorkHub={openWorkHub} onOpenSession={(sessionId) => { closeSettings(); openSession(sessionId); }} /> | ||
| <WorkHubDock workbar={selectors} workbarTogglePosition={workbarTogglePosition} visible={workHubActive && sessionsSelected && !shellObscured} /> | ||
| <ChatSurfaceLayout | ||
| className={activeSessionForView?.labels?.includes('personal-assistant') ? 'personal-assistant-chat' : undefined} | ||
| data-session-history-surface="true" | ||
| // ChatView positions this transcript: switching conversations, | ||
| // following the tail and the moves the reader asks for are one | ||
|
|
@@ -1364,6 +1365,9 @@ function AppShellContent({ | |
| contextPickEnabled={canStageComposerContext} | ||
| executorComposer={{ selection: executor, taskSubmissionHardBlocked, connectionCount: connections.length, onSetup: () => openSettingsSection('external-agents'), onNewTask: openNewTaskSurface }} | ||
| activeSession={activeSessionForView} | ||
| compactControls={activeSessionForView?.labels?.includes('personal-assistant')} | ||
| placeholder={activeSessionForView?.labels?.includes('personal-assistant') | ||
| ? (uiLocale === 'zh-CN' ? '聊点什么…' : 'Send a message…') : undefined} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The inline locale ternary sends zh-TW to English and grows app-shell.tsx tokens, which the architecture ratchet rejects. Use a catalog entry owned by the feature. |
||
| {...{ executorTarget, onExecutorTargetChange }} | ||
| usageModel={activeModel} | ||
| usageRoute={activeSessionForModelControls} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,8 @@ import { | |
| useUiLocale, | ||
| type LiveTurnProjection, | ||
| } from '@maka/ui'; | ||
| import { PersonalAssistantTyping } from './features/personal-assistant/typing.js'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 (check:architecture): Import this through |
||
| import { PersonalAssistantAvatar, useAssistantAvatar } from './features/personal-assistant/index.js'; | ||
| import { OnboardingHero } from './onboarding-hero'; | ||
| import type { SessionHealthNoticeView, SessionUiReads } from './features/conversation/index.js'; | ||
| import type { WorkspaceReadinessRecovery } from './workspace-readiness-recovery'; | ||
|
|
@@ -124,6 +126,9 @@ export function ChatMessageSurface({ | |
| onSkip, | ||
| ...chatViewRest | ||
| }: ChatMessageSurfaceProps) { | ||
| const isAssistant = chatViewRest.activeSession?.labels?.includes('personal-assistant') === true; | ||
| const avatar = useAssistantAvatar(isAssistant, activeSessionId); | ||
| const assistantAvatar = useMemo(() => isAssistant ? <PersonalAssistantAvatar avatar={avatar} /> : undefined, [isAssistant, avatar]); | ||
| const locale = useUiLocale(); | ||
| const copy = getShellCopy(locale).app; | ||
| // Configuration notices share the Settings label; identity recovery supplies | ||
|
|
@@ -204,6 +209,8 @@ export function ChatMessageSurface({ | |
| {(goalProjection) => ( | ||
| <StagedQuoteChatView | ||
| {...chatViewRest} | ||
| assistantAvatar={assistantAvatar} | ||
| activityContent={isAssistant && visible && activeSessionId ? <PersonalAssistantTyping key={activeSessionId} sessionId={activeSessionId} avatar={assistantAvatar} /> : undefined} | ||
| liveTurns={seededLiveTurns} | ||
| // Every branch above reseeds `sessionId` to `activeSessionId`, and a | ||
| // render-phase setState re-runs this body before anything commits, so | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
|
|
||
| import { useEffect, useState } from 'react'; | ||
| import { DEFAULT_ASSISTANT_AVATAR, assistantAvatarSchema, type AssistantAvatar } from '@maka/runtime-host/protocol'; | ||
| import { usePersonalAssistantServices } from './services.js'; | ||
| import { originalHost } from './navigation.js'; | ||
|
|
||
| type AvatarView = AssistantAvatar & { imageUrl?: string }; | ||
|
|
||
| export function useAssistantAvatar(enabled: boolean, sessionId?: string) { | ||
| const services = usePersonalAssistantServices(); | ||
| const [state, setState] = useState<{ scope?: string; avatar: AvatarView }>({ avatar: DEFAULT_ASSISTANT_AVATAR }); | ||
| useEffect(() => { | ||
| if (!enabled) return; | ||
| let live = true; | ||
| let cachedImage: { id: string; url: string } | undefined; | ||
| let timer: ReturnType<typeof setTimeout>; | ||
| const refresh = async () => { | ||
| try { | ||
| const host = sessionId | ||
| ? originalHost(sessionId, (await services.hostSnapshot()).entries).host | ||
| : await services.defaultHost(); | ||
| const result = await services.call('assistant.avatar', {}, host); | ||
| const avatar: AvatarView = assistantAvatarSchema.parse(result.avatar); | ||
| if (avatar.image) { | ||
| if (cachedImage?.id !== avatar.image.id) { | ||
| let data = ''; | ||
| while (live && data.length < avatar.image.length) { | ||
| const chunk = await services.call('assistant.avatar-image', { id: avatar.image.id, offset: data.length }, host); | ||
| if (chunk.id !== avatar.image.id || chunk.offset !== data.length || typeof chunk.data !== 'string' | ||
| || !chunk.data.length || data.length + chunk.data.length > avatar.image.length) throw Error('Invalid avatar image'); | ||
| data += chunk.data; | ||
| } | ||
| if (!live) return; | ||
| cachedImage = { id: avatar.image.id, url: `data:${avatar.image.mimeType};base64,${data}` }; | ||
| } | ||
| avatar.imageUrl = cachedImage.url; | ||
| } else cachedImage = undefined; | ||
| if (live) setState((old) => old.scope === sessionId && old.avatar.emoji === avatar.emoji && old.avatar.background === avatar.background && old.avatar.imageUrl === avatar.imageUrl | ||
| ? old : { scope: sessionId, avatar }); | ||
| } catch { | ||
| // A disconnected Host must not display another Host's identity. | ||
| if (live) setState({ scope: sessionId, avatar: DEFAULT_ASSISTANT_AVATAR }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: On Host failure this sets a fresh object every 3 s. That changes the |
||
| } finally { | ||
| if (live) timer = setTimeout(refresh, 3000); | ||
| } | ||
| }; | ||
| void refresh(); | ||
| return () => { live = false; clearTimeout(timer); }; | ||
| }, [enabled, sessionId, services]); | ||
| return enabled && state.scope === sessionId ? state.avatar : DEFAULT_ASSISTANT_AVATAR; | ||
| } | ||
|
|
||
| export function PersonalAssistantAvatar({ avatar }: { avatar: AvatarView }) { | ||
| return <span className="personal-assistant-avatar" role="img" aria-label="个人助手头像" | ||
| style={{ backgroundColor: avatar.background }}> | ||
| {avatar.imageUrl ? <img src={avatar.imageUrl} alt="" draggable={false} /> : avatar.emoji} | ||
| </span>; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Preserve the ordinary-Session browser visibility gate
Removing the coordination-Session predicate grants every selected ordinary Session a background action lease. A renderer can remain selected while its parent view or native window is hidden/minimized.
createBrowserViewHost.canDrivethen returns true before checking visibility/viewport, andrevokeHiddenActionsno longer severs its running action. An agent can therefore read or mutate an authenticated page that the user cannot see, contrary to the explicit security policy in browser/logic.ts:99–107.Using the registered IPC and actual
withBrowserPagepath with the existing native/CDP fixture, I observed all three action callbacks execute with the ordinary owner hidden, its window hidden, and its window minimized. Loading only the actual d6ff911 IPC module blocks all nine with BrowserActionBlockedError. Collapsing the panel also newly admits mutation without a viewport. The changed test now expects these hidden actions to succeed and removes the revocation assertion.Keep background access restricted to WorkHub coordination as before, or explicitly revoke ordinary-Session access when the view/window is hidden, minimized or the panel is collapsed. Retain negative tests for hidden reads, mutations and in-flight revocation; selection/Host identity alone is not an equivalent security boundary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The feature owner has explicitly confirmed that background browser operation includes a hidden or minimized Maka window, as well as a collapsed browser panel. This does not exceed the requested product scope; the changed admission expectations are intentional. We will retain this capability rather than restore visibility-based denial. This does not authorize arbitrary unselected sessions: selected-session ownership, renderer lifetime, active Host and epoch checks still apply. The old visibility-only policy comment needs to be reconciled with this intentional policy change; it should not be treated as evidence that the requested behavior itself is accidental.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Astro-Han Browser operation while the Maka window is hidden or minimized, or the browser panel is collapsed, is intended product behavior. Please re-review the browser finding on that basis, including whether the remaining selected-session ownership, renderer lifetime, active Host and epoch checks are sufficient for this intended capability. This clarification does not claim that the separate Inbox admission race or architecture-check failures have been fixed.