From 696f3ac7d541dbb0e286c18e5b6c8bb926445063 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 06:23:31 -0800 Subject: [PATCH 1/3] feat(connectors): Linear's drawer says older connections reconnect to write Carries #2641's notice onto the current Connectors page. When Linear has a connection, its drawer shows a note above the grant list: a connection made before write access was added must be removed and connected again before it can write, and every write asks first. It sits in Linear's drawer only, granted or not. zh-CN reworks #2641's string as a conditional clause rather than a long modifier before the noun. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- frontend/src/i18n/locales/en.json | 1 + frontend/src/i18n/locales/zh-CN.json | 1 + .../__tests__/ConnectorsPage.tools.test.tsx | 22 +++++++++++++++++++ frontend/src/v2/connectors/ConnectorsPage.tsx | 5 +++++ 4 files changed, 29 insertions(+) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 511ca246f..1b7940a2f 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2594,6 +2594,7 @@ "loadFailed": "Could not load artifacts." }, "tools": { + "linearWriteReconnect": "Linear connections made before write access was added need to be removed and connected again to enable writes. Each write requires your approval.", "granted": "Granted", "beta": "beta", "adminOnce": "An admin installs this once", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index ae5d3ff64..49c222b7a 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2597,6 +2597,7 @@ "loadFailed": "无法加载产物。" }, "tools": { + "linearWriteReconnect": "如果你的 Linear 是在支持写入之前连接的,需要先移除再重新连接才能写入。每次写入都需要你批准。", "granted": "已授权", "beta": "测试版", "adminOnce": "需管理员安装一次", diff --git a/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx b/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx index d5fb519d7..0d780f18e 100644 --- a/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx +++ b/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx @@ -1393,3 +1393,25 @@ describe('Connectors page: tools', () => { }); }); }); + +describe('Linear write reconnect note', () => { + afterEach(async () => { await act(async () => { await i18n.changeLanguage('en'); }); }); + + it.each(['en', 'zh-CN'])('a connected Linear drawer says older connections must reconnect to write, in %s', async (language) => { + await i18nReady; + await act(async () => { await i18n.changeLanguage(language); }); + mockTools({ entries: [githubEntry, linearEntry], grants: [grantLinear] }); + renderPage(); + const drawer = await openTool('Linear'); + const note = within(drawer).getByRole('note'); + expect(note).toHaveTextContent(language === 'en' ? 'removed and connected again to enable writes' : '先移除再重新连接'); + expect(note).toHaveTextContent(language === 'en' ? 'Each write requires your approval' : '每次写入都需要你批准'); + }); + + it('no other drawer carries it', async () => { + mockTools({ entries: [githubEntry, linearEntry], grants: [grantLive] }); + renderPage(); + const drawer = await openTool('GitHub'); + expect(within(drawer).queryByRole('note')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/v2/connectors/ConnectorsPage.tsx b/frontend/src/v2/connectors/ConnectorsPage.tsx index b00075a32..8c5314e90 100644 --- a/frontend/src/v2/connectors/ConnectorsPage.tsx +++ b/frontend/src/v2/connectors/ConnectorsPage.tsx @@ -463,6 +463,11 @@ const ConnectorsPage: React.FC = () => { const deadGrants = entryGrants.filter((grant) => isDead(grant, m.now)); return ( + {/* A Linear connection made before write access cannot write until it is + removed and connected again; the server refuses writes on it. */} + {toolEntry.connectionType === 'hosted-mcp' && toolEntry.entryId === 'linear' && toolEntry.connections.length > 0 && ( +

{t('tools.linearWriteReconnect')}

+ )} {!toolGranted(toolEntry) && (

{catalogDescription(i18n, toolEntry.installableId, toolEntry.description)}

From 0cb80599692a0fda13235a758f6830cf671d92a9 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 06:26:57 -0800 Subject: [PATCH 2/3] fix(connectors): the Linear write note reaches the popover, and says Disconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fixes: - A connected Linear with no grant opens a popover, not the drawer, so that user never saw the note right before choosing a write mode. The note now renders through renderEntryNote, which covers the popover and the drawer's ungranted offer. The drawer adds it directly only when the entry is granted, so it never shows twice. - The control for a hosted connection is Disconnect / 断开连接, so the copy says "disconnected" (断开) rather than "removed" (移除). The subject in zh-CN is now 你, not "your Linear". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- frontend/src/i18n/locales/en.json | 2 +- frontend/src/i18n/locales/zh-CN.json | 2 +- .../v2/__tests__/ConnectorsPage.tools.test.tsx | 18 +++++++++++++++++- frontend/src/v2/connectors/ConnectorsPage.tsx | 7 ++----- frontend/src/v2/connectors/ToolPanels.tsx | 11 ++++++++++- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 1b7940a2f..18d36d4a2 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2594,7 +2594,7 @@ "loadFailed": "Could not load artifacts." }, "tools": { - "linearWriteReconnect": "Linear connections made before write access was added need to be removed and connected again to enable writes. Each write requires your approval.", + "linearWriteReconnect": "Linear connections made before write access was added need to be disconnected and connected again to enable writes. Each write requires your approval.", "granted": "Granted", "beta": "beta", "adminOnce": "An admin installs this once", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 49c222b7a..2f61ff121 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2597,7 +2597,7 @@ "loadFailed": "无法加载产物。" }, "tools": { - "linearWriteReconnect": "如果你的 Linear 是在支持写入之前连接的,需要先移除再重新连接才能写入。每次写入都需要你批准。", + "linearWriteReconnect": "如果你是在支持写入之前连接的 Linear,需要先断开再重新连接,才能写入。每次写入都需要你批准。", "granted": "已授权", "beta": "测试版", "adminOnce": "需管理员安装一次", diff --git a/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx b/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx index 0d780f18e..fd4efc9bb 100644 --- a/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx +++ b/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx @@ -1404,10 +1404,26 @@ describe('Linear write reconnect note', () => { renderPage(); const drawer = await openTool('Linear'); const note = within(drawer).getByRole('note'); - expect(note).toHaveTextContent(language === 'en' ? 'removed and connected again to enable writes' : '先移除再重新连接'); + expect(note).toHaveTextContent(language === 'en' ? 'disconnected and connected again to enable writes' : '先断开再重新连接'); expect(note).toHaveTextContent(language === 'en' ? 'Each write requires your approval' : '每次写入都需要你批准'); }); + it('a connected Linear with no grant shows it once in the popover, before Add', async () => { + mockTools({ entries: [githubEntry, linearEntry], grants: [] }); + renderPage(); + const pop = await openToolPopover('Linear'); + expect(within(pop).getAllByRole('note')).toHaveLength(1); + expect(within(pop).getByRole('note')).toHaveTextContent('disconnected and connected again to enable writes'); + }); + + it('a Linear drawer with only an ended grant shows it once, in the offer', async () => { + const ended = { ...grantLinear, revokedAt: iso(-600000), revokedBy: 'u1' }; + mockTools({ entries: [githubEntry, linearEntry], grants: [ended] }); + renderPage(); + const drawer = await openTool('Linear'); + expect(within(drawer).getAllByRole('note')).toHaveLength(1); + }); + it('no other drawer carries it', async () => { mockTools({ entries: [githubEntry, linearEntry], grants: [grantLive] }); renderPage(); diff --git a/frontend/src/v2/connectors/ConnectorsPage.tsx b/frontend/src/v2/connectors/ConnectorsPage.tsx index 8c5314e90..ca7f79f66 100644 --- a/frontend/src/v2/connectors/ConnectorsPage.tsx +++ b/frontend/src/v2/connectors/ConnectorsPage.tsx @@ -463,11 +463,8 @@ const ConnectorsPage: React.FC = () => { const deadGrants = entryGrants.filter((grant) => isDead(grant, m.now)); return ( - {/* A Linear connection made before write access cannot write until it is - removed and connected again; the server refuses writes on it. */} - {toolEntry.connectionType === 'hosted-mcp' && toolEntry.entryId === 'linear' && toolEntry.connections.length > 0 && ( -

{t('tools.linearWriteReconnect')}

- )} + {/* Ungranted, the offer below carries the note through renderEntryNote. */} + {toolGranted(toolEntry) && tp.renderLinearWriteNote(toolEntry)} {!toolGranted(toolEntry) && (

{catalogDescription(i18n, toolEntry.installableId, toolEntry.description)}

diff --git a/frontend/src/v2/connectors/ToolPanels.tsx b/frontend/src/v2/connectors/ToolPanels.tsx index b3bbb6aec..c757c5087 100644 --- a/frontend/src/v2/connectors/ToolPanels.tsx +++ b/frontend/src/v2/connectors/ToolPanels.tsx @@ -333,6 +333,14 @@ export const useToolPanels = (m: ToolConnectorsModel) => { // The one line under an entry's name that says what is left to do before it // works: connect your own account, ask an admin, or the modes it grants. + // A Linear connection made before write access cannot write until it is + // disconnected and connected again; the server refuses writes on it. + const renderLinearWriteNote = (entry: ToolCatalogEntry) => ( + entry.connectionType === 'hosted-mcp' && entry.entryId === 'linear' && entry.connections.length > 0 + ?

{t('tools.linearWriteReconnect')}

+ : null + ); + const renderEntryNote = (entry: ToolCatalogEntry) => { const isHosted = entry.connectionType === 'hosted-mcp' && Boolean(entry.entryId); const canConnectHosted = Boolean(isHosted) && entry.available && entry.connections.length === 0; @@ -347,6 +355,7 @@ export const useToolPanels = (m: ToolConnectorsModel) => { : entryModeCopy(entry)}

)} + {renderLinearWriteNote(entry)} {canConnectHosted && entry.connectNotice &&

{entry.connectNotice}

} {isHosted && entry.available && entry.accessPage &&

{renderAccessLink(entry)}

} {renderManualRevokeNotice(entry)} @@ -612,5 +621,5 @@ export const useToolPanels = (m: ToolConnectorsModel) => { ); }; - return { renderManualRevokeNotice, renderDisconnect, renderHiddenConnection, renderRow, renderAccessLink, renderEntryActions, renderEntryNote, renderGithubAppSetup, renderDraft, renderAside }; + return { renderManualRevokeNotice, renderLinearWriteNote, renderDisconnect, renderHiddenConnection, renderRow, renderAccessLink, renderEntryActions, renderEntryNote, renderGithubAppSetup, renderDraft, renderAside }; }; From d1afa2d70ec288f06f918c05814ca2c0d82cd9a6 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:32:08 -0800 Subject: [PATCH 3/3] fix(connectors): plainer Linear write note, and a test that pins its gate Review fixes: - The English note says what to do in the active voice: "If you connected Linear before it could write, disconnect it and connect it again to turn on writes. Each write asks for your approval first." - A connected Atlassian carries no note. GitHub alone failed the hosted-mcp check first, so dropping the Linear check passed every test. - renderEntryNote's comment is back above renderEntryNote, and the new comment says what the server refuses (write mode without write scope). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- frontend/src/i18n/locales/en.json | 2 +- .../src/v2/__tests__/ConnectorsPage.tools.test.tsx | 14 +++++++++++--- frontend/src/v2/connectors/ToolPanels.tsx | 9 +++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 18d36d4a2..e909142a9 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2594,7 +2594,7 @@ "loadFailed": "Could not load artifacts." }, "tools": { - "linearWriteReconnect": "Linear connections made before write access was added need to be disconnected and connected again to enable writes. Each write requires your approval.", + "linearWriteReconnect": "If you connected Linear before it could write, disconnect it and connect it again to turn on writes. Each write asks for your approval first.", "granted": "Granted", "beta": "beta", "adminOnce": "An admin installs this once", diff --git a/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx b/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx index fd4efc9bb..089f6c79a 100644 --- a/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx +++ b/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx @@ -1404,8 +1404,8 @@ describe('Linear write reconnect note', () => { renderPage(); const drawer = await openTool('Linear'); const note = within(drawer).getByRole('note'); - expect(note).toHaveTextContent(language === 'en' ? 'disconnected and connected again to enable writes' : '先断开再重新连接'); - expect(note).toHaveTextContent(language === 'en' ? 'Each write requires your approval' : '每次写入都需要你批准'); + expect(note).toHaveTextContent(language === 'en' ? 'disconnect it and connect it again to turn on writes' : '先断开再重新连接'); + expect(note).toHaveTextContent(language === 'en' ? 'Each write asks for your approval first' : '每次写入都需要你批准'); }); it('a connected Linear with no grant shows it once in the popover, before Add', async () => { @@ -1413,7 +1413,7 @@ describe('Linear write reconnect note', () => { renderPage(); const pop = await openToolPopover('Linear'); expect(within(pop).getAllByRole('note')).toHaveLength(1); - expect(within(pop).getByRole('note')).toHaveTextContent('disconnected and connected again to enable writes'); + expect(within(pop).getByRole('note')).toHaveTextContent('disconnect it and connect it again to turn on writes'); }); it('a Linear drawer with only an ended grant shows it once, in the offer', async () => { @@ -1430,4 +1430,12 @@ describe('Linear write reconnect note', () => { const drawer = await openTool('GitHub'); expect(within(drawer).queryByRole('note')).not.toBeInTheDocument(); }); + + it('another connected hosted connector does not carry it', async () => { + const atlassian = { ...linearEntry, installableId: 'atlassian', entryId: 'atlassian', label: 'Atlassian', connections: [{ connectionId: 'conn-atl', owner: '', repo: '' }] }; + mockTools({ entries: [atlassian], grants: [] }); + renderPage(); + const pop = await openToolPopover('Atlassian'); + expect(within(pop).queryByRole('note')).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/v2/connectors/ToolPanels.tsx b/frontend/src/v2/connectors/ToolPanels.tsx index c757c5087..e7d55c1c8 100644 --- a/frontend/src/v2/connectors/ToolPanels.tsx +++ b/frontend/src/v2/connectors/ToolPanels.tsx @@ -331,16 +331,17 @@ export const useToolPanels = (m: ToolConnectorsModel) => { ); }; - // The one line under an entry's name that says what is left to do before it - // works: connect your own account, ask an admin, or the modes it grants. - // A Linear connection made before write access cannot write until it is - // disconnected and connected again; the server refuses writes on it. + // A Linear connection made before write access lacks the write scope, so the + // server refuses to grant it write mode until it is connected again. The page + // cannot see a connection's scope, so the note is conditional and always shown. const renderLinearWriteNote = (entry: ToolCatalogEntry) => ( entry.connectionType === 'hosted-mcp' && entry.entryId === 'linear' && entry.connections.length > 0 ?

{t('tools.linearWriteReconnect')}

: null ); + // The one line under an entry's name that says what is left to do before it + // works: connect your own account, ask an admin, or the modes it grants. const renderEntryNote = (entry: ToolCatalogEntry) => { const isHosted = entry.connectionType === 'hosted-mcp' && Boolean(entry.entryId); const canConnectHosted = Boolean(isHosted) && entry.available && entry.connections.length === 0;