From 40b962c739bbe2ca23a02f82a8bd28141ed4dab5 Mon Sep 17 00:00:00 2001 From: netcon Date: Tue, 15 Sep 2026 00:13:55 +0800 Subject: [PATCH] feat: github1s-ai click attachment chip open file (#747) --- extensions/github1s-ai/assets/chat.css | 4 ++ extensions/github1s-ai/src/chat-view.ts | 28 ++++----- extensions/github1s-ai/src/common/protocol.ts | 58 ++++++++----------- extensions/github1s-ai/src/controllers/app.ts | 15 +++++ extensions/github1s-ai/src/webview/App.ts | 4 +- .../src/webview/components/AttachmentChips.ts | 7 ++- .../src/webview/components/ChatPage.ts | 4 +- .../src/webview/components/Composer.ts | 1 + .../src/webview/components/MessageView.ts | 13 +++-- .../src/webview/helpers/highlighting.ts | 4 +- extensions/github1s-ai/src/webview/index.ts | 4 +- extensions/github1s-ai/test/protocol.test.ts | 10 ++-- 12 files changed, 80 insertions(+), 72 deletions(-) diff --git a/extensions/github1s-ai/assets/chat.css b/extensions/github1s-ai/assets/chat.css index 739a52ead..bcc309530 100644 --- a/extensions/github1s-ai/assets/chat.css +++ b/extensions/github1s-ai/assets/chat.css @@ -1307,6 +1307,10 @@ summary:focus-visible, .attachment-name { gap: 3px; + padding: 0; + color: inherit; + background: transparent; + border: 0; } .attachment-label { diff --git a/extensions/github1s-ai/src/chat-view.ts b/extensions/github1s-ai/src/chat-view.ts index 07987fe14..0f979d1ec 100644 --- a/extensions/github1s-ai/src/chat-view.ts +++ b/extensions/github1s-ai/src/chat-view.ts @@ -1,11 +1,6 @@ import * as vscode from 'vscode'; -import { - parseMarkdownHighlightRequest, - parseViewEvent, - type MarkdownHighlightRequest, - type ViewMessage, -} from '@/common/protocol'; +import { parseViewEvent, type ViewEvent, type ViewMessage } from '@/common/protocol'; import { Controllers } from '@/controllers'; import { currentFile, currentSelection } from '@/controllers/context'; import { computeSyntaxHighlighting, onDidChangeSyntaxHighlighting } from '@/helpers/highlighting'; @@ -78,18 +73,17 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp void view.webview.postMessage({ type: 'app.setState', state } satisfies ViewMessage); }), view.webview.onDidReceiveMessage((event) => { - const highlightRequest = parseMarkdownHighlightRequest(event); - if (highlightRequest) { - void this.respondWithSyntaxHighlighting(view.webview, highlightRequest).catch(() => undefined); - return; - } const parsed = parseViewEvent(event); - if (parsed) { - this.controllers - .emit(parsed) - .then((message) => message && view.webview.postMessage(message)) - .catch(() => undefined); + if (!parsed) return; + if (parsed.type === 'markdown.highlight') { + // Bypass Controllers.emit to avoid syncing the full view state for each code block. + void this.respondWithSyntaxHighlighting(view.webview, parsed).catch(() => undefined); + return; } + this.controllers + .emit(parsed) + .then((message) => message && view.webview.postMessage(message)) + .catch(() => undefined); }), ); const highlightingListener = onDidChangeSyntaxHighlighting(() => { @@ -137,7 +131,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp private async respondWithSyntaxHighlighting( webview: vscode.Webview, - request: MarkdownHighlightRequest, + request: ViewEvent<'markdown.highlight'>, ): Promise { const highlighting = await computeSyntaxHighlighting(request.source, request.languageId); await webview.postMessage({ diff --git a/extensions/github1s-ai/src/common/protocol.ts b/extensions/github1s-ai/src/common/protocol.ts index 7744156ec..b7c79329b 100644 --- a/extensions/github1s-ai/src/common/protocol.ts +++ b/extensions/github1s-ai/src/common/protocol.ts @@ -33,19 +33,13 @@ export interface ViewState { conversations: ConversationSummary[]; } -export interface MarkdownHighlightRequest { - type: 'markdown.highlight'; - requestId: string; - languageId: string; - source: string; -} - type AllViewEvents = | { type: 'app.ready' } | { type: 'app.newChat' } | { type: 'app.openChat' } | { type: 'app.openHistory' } | { type: 'app.openSettings' } + | { type: 'app.openFile'; source: string } | { type: 'chat.send'; text: string } | { type: 'chat.runQuickAction'; action: ChatQuickAction } | { type: 'chat.addContextAttachment'; action: ContextAttachmentAction } @@ -55,6 +49,7 @@ type AllViewEvents = | { type: 'chat.cancel' } | { type: 'history.selectConversation'; id: string } | { type: 'history.deleteConversation'; id: string } + | { type: 'markdown.highlight'; requestId: string; languageId: string; source: string } | { type: 'settings.selectModelConfig'; id: string } | { type: 'settings.clearFeedback' } | { type: 'settings.exportHistory' } @@ -66,39 +61,12 @@ type AllViewEvents = export type ViewEvent = Extract; -export type ViewRequest = ViewEvent | MarkdownHighlightRequest; - export type ViewMessage = | { type: 'app.setState'; state: ViewState } | { type: 'settings.historyExport'; filename: string; content: string } | { type: 'markdown.highlightResult'; requestId: string; highlighting?: SyntaxHighlightingData } | { type: 'markdown.highlightingChanged' }; -export const parseMarkdownHighlightRequest = (value: unknown): MarkdownHighlightRequest | undefined => { - if (!isPlainObject(value)) return undefined; - const request = value as Record; - if ( - !hasExactKeys(request, ['type', 'requestId', 'languageId', 'source']) || - request.type !== 'markdown.highlight' || - typeof request.requestId !== 'string' || - request.requestId.length === 0 || - request.requestId.length > 100 || - typeof request.languageId !== 'string' || - request.languageId.length === 0 || - request.languageId.length > 100 || - typeof request.source !== 'string' || - request.source.length > MAX_SYNTAX_HIGHLIGHT_SOURCE_LENGTH - ) { - return undefined; - } - return { - type: request.type, - requestId: request.requestId, - languageId: request.languageId, - source: request.source, - }; -}; - export const parseViewEvent = (value: unknown): ViewEvent | undefined => { if (!isPlainObject(value)) return undefined; const event = value as Record; @@ -115,6 +83,9 @@ export const parseViewEvent = (value: unknown): ViewEvent | undefined => { case 'settings.clearAllData': return hasExactKeys(event, ['type']) ? { type: event.type } : undefined; + case 'app.openFile': + return exactString(event, 'source') ? { type: event.type, source: event.source as string } : undefined; + case 'chat.send': return hasExactKeys(event, ['type', 'text']) && typeof event.text === 'string' ? { type: event.type, text: event.text } @@ -147,6 +118,25 @@ export const parseViewEvent = (value: unknown): ViewEvent | undefined => { ? { type: event.type, action: event.action } : undefined; + case 'markdown.highlight': + if ( + !hasExactKeys(event, ['type', 'requestId', 'languageId', 'source']) || + typeof event.requestId !== 'string' || + event.requestId.length === 0 || + typeof event.languageId !== 'string' || + event.languageId.length === 0 || + typeof event.source !== 'string' || + event.source.length > MAX_SYNTAX_HIGHLIGHT_SOURCE_LENGTH + ) { + return undefined; + } + return { + type: event.type, + requestId: event.requestId, + languageId: event.languageId, + source: event.source, + }; + case 'settings.saveModelConfig': { if (!hasExactKeys(event, ['type', 'config'])) return undefined; return isModelConfigInput(event.config) ? { type: event.type, config: event.config } : undefined; diff --git a/extensions/github1s-ai/src/controllers/app.ts b/extensions/github1s-ai/src/controllers/app.ts index 79ddcd7d6..5898428e0 100644 --- a/extensions/github1s-ai/src/controllers/app.ts +++ b/extensions/github1s-ai/src/controllers/app.ts @@ -1,3 +1,5 @@ +import * as vscode from 'vscode'; + import { markStreamingMessagesUnknown } from '@/common/conversation'; import type { ViewEvent } from '@/common/protocol'; import type { RuntimeState } from '@/common/state'; @@ -47,6 +49,19 @@ export class AppController extends Controller { await this.openPage('settings'); } + @Controller.handler('app.openFile') + async handleOpenFile(event: ViewEvent<'app.openFile'>): Promise { + const uri = vscode.Uri.parse(event.source); + const match = /^L(\d+):(\d+)-L(\d+):(\d+)$/.exec(uri.fragment); + const positions = match?.slice(1).map(Number); + let selection: vscode.Range | undefined; + if (positions && positions.every((position) => position >= 1)) { + const [startLine, startCharacter, endLine, endCharacter] = positions; + selection = new vscode.Range(startLine - 1, startCharacter - 1, endLine - 1, endCharacter - 1); + } + await vscode.window.showTextDocument(uri.with({ fragment: '' }), { selection }); + } + private async openPage(page: RuntimeState['page'], resetConversation = false): Promise { await this.runner.cancelPreparation(); const runtime = await this.stores.runtime.get(); diff --git a/extensions/github1s-ai/src/webview/App.ts b/extensions/github1s-ai/src/webview/App.ts index a1f1713f9..8db005a05 100644 --- a/extensions/github1s-ai/src/webview/App.ts +++ b/extensions/github1s-ai/src/webview/App.ts @@ -1,14 +1,14 @@ import { html } from 'htm/preact'; import { useEffect, useState } from 'preact/hooks'; -import type { ViewMessage, ViewRequest, ViewState } from '@/common/protocol'; +import type { ViewEvent, ViewMessage, ViewState } from '@/common/protocol'; import { ChatPage } from './components/ChatPage'; import { HistoryPage } from './components/HistoryPage'; import { SettingsPage } from './components/SettingsPage'; interface AppProps { - post: (request: ViewRequest) => void; + post: (event: ViewEvent) => void; } export const App = ({ post }: AppProps) => { diff --git a/extensions/github1s-ai/src/webview/components/AttachmentChips.ts b/extensions/github1s-ai/src/webview/components/AttachmentChips.ts index f8cec4d2a..f64451686 100644 --- a/extensions/github1s-ai/src/webview/components/AttachmentChips.ts +++ b/extensions/github1s-ai/src/webview/components/AttachmentChips.ts @@ -9,10 +9,11 @@ import { Tooltip, TooltipButton } from './Tooltip'; interface AttachmentChipsProps { attachments: readonly ContextAttachmentDescriptor[]; disabled?: boolean; + onOpen: (source: string) => void; onRemove?: (id: string) => void; } -export const AttachmentChips = ({ attachments, disabled = false, onRemove }: AttachmentChipsProps) => { +export const AttachmentChips = ({ attachments, disabled = false, onOpen, onRemove }: AttachmentChipsProps) => { if (attachments.length === 0) return null; const readOnly = onRemove === undefined; @@ -27,11 +28,11 @@ export const AttachmentChips = ({ attachments, disabled = false, onRemove }: Att const iconClass = `seti-file-icon seti-file-icon-${icon} seti-file-icon-color-${presentation.color} attachment-icon`; return html` <${Tooltip} content=${attachment.source}> - + ${onRemove ? html`<${TooltipButton} diff --git a/extensions/github1s-ai/src/webview/components/ChatPage.ts b/extensions/github1s-ai/src/webview/components/ChatPage.ts index acb33efa4..66407776a 100644 --- a/extensions/github1s-ai/src/webview/components/ChatPage.ts +++ b/extensions/github1s-ai/src/webview/components/ChatPage.ts @@ -2,7 +2,7 @@ import { html } from 'htm/preact'; import { useLayoutEffect, useRef } from 'preact/hooks'; import type { ConversationMessage } from '@/common/conversation'; -import type { ViewRequest, ViewState } from '@/common/protocol'; +import type { ViewEvent, ViewState } from '@/common/protocol'; import { QUICK_ACTIONS } from '@/common/quick-actions'; import { Composer } from './Composer'; @@ -11,7 +11,7 @@ import { MessageView } from './MessageView'; interface ChatContextProps { state: ViewState; - post: (request: ViewRequest) => void; + post: (event: ViewEvent) => void; } interface ChatPageProps extends ChatContextProps { diff --git a/extensions/github1s-ai/src/webview/components/Composer.ts b/extensions/github1s-ai/src/webview/components/Composer.ts index 5d6537391..c9d55b015 100644 --- a/extensions/github1s-ai/src/webview/components/Composer.ts +++ b/extensions/github1s-ai/src/webview/components/Composer.ts @@ -81,6 +81,7 @@ export const Composer = ({ state, busy, active, post }: ComposerProps) => { <${AttachmentChips} attachments=${chat.pendingAttachments ?? []} disabled=${preparing} + onOpen=${(source: string) => post({ type: 'app.openFile', source })} onRemove=${(id: string) => post({ type: 'chat.removeContextAttachment', id })} />