-
Notifications
You must be signed in to change notification settings - Fork 105
fix(chat): restore artifact open actions ("Open in editor") broken by provider scoping #267
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
base: main
Are you sure you want to change the base?
Changes from all commits
4aed3cd
175107c
ab6b861
1244c8b
90cfe94
dcf8b24
3dd1684
3887555
5f1a940
2eba344
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 |
|---|---|---|
| @@ -1 +1 @@ | ||
| {"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-toggle-maximize","core:window:allow-show","core:window:allow-close","core:window:allow-set-size","core:window:allow-set-min-size","deep-link:default","opener:default",{"identifier":"opener:allow-open-url","allow":[{"url":"linear://*"},{"url":"https://linear.app/*"}]},{"identifier":"opener:allow-open-path","allow":[{"path":"$HOME/**"},{"path":"$HOME/.goose/**"},{"path":"$TEMP/**"},{"path":"/Volumes/**"},{"path":"/mnt/**"},{"path":"/workspace/**"},{"path":"/workspaces/**"},{"path":"/opt/**"},{"path":"/srv/**"},{"path":"*:/**"}]},"window-state:allow-restore-state","window-state:allow-save-window-state","dialog:allow-open","dialog:allow-save","clipboard-manager:allow-write-text","app-test-driver:default","berdctl:default","updater:default","process:allow-restart","notification:default"]},"session-window":{"identifier":"session-window","description":"Capability for popped-out chat session windows","local":true,"windows":["session:*"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-show","core:window:allow-close","core:window:allow-set-focus","core:window:allow-set-size","core:window:allow-set-min-size","opener:default",{"identifier":"opener:allow-open-url","allow":[{"url":"linear://*"},{"url":"https://linear.app/*"}]},{"identifier":"opener:allow-open-path","allow":[{"path":"$DOWNLOAD"}]},"dialog:allow-open","dialog:allow-save","berdctl:allow-status"],"platforms":["macOS"]},"voice-buddy":{"identifier":"voice-buddy","description":"Capability for the always-on-top voice conversation buddy","local":true,"windows":["voice-buddy"],"permissions":["core:default","core:window:allow-start-dragging"]}} | ||
| {"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-toggle-maximize","core:window:allow-show","core:window:allow-close","core:window:allow-set-size","core:window:allow-set-min-size","deep-link:default","opener:default",{"identifier":"opener:allow-open-url","allow":[{"url":"linear://*"},{"url":"https://linear.app/*"}]},{"identifier":"opener:allow-open-path","allow":[{"path":"$HOME/**"},{"path":"$HOME/.goose/**"},{"path":"$TEMP/**"},{"path":"/Volumes/**"},{"path":"/mnt/**"},{"path":"/workspace/**"},{"path":"/workspaces/**"},{"path":"/opt/**"},{"path":"/srv/**"},{"path":"*:/**"}]},"window-state:allow-restore-state","window-state:allow-save-window-state","dialog:allow-open","dialog:allow-save","clipboard-manager:allow-write-text","app-test-driver:default","berdctl:default","updater:default","process:allow-restart","notification:default"]},"session-window":{"identifier":"session-window","description":"Capability for popped-out chat session windows","local":true,"windows":["session:*"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-show","core:window:allow-close","core:window:allow-set-focus","core:window:allow-set-size","core:window:allow-set-min-size","opener:default",{"identifier":"opener:allow-open-url","allow":[{"url":"linear://*"},{"url":"https://linear.app/*"}]},{"identifier":"opener:allow-open-path","allow":[{"path":"$HOME/**"},{"path":"$HOME/.goose/**"},{"path":"$TEMP/**"},{"path":"/Volumes/**"},{"path":"/mnt/**"},{"path":"/workspace/**"},{"path":"/workspaces/**"},{"path":"/opt/**"},{"path":"/srv/**"},{"path":"*:/**"}]},"dialog:allow-open","dialog:allow-save","berdctl:allow-status"],"platforms":["macOS"]},"voice-buddy":{"identifier":"voice-buddy","description":"Capability for the always-on-top voice conversation buddy","local":true,"windows":["voice-buddy"],"permissions":["core:default","core:window:allow-start-dragging"]}} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ import { | |
| CONVERSATION_MIN_WIDTH_WITH_VIEWER, | ||
| } from "./ArtifactViewerPanel"; | ||
| import { useOpenArtifact } from "../stores/artifactViewerStore"; | ||
| import { ArtifactPolicyProvider } from "../hooks/ArtifactPolicyContext"; | ||
| import { ArtifactAutoOpenMount } from "./ArtifactAutoOpenMount"; | ||
| import { | ||
| CP_TOTAL_W, | ||
|
|
@@ -124,7 +125,6 @@ export function ChatView({ | |
| }: ChatViewProps) { | ||
| const { t } = useTranslation("chat"); | ||
| useRegisterSecurityConfirmationSurface(sessionId); | ||
| const isArtifactViewerOpen = useOpenArtifact(sessionId) !== null; | ||
| const mountStart = useRef(performance.now()); | ||
| const terminalRootRef = useRef<HTMLDivElement | null>(null); | ||
| const chatColumnRef = useRef<HTMLDivElement | null>(null); | ||
|
|
@@ -201,6 +201,14 @@ export function ChatView({ | |
| ]); | ||
| const workspaceRepository = useWorkspaceRepository(); | ||
| const effectiveSession = controller.session ?? activeSession ?? null; | ||
| // The effective session identity: during session replacement or | ||
| // reconciliation the requested sessionId can briefly disagree with the | ||
| // snapshot the controller serves. Every artifact-store read/write and | ||
| // every layout decision derived from viewer state must use THIS id, so | ||
| // the panel, the policy provider, and the width math all describe the | ||
| // same store entry. (Audited: all useOpenArtifact call sites in ChatView.) | ||
| const timelineSessionId = effectiveSession?.id ?? sessionId; | ||
| const isArtifactViewerOpen = useOpenArtifact(timelineSessionId) !== null; | ||
| const isReadOnly = Boolean(readOnlyStatus); | ||
| // A remote session's cwd and artifact paths live on its SSH host: the | ||
| // in-chat terminal (a local PTY), local folder pickers, and local file | ||
|
|
@@ -728,7 +736,6 @@ export function ChatView({ | |
| </div> | ||
| ); | ||
|
|
||
| const timelineSessionId = effectiveSession?.id ?? sessionId; | ||
| const messageTimeline = ( | ||
| <ChatTranscriptSurface | ||
| sessionId={timelineSessionId} | ||
|
|
@@ -793,11 +800,28 @@ export function ChatView({ | |
| }); | ||
|
|
||
| return ( | ||
| <> | ||
| // The single artifact policy owner for this session boundary. It must | ||
| // wrap the whole chat row because the transcript AND its siblings | ||
| // consume the context: ArtifactViewerPanel ("Open in editor"), the | ||
| // right rail's ArtifactsWidget (row opens), and ArtifactAutoOpenMount | ||
| // (the artifact list). ChatTranscriptSurface intentionally does NOT own | ||
| // artifact policy — a nested provider would derive a second artifact | ||
| // inventory and split the per-path open debounce (enforced by the | ||
| // provider-boundary tests). Consumers outside the provider get the | ||
| // inert default context and silently no-op. | ||
| // The identity, messages, and cwd must describe one session snapshot: | ||
| // timelineSessionId (the controller's effective session) rather than the | ||
| // raw requested sessionId, which can briefly disagree with | ||
| // controller.messages during session replacement or reconciliation. | ||
| <ArtifactPolicyProvider | ||
|
Collaborator
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 · Provider state crosses session replacement (non-blocking) The moved ArtifactPolicyProvider receives timelineSessionId, but React reuses the same provider instance when that prop changes. Provider-local refs, including the per-path open debounce map, therefore survive when reconciliation replaces the effective session without unmounting ChatView. A path opened in the previous session remains temporarily marked as recently opened in the replacement session. User effect: If someone opens the same file path immediately after a chat session is replaced or reconciled, the first open in the new session can appear to do nothing because it is suppressed by the previous session's debounce state. Recommended fix: Key the ArtifactPolicyProvider by timelineSessionId, or explicitly reset all session-scoped provider state when the session identity changes, so each effective session starts with an independent artifact-action lifecycle. Test: Add a reconciliation test that opens a path, changes the effective session identity without unmounting ChatView, and verifies that opening the same path in the replacement session reaches the OS boundary immediately. |
||
| messages={controller.messages} | ||
| sessionCwd={controller.sessionArtifactCwd} | ||
| sessionId={timelineSessionId} | ||
| > | ||
| <ArtifactAutoOpenMount | ||
| // Remote artifacts cannot be read locally, so never auto-open the | ||
| // viewer for them; a null session absorbs appearances silently. | ||
| sessionId={sessionIsRemote ? null : sessionId} | ||
| sessionId={sessionIsRemote ? null : timelineSessionId} | ||
| isHistoryLoading={controller.isLoadingHistory} | ||
| sessionCwd={controller.sessionArtifactCwd} | ||
| /> | ||
|
|
@@ -943,7 +967,9 @@ export function ChatView({ | |
| </div> | ||
|
|
||
| {sessionId && !isAgentBuilderSession ? ( | ||
| <ArtifactViewerPanel sessionId={sessionId} /> | ||
| // Keyed by the same effective identity the providers use, so the | ||
| // panel reads the viewer-store entry that openInApp writes. | ||
| <ArtifactViewerPanel sessionId={timelineSessionId} /> | ||
|
Collaborator
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. 🤖 P1 · Viewer layout reads stale session (blocking) ArtifactViewerPanel now correctly reads timelineSessionId, but isArtifactViewerOpen is still derived near the top of ChatView with useOpenArtifact(sessionId). During the reconciliation state this follow-up explicitly supports, the panel and the layout calculation therefore read different artifact-store entries. Width allocation and compact-mode decisions can disagree with the viewer actually rendered. User effect: While a chat is being restored or reconciled, an open artifact viewer can appear without reserved space and overlap or compress the chat, while a stale viewer can leave an empty reserved column. Recommended fix: Derive the effective timeline session identity before the viewer-store lookup and use it consistently for useOpenArtifact, ArtifactViewerPanel, and every layout calculation driven by viewer visibility. Test: Extend the reconciliation test so requested and effective session IDs differ, seed viewer state only for the effective session, and assert that the rendered chat layout enters its viewer-open configuration; also verify stale requested-session viewer state does not reserve space. |
||
| ) : null} | ||
|
|
||
| <ChatRightRail | ||
|
|
@@ -979,6 +1005,6 @@ export function ChatView({ | |
| } | ||
| /> | ||
| </div> | ||
| </> | ||
| </ArtifactPolicyProvider> | ||
| ); | ||
| } | ||
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.
🤖 P2 · Artifact policy has duplicate owners (non-blocking)
The new ChatView provider wraps a ChatTranscriptSurface that still creates its own provider, and ChatCanvasCard similarly gives its transcript and composer separate providers. Each provider independently derives artifact inventory and owns its own per-path open debounce. The same visible session can therefore process different message projections and bypass duplicate-open suppression when actions originate from different surfaces.
User effect: Opening the same artifact quickly from two parts of one chat can hand it to the operating system twice, and artifact availability can drift between the transcript and adjacent viewer, rail, or composer surfaces.
Recommended fix: Mount one ArtifactPolicyProvider per rendered session boundary, placing transcript, composer, viewer, auto-open mount, and artifact widgets beneath it. Keep standalone transcript support through an explicit wrapper rather than implicit nested ownership.
Test: Add a provider-boundary test proving one policy owner covers all full-chat consumers and one covers both canvas transcript and composer; verify opening one path across two consumer surfaces shares the same debounce lifecycle.