feat(chat): structured context attachments with metadata round-trip
Every user-attached context item (diff/file/plan comments, terminal selections, browser annotations, PR comments and failed checks, linked issues/PRs, and new chat-quote comments from the selection menu) is now sent as its own synthetic text part carrying an openchamberContext metadata payload. The model-facing text keeps the previous wording; the timeline reads the metadata back and renders each item as a context card instead of raw prompt text. Legacy messages still render via the old text sniffing. The selection menu gains a Comment option with an inline multiline input, the quoted fragment stays highlighted while commenting, and on mobile the input overlays the composer pill by rendering inside the composer form. Add to chat is renamed Add to input; the menu is restyled and the mobile Copy tile removed. Terminal drafts move their terminal id out of the language field (persisted-draft migration v3), and the dead preview-console source is deleted.
This commit is contained in:
@@ -222,7 +222,8 @@ export const buildAnnotationOverlayScript = (
|
||||
'.editor{position:fixed;left:0;top:0;display:none;align-items:center;gap:8px;width:min(420px,calc(100vw - 24px));padding:6px;padding-left:16px;border-radius:22px;border:1px solid ' + THEME.border + ';background:' + THEME.glassSurface + ';-webkit-backdrop-filter:' + THEME.glassFilter + ';backdrop-filter:' + THEME.glassFilter + ';box-shadow:0 8px 28px rgba(0,0,0,.3);pointer-events:auto}',
|
||||
'.editor textarea{flex:1;min-width:0;resize:none;border:none;background:transparent;color:' + THEME.text + ';font-size:13px;line-height:20px;outline:none;padding:6px 0;min-height:32px;max-height:104px;display:block}',
|
||||
'.editor textarea::placeholder{color:' + THEME.mutedText + '}',
|
||||
'.editor button{appearance:none;border:none;background:' + THEME.primary + ';color:' + THEME.primaryContrast + ';border-radius:999px;padding:8px 18px;font-size:12px;line-height:18px;font-weight:600;cursor:pointer;white-space:nowrap}',
|
||||
'.editor button{appearance:none;border:none;background:' + THEME.primary + ';color:' + THEME.primaryContrast + ';border-radius:999px;width:32px;height:32px;padding:0;display:flex;align-items:center;justify-content:center;flex-shrink:0;cursor:pointer}',
|
||||
'.editor button svg{width:16px;height:16px;display:block}',
|
||||
'.editor button[disabled]{opacity:.5;cursor:default}'
|
||||
].join('');
|
||||
shadow.appendChild(style);
|
||||
@@ -278,7 +279,11 @@ export const buildAnnotationOverlayScript = (
|
||||
comment.placeholder = LABELS.commentPlaceholder;
|
||||
var submit = document.createElement('button');
|
||||
submit.type = 'button';
|
||||
submit.textContent = LABELS.submit;
|
||||
// Icon-only attach button (Remix attachment-2), matching the chat comment
|
||||
// input; the localized label stays available to assistive tech.
|
||||
submit.setAttribute('aria-label', LABELS.submit);
|
||||
submit.title = LABELS.submit;
|
||||
submit.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14.8287 7.75737L9.1718 13.4142C8.78127 13.8047 8.78127 14.4379 9.1718 14.8284C9.56232 15.219 10.1955 15.219 10.586 14.8284L16.2429 9.17158C17.4144 8.00001 17.4144 6.10052 16.2429 4.92894C15.0713 3.75737 13.1718 3.75737 12.0002 4.92894L6.34337 10.5858C4.39075 12.5384 4.39075 15.7042 6.34337 17.6569C8.29599 19.6095 11.4618 19.6095 13.4144 17.6569L19.0713 12L20.4855 13.4142L14.8287 19.0711C12.095 21.8047 7.66283 21.8047 4.92916 19.0711C2.19549 16.3374 2.19549 11.9053 4.92916 9.17158L10.586 3.51473C12.5386 1.56211 15.7045 1.56211 17.6571 3.51473C19.6097 5.46735 19.6097 8.63317 17.6571 10.5858L12.0002 16.2427C10.8287 17.4142 8.92916 17.4142 7.75759 16.2427C6.58601 15.0711 6.58601 13.1716 7.75759 12L13.4144 6.34316L14.8287 7.75737Z" fill="currentColor"/></svg>';
|
||||
editor.append(comment, submit);
|
||||
|
||||
/**
|
||||
|
||||
@@ -2009,9 +2009,12 @@ export const dict = {
|
||||
'chat.textSelection.toast.addToNotesFailed': 'Fehler beim Hinzufügen zu Notizen',
|
||||
'chat.textSelection.toast.addToNotesSuccess': 'Ausgewählter Text zu Notizen hinzugefügt',
|
||||
'chat.textSelection.toast.addToNotesSummaryFailed': 'Zusammenfassung der Auswahl nicht möglich, ausgewählter Text wurde zu Notizen hinzugefügt',
|
||||
'chat.textSelection.actions.addToChat': 'Zum Chat hinzufügen',
|
||||
'chat.textSelection.actions.addToInput': 'Zur Eingabe hinzufügen',
|
||||
'chat.textSelection.actions.comment': 'Kommentieren',
|
||||
'chat.textSelection.title.commentOnSelection': 'Auswahl kommentieren',
|
||||
'chat.textSelection.comment.placeholder': 'Optionalen Kommentar hinzufügen...',
|
||||
'chat.textSelection.comment.attach': 'Anhängen',
|
||||
'chat.textSelection.actions.newSession': 'Neue Sitzung',
|
||||
'chat.textSelection.actions.copy': 'Kopieren',
|
||||
'chat.textSelection.actions.addToNotes': 'Zu Notizen hinzufügen',
|
||||
'chat.textSelection.title.addToCurrentChat': 'Zum aktuellen Chat hinzufügen',
|
||||
'chat.textSelection.title.newSessionWithSelection': 'Neue Sitzung mit Auswahl erstellen',
|
||||
@@ -2117,8 +2120,6 @@ export const dict = {
|
||||
'chat.chatInput.toast.togglePermissionAutoAcceptFailed': 'Fehler beim Umschalten der automatischen Zustimmung zur Berechtigung',
|
||||
'chat.chatInput.reviewComments': 'Kommentare zur Überprüfung:',
|
||||
'chat.chatInput.reviewCommentsRemove': 'Kommentare zur Überprüfung entfernen',
|
||||
'chat.chatInput.devServerLogs': 'Entwicklungsserver-Protokolle:',
|
||||
'chat.chatInput.devServerLogsRemove': 'Entwicklungsserver-Protokolle entfernen',
|
||||
'chat.chatInput.previewAnnotations': 'Vorschau-Anmerkungen:',
|
||||
'chat.chatInput.previewContext': 'Vorschau-Kontext:',
|
||||
'chat.chatInput.previewContextRemove': 'Vorschau-Kontext entfernen',
|
||||
@@ -2903,6 +2904,14 @@ export const dict = {
|
||||
'terminalView.actions.attachSelection': 'Ausgewählte Ausgabe anhängen',
|
||||
'terminalView.actions.restart': 'Terminal neu starten',
|
||||
'chat.message.terminalContext': '{terminal}, Zeilen {start}-{end}',
|
||||
'chat.message.context.codeComment': 'Kommentar zu {file}, Zeilen {start}-{end}',
|
||||
'chat.message.context.codeCommentLine': 'Kommentar zu {file}, Zeile {line}',
|
||||
'chat.message.context.chatQuote': 'Zitat aus einer früheren Nachricht',
|
||||
'chat.chatInput.chatQuoteContext': 'Chat-Zitate',
|
||||
'chat.chatInput.chatQuoteContextRemove': 'Chat-Zitate entfernen',
|
||||
'chat.message.context.browserAnnotation': 'Browser-Anmerkung ({page})',
|
||||
'chat.message.context.prComment': 'GitHub-PR-Kommentar ({label})',
|
||||
'chat.message.context.prCheck': 'Fehlgeschlagener GitHub-PR-Check ({label})',
|
||||
'chat.chatInput.terminalContext': '{terminal}, Zeilen {start}-{end}',
|
||||
'chat.chatInput.terminalContextRemove': 'Terminal-Kontext entfernen',
|
||||
'chat.chatInput.prCommentContext': 'PR-Kommentare',
|
||||
|
||||
@@ -5,6 +5,14 @@ export const dict = {
|
||||
'terminalView.actions.attachSelection': 'Attach selected output',
|
||||
'terminalView.actions.restart': 'Restart terminal',
|
||||
'chat.message.terminalContext': '{terminal}, lines {start}-{end}',
|
||||
'chat.message.context.codeComment': 'Comment on {file}, lines {start}-{end}',
|
||||
'chat.message.context.codeCommentLine': 'Comment on {file}, line {line}',
|
||||
'chat.message.context.chatQuote': 'Quoted from an earlier message',
|
||||
'chat.chatInput.chatQuoteContext': 'Chat quotes',
|
||||
'chat.chatInput.chatQuoteContextRemove': 'Remove chat quotes',
|
||||
'chat.message.context.browserAnnotation': 'Browser annotation ({page})',
|
||||
'chat.message.context.prComment': 'GitHub PR comment ({label})',
|
||||
'chat.message.context.prCheck': 'Failed GitHub PR check ({label})',
|
||||
'chat.chatInput.terminalContext': '{terminal}, lines {start}-{end}',
|
||||
'chat.chatInput.terminalContextRemove': 'Remove terminal context',
|
||||
'chat.chatInput.prCommentContext': 'PR comments',
|
||||
@@ -2182,9 +2190,12 @@ export const dict = {
|
||||
'chat.textSelection.toast.addToNotesFailed': 'Failed to add to notes',
|
||||
'chat.textSelection.toast.addToNotesSuccess': 'Added selected text to notes',
|
||||
'chat.textSelection.toast.addToNotesSummaryFailed': 'Could not summarize selection, added selected text to notes',
|
||||
'chat.textSelection.actions.addToChat': 'Add to chat',
|
||||
'chat.textSelection.actions.addToInput': 'Add to input',
|
||||
'chat.textSelection.actions.comment': 'Comment',
|
||||
'chat.textSelection.title.commentOnSelection': 'Comment on selection',
|
||||
'chat.textSelection.comment.placeholder': 'Add an optional comment...',
|
||||
'chat.textSelection.comment.attach': 'Attach',
|
||||
'chat.textSelection.actions.newSession': 'New session',
|
||||
'chat.textSelection.actions.copy': 'Copy',
|
||||
'chat.textSelection.actions.addToNotes': 'Add to notes',
|
||||
'chat.textSelection.title.addToCurrentChat': 'Add to current chat',
|
||||
'chat.textSelection.title.newSessionWithSelection': 'Create new session with selection',
|
||||
@@ -2294,8 +2305,6 @@ export const dict = {
|
||||
'chat.chatInput.toast.togglePermissionAutoAcceptFailed': 'Failed to toggle permission auto-accept',
|
||||
'chat.chatInput.reviewComments': 'Review comments:',
|
||||
'chat.chatInput.reviewCommentsRemove': 'Remove review comments',
|
||||
'chat.chatInput.devServerLogs': 'Dev Server logs:',
|
||||
'chat.chatInput.devServerLogsRemove': 'Remove Dev Server logs',
|
||||
'chat.chatInput.previewAnnotations': 'Preview annotations:',
|
||||
'chat.chatInput.previewContext': 'Preview context:',
|
||||
'chat.chatInput.previewContextRemove': 'Remove preview context',
|
||||
|
||||
@@ -6,6 +6,14 @@ export const dict: Record<I18nKey, string> = {
|
||||
'terminalView.actions.attachSelection': 'Adjuntar salida seleccionada',
|
||||
'terminalView.actions.restart': 'Reiniciar terminal',
|
||||
'chat.message.terminalContext': '{terminal}, líneas {start}-{end}',
|
||||
'chat.message.context.codeComment': 'Comentario en {file}, líneas {start}-{end}',
|
||||
'chat.message.context.codeCommentLine': 'Comentario en {file}, línea {line}',
|
||||
'chat.message.context.chatQuote': 'Cita de un mensaje anterior',
|
||||
'chat.chatInput.chatQuoteContext': 'Citas del chat',
|
||||
'chat.chatInput.chatQuoteContextRemove': 'Quitar citas del chat',
|
||||
'chat.message.context.browserAnnotation': 'Anotación del navegador ({page})',
|
||||
'chat.message.context.prComment': 'Comentario de PR de GitHub ({label})',
|
||||
'chat.message.context.prCheck': 'Verificación de PR de GitHub fallida ({label})',
|
||||
'chat.chatInput.terminalContext': '{terminal}, líneas {start}-{end}',
|
||||
'chat.chatInput.terminalContextRemove': 'Eliminar contexto del terminal',
|
||||
'chat.chatInput.prCommentContext': 'Comentarios del PR',
|
||||
@@ -2160,9 +2168,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.textSelection.toast.addToNotesFailed": "No se pudo añadir a las notas",
|
||||
"chat.textSelection.toast.addToNotesSuccess": "Texto seleccionado añadido a notas",
|
||||
"chat.textSelection.toast.addToNotesSummaryFailed": "No se pudo resumir la selección; se añadió el texto seleccionado a las notas",
|
||||
"chat.textSelection.actions.addToChat": "Añadir al chat",
|
||||
"chat.textSelection.actions.addToInput": "Añadir a la entrada",
|
||||
"chat.textSelection.actions.comment": "Comentar",
|
||||
"chat.textSelection.title.commentOnSelection": "Comentar la selección",
|
||||
"chat.textSelection.comment.placeholder": "Añade un comentario opcional...",
|
||||
"chat.textSelection.comment.attach": "Adjuntar",
|
||||
"chat.textSelection.actions.newSession": "Nueva sesión",
|
||||
"chat.textSelection.actions.copy": "Copiar",
|
||||
"chat.textSelection.actions.addToNotes": "Añadir a las notas",
|
||||
"chat.textSelection.title.addToCurrentChat": "Añadir al chat actual",
|
||||
"chat.textSelection.title.newSessionWithSelection": "Crear nueva sesión con selección",
|
||||
@@ -2260,8 +2271,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.chatInput.toast.togglePermissionAutoAcceptFailed": "No se pudo cambiar la aceptación automática de permisos",
|
||||
"chat.chatInput.reviewComments": "Comentarios de revisión:",
|
||||
"chat.chatInput.reviewCommentsRemove": "Quitar comentarios de revisión",
|
||||
"chat.chatInput.devServerLogs": "Logs del Dev Server:",
|
||||
"chat.chatInput.devServerLogsRemove": "Quitar logs del Dev Server",
|
||||
"chat.chatInput.previewAnnotations": "Anotaciones de vista previa:",
|
||||
"chat.chatInput.previewContext": "Contexto de vista previa:",
|
||||
"chat.chatInput.previewContextRemove": "Quitar contexto de vista previa",
|
||||
|
||||
@@ -5,6 +5,14 @@ export const dict = {
|
||||
'terminalView.actions.attachSelection': 'Joindre la sortie sélectionnée',
|
||||
'terminalView.actions.restart': 'Redémarrer le terminal',
|
||||
'chat.message.terminalContext': '{terminal}, lignes {start}-{end}',
|
||||
'chat.message.context.codeComment': 'Commentaire sur {file}, lignes {start}-{end}',
|
||||
'chat.message.context.codeCommentLine': 'Commentaire sur {file}, ligne {line}',
|
||||
'chat.message.context.chatQuote': 'Citation d’un message précédent',
|
||||
'chat.chatInput.chatQuoteContext': 'Citations du chat',
|
||||
'chat.chatInput.chatQuoteContextRemove': 'Supprimer les citations du chat',
|
||||
'chat.message.context.browserAnnotation': 'Annotation du navigateur ({page})',
|
||||
'chat.message.context.prComment': 'Commentaire de PR GitHub ({label})',
|
||||
'chat.message.context.prCheck': 'Vérification de PR GitHub échouée ({label})',
|
||||
'chat.chatInput.terminalContext': '{terminal}, lignes {start}-{end}',
|
||||
'chat.chatInput.terminalContextRemove': 'Supprimer le contexte du terminal',
|
||||
'chat.chatInput.prCommentContext': 'Commentaires de PR',
|
||||
@@ -1909,9 +1917,12 @@ export const dict = {
|
||||
'chat.textSelection.toast.addToNotesFailed': 'Échec de l\'ajout aux notes',
|
||||
'chat.textSelection.toast.addToNotesSuccess': 'Ajout du texte sélectionné aux notes',
|
||||
'chat.textSelection.toast.addToNotesSummaryFailed': 'Impossible de résumer la sélection, ajout du texte sélectionné aux notes',
|
||||
'chat.textSelection.actions.addToChat': 'Ajouter au chat',
|
||||
'chat.textSelection.actions.addToInput': 'Ajouter à la saisie',
|
||||
'chat.textSelection.actions.comment': 'Commenter',
|
||||
'chat.textSelection.title.commentOnSelection': 'Commenter la sélection',
|
||||
'chat.textSelection.comment.placeholder': 'Ajouter un commentaire facultatif...',
|
||||
'chat.textSelection.comment.attach': 'Joindre',
|
||||
'chat.textSelection.actions.newSession': 'Nouvelle session',
|
||||
'chat.textSelection.actions.copy': 'Copie',
|
||||
'chat.textSelection.actions.addToNotes': 'Ajouter aux notes',
|
||||
'chat.textSelection.title.addToCurrentChat': 'Ajouter au chat actuel',
|
||||
'chat.textSelection.title.newSessionWithSelection': 'Créer une nouvelle session avec sélection',
|
||||
@@ -2006,8 +2017,6 @@ export const dict = {
|
||||
'chat.chatInput.toast.openSessionFirst': 'Ouvrir d\'abord une session',
|
||||
'chat.chatInput.toast.togglePermissionAutoAcceptFailed': 'Échec de l\'activation de l\'acceptation automatique des autorisations',
|
||||
'chat.chatInput.reviewComments': 'Commentaires de révision :',
|
||||
'chat.chatInput.devServerLogs': 'Journaux du serveur de développement :',
|
||||
'chat.chatInput.devServerLogsRemove': 'Supprimer les journaux du serveur de développement',
|
||||
'chat.chatInput.previewAnnotations': 'Aperçu des annotations :',
|
||||
'chat.chatInput.previewContext': 'Contexte d\'aperçu :',
|
||||
'chat.chatInput.previewContextRemove': 'Supprimer le contexte d\'aperçu',
|
||||
|
||||
@@ -6,6 +6,14 @@ export const dict: Record<I18nKey, string> = {
|
||||
'terminalView.actions.attachSelection': '選択した出力を添付',
|
||||
'terminalView.actions.restart': 'ターミナルを再起動',
|
||||
'chat.message.terminalContext': '{terminal}、{start}〜{end}行',
|
||||
'chat.message.context.codeComment': '{file} の {start}〜{end} 行へのコメント',
|
||||
'chat.message.context.codeCommentLine': '{file} の {line} 行へのコメント',
|
||||
'chat.message.context.chatQuote': '以前のメッセージからの引用',
|
||||
'chat.chatInput.chatQuoteContext': 'チャット引用',
|
||||
'chat.chatInput.chatQuoteContextRemove': 'チャット引用を削除',
|
||||
'chat.message.context.browserAnnotation': 'ブラウザ注釈({page})',
|
||||
'chat.message.context.prComment': 'GitHub PR コメント({label})',
|
||||
'chat.message.context.prCheck': '失敗した GitHub PR チェック({label})',
|
||||
'chat.chatInput.terminalContext': '{terminal}、{start}〜{end}行',
|
||||
'chat.chatInput.terminalContextRemove': 'ターミナルコンテキストを削除',
|
||||
'chat.chatInput.prCommentContext': 'PRコメント',
|
||||
@@ -2178,9 +2186,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.textSelection.toast.addToNotesFailed': 'メモへの追加に失敗しました',
|
||||
'chat.textSelection.toast.addToNotesSuccess': '選択テキストをメモに追加しました',
|
||||
'chat.textSelection.toast.addToNotesSummaryFailed': '選択範囲を要約できませんでした。選択テキストをメモに追加しました。',
|
||||
'chat.textSelection.actions.addToChat': 'チャットに追加',
|
||||
'chat.textSelection.actions.addToInput': '入力欄に追加',
|
||||
'chat.textSelection.actions.comment': 'コメント',
|
||||
'chat.textSelection.title.commentOnSelection': '選択範囲にコメント',
|
||||
'chat.textSelection.comment.placeholder': '任意のコメントを追加...',
|
||||
'chat.textSelection.comment.attach': '添付',
|
||||
'chat.textSelection.actions.newSession': '新しいセッション',
|
||||
'chat.textSelection.actions.copy': 'コピー',
|
||||
'chat.textSelection.actions.addToNotes': 'メモに追加',
|
||||
'chat.textSelection.title.addToCurrentChat': '現在のチャットに追加',
|
||||
'chat.textSelection.title.newSessionWithSelection': '選択範囲で新しいセッションを作成',
|
||||
@@ -2293,8 +2304,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.chatInput.toast.togglePermissionAutoAcceptFailed': '権限の自動承認の切り替えに失敗しました',
|
||||
'chat.chatInput.reviewComments': 'レビューコメント:',
|
||||
'chat.chatInput.reviewCommentsRemove': 'レビューコメントを削除',
|
||||
'chat.chatInput.devServerLogs': '開発サーバーログ:',
|
||||
'chat.chatInput.devServerLogsRemove': '開発サーバーログを削除',
|
||||
'chat.chatInput.previewAnnotations': 'プレビュー注釈:',
|
||||
'chat.chatInput.previewContext': 'プレビューコンテキスト:',
|
||||
'chat.chatInput.previewContextRemove': 'プレビューコンテキストを削除',
|
||||
|
||||
@@ -6,6 +6,14 @@ export const dict: Record<I18nKey, string> = {
|
||||
'terminalView.actions.attachSelection': '선택한 출력 첨부',
|
||||
'terminalView.actions.restart': '터미널 다시 시작',
|
||||
'chat.message.terminalContext': '{terminal}, {start}-{end}행',
|
||||
'chat.message.context.codeComment': '{file} {start}-{end}행에 대한 댓글',
|
||||
'chat.message.context.codeCommentLine': '{file} {line}행에 대한 댓글',
|
||||
'chat.message.context.chatQuote': '이전 메시지에서 인용',
|
||||
'chat.chatInput.chatQuoteContext': '채팅 인용',
|
||||
'chat.chatInput.chatQuoteContextRemove': '채팅 인용 제거',
|
||||
'chat.message.context.browserAnnotation': '브라우저 주석 ({page})',
|
||||
'chat.message.context.prComment': 'GitHub PR 댓글 ({label})',
|
||||
'chat.message.context.prCheck': '실패한 GitHub PR 검사 ({label})',
|
||||
'chat.chatInput.terminalContext': '{terminal}, {start}-{end}행',
|
||||
'chat.chatInput.terminalContextRemove': '터미널 컨텍스트 제거',
|
||||
'chat.chatInput.prCommentContext': 'PR 댓글',
|
||||
@@ -2184,9 +2192,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.textSelection.toast.addToNotesFailed': '메모 추가 실패',
|
||||
'chat.textSelection.toast.addToNotesSuccess': '선택한 텍스트를 메모에 추가함',
|
||||
'chat.textSelection.toast.addToNotesSummaryFailed': '선택 영역을 요약할 수 없어 선택한 텍스트를 메모에 추가함',
|
||||
'chat.textSelection.actions.addToChat': '채팅에 추가',
|
||||
'chat.textSelection.actions.addToInput': '입력란에 추가',
|
||||
'chat.textSelection.actions.comment': '댓글',
|
||||
'chat.textSelection.title.commentOnSelection': '선택 영역에 댓글 달기',
|
||||
'chat.textSelection.comment.placeholder': '선택적 댓글 추가...',
|
||||
'chat.textSelection.comment.attach': '첨부',
|
||||
'chat.textSelection.actions.newSession': '새 세션',
|
||||
'chat.textSelection.actions.copy': '복사',
|
||||
'chat.textSelection.actions.addToNotes': '메모에 추가',
|
||||
'chat.textSelection.title.addToCurrentChat': '현재 채팅에 추가',
|
||||
'chat.textSelection.title.newSessionWithSelection': '선택한 내용으로 새 세션 생성',
|
||||
@@ -2294,8 +2305,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.chatInput.toast.togglePermissionAutoAcceptFailed': '권한 자동 승인 전환에 실패했습니다',
|
||||
'chat.chatInput.reviewComments': '검토 댓글:',
|
||||
'chat.chatInput.reviewCommentsRemove': '검토 댓글 제거',
|
||||
'chat.chatInput.devServerLogs': 'Dev Server 로그:',
|
||||
'chat.chatInput.devServerLogsRemove': 'Dev Server 로그 제거',
|
||||
'chat.chatInput.previewAnnotations': '미리보기 주석:',
|
||||
'chat.chatInput.previewContext': '미리보기 컨텍스트:',
|
||||
'chat.chatInput.previewContextRemove': '미리보기 컨텍스트 제거',
|
||||
|
||||
@@ -6,6 +6,14 @@ export const dict: Record<I18nKey, string> = {
|
||||
'terminalView.actions.attachSelection': 'Dołącz zaznaczone dane wyjściowe',
|
||||
'terminalView.actions.restart': 'Uruchom terminal ponownie',
|
||||
'chat.message.terminalContext': '{terminal}, wiersze {start}-{end}',
|
||||
'chat.message.context.codeComment': 'Komentarz do {file}, wiersze {start}-{end}',
|
||||
'chat.message.context.codeCommentLine': 'Komentarz do {file}, wiersz {line}',
|
||||
'chat.message.context.chatQuote': 'Cytat z wcześniejszej wiadomości',
|
||||
'chat.chatInput.chatQuoteContext': 'Cytaty z czatu',
|
||||
'chat.chatInput.chatQuoteContextRemove': 'Usuń cytaty z czatu',
|
||||
'chat.message.context.browserAnnotation': 'Adnotacja przeglądarki ({page})',
|
||||
'chat.message.context.prComment': 'Komentarz PR GitHub ({label})',
|
||||
'chat.message.context.prCheck': 'Nieudane sprawdzenie PR GitHub ({label})',
|
||||
'chat.chatInput.terminalContext': '{terminal}, wiersze {start}-{end}',
|
||||
'chat.chatInput.terminalContextRemove': 'Usuń kontekst terminala',
|
||||
'chat.chatInput.prCommentContext': 'Komentarze PR',
|
||||
@@ -874,9 +882,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.textSelection.toast.addToNotesFailed': 'Nie udało się dodać do notatek',
|
||||
'chat.textSelection.toast.addToNotesSuccess': 'Dodano zaznaczony tekst do notatek',
|
||||
'chat.textSelection.toast.addToNotesSummaryFailed': 'Nie można podsumować zaznaczenia, dodano wybrany tekst do notatek',
|
||||
'chat.textSelection.actions.addToChat': 'Dodaj do czatu',
|
||||
'chat.textSelection.actions.addToInput': 'Dodaj do pola wpisywania',
|
||||
'chat.textSelection.actions.comment': 'Skomentuj',
|
||||
'chat.textSelection.title.commentOnSelection': 'Skomentuj zaznaczenie',
|
||||
'chat.textSelection.comment.placeholder': 'Dodaj opcjonalny komentarz...',
|
||||
'chat.textSelection.comment.attach': 'Załącz',
|
||||
'chat.textSelection.actions.newSession': 'Nowa sesja',
|
||||
'chat.textSelection.actions.copy': 'Kopiuj',
|
||||
'chat.textSelection.actions.addToNotes': 'Dodaj do notatek',
|
||||
'chat.textSelection.title.addToCurrentChat': 'Dodaj do obecnego czatu',
|
||||
'chat.textSelection.title.newSessionWithSelection': 'Utwórz nową sesję z zaznaczeniem',
|
||||
@@ -1210,8 +1221,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.chatInput.draftPicker.projectTitle': 'Projekt',
|
||||
'chat.chatInput.draftPicker.searchProjects': 'Szukaj projektów...',
|
||||
'chat.chatInput.draftPicker.searchBranches': 'Szukaj gałęzi...',
|
||||
'chat.chatInput.devServerLogs': 'Dev Server logs:',
|
||||
'chat.chatInput.devServerLogsRemove': 'Usuń logi serwera deweloperskiego',
|
||||
'chat.chatInput.drop.attachFiles': 'Drop files here to attach',
|
||||
'chat.chatInput.drop.insertMention': 'Drop to insert as mention',
|
||||
'chat.chatInput.fileFallback': 'file',
|
||||
|
||||
@@ -6,6 +6,14 @@ export const dict: Record<I18nKey, string> = {
|
||||
'terminalView.actions.attachSelection': 'Anexar saída selecionada',
|
||||
'terminalView.actions.restart': 'Reiniciar terminal',
|
||||
'chat.message.terminalContext': '{terminal}, linhas {start}-{end}',
|
||||
'chat.message.context.codeComment': 'Comentário em {file}, linhas {start}-{end}',
|
||||
'chat.message.context.codeCommentLine': 'Comentário em {file}, linha {line}',
|
||||
'chat.message.context.chatQuote': 'Citação de uma mensagem anterior',
|
||||
'chat.chatInput.chatQuoteContext': 'Citações do chat',
|
||||
'chat.chatInput.chatQuoteContextRemove': 'Remover citações do chat',
|
||||
'chat.message.context.browserAnnotation': 'Anotação do navegador ({page})',
|
||||
'chat.message.context.prComment': 'Comentário de PR do GitHub ({label})',
|
||||
'chat.message.context.prCheck': 'Verificação de PR do GitHub com falha ({label})',
|
||||
'chat.chatInput.terminalContext': '{terminal}, linhas {start}-{end}',
|
||||
'chat.chatInput.terminalContextRemove': 'Remover contexto do terminal',
|
||||
'chat.chatInput.prCommentContext': 'Comentários do PR',
|
||||
@@ -2160,9 +2168,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.textSelection.toast.addToNotesFailed": "Não foi possível adicionar às notas",
|
||||
"chat.textSelection.toast.addToNotesSuccess": "Texto selecionado adicionado às notas",
|
||||
"chat.textSelection.toast.addToNotesSummaryFailed": "Não foi possível resumir a seleção; o texto selecionado foi adicionado às notas",
|
||||
"chat.textSelection.actions.addToChat": "Adicionar ao chat",
|
||||
"chat.textSelection.actions.addToInput": "Adicionar à entrada",
|
||||
"chat.textSelection.actions.comment": "Comentar",
|
||||
"chat.textSelection.title.commentOnSelection": "Comentar a seleção",
|
||||
"chat.textSelection.comment.placeholder": "Adicione um comentário opcional...",
|
||||
"chat.textSelection.comment.attach": "Anexar",
|
||||
"chat.textSelection.actions.newSession": "Nova sessão",
|
||||
"chat.textSelection.actions.copy": "Copiar",
|
||||
"chat.textSelection.actions.addToNotes": "Adicionar às notas",
|
||||
"chat.textSelection.title.addToCurrentChat": "Adicionar ao chat atual",
|
||||
"chat.textSelection.title.newSessionWithSelection": "Criar nova sessão com seleção",
|
||||
@@ -2260,8 +2271,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.chatInput.toast.togglePermissionAutoAcceptFailed": "Não foi possível alterar a aceitação automática de permissões",
|
||||
"chat.chatInput.reviewComments": "Comentários de revisão:",
|
||||
"chat.chatInput.reviewCommentsRemove": "Remover comentários de revisão",
|
||||
"chat.chatInput.devServerLogs": "Logs do Dev Server:",
|
||||
"chat.chatInput.devServerLogsRemove": "Remover logs do Dev Server",
|
||||
"chat.chatInput.previewAnnotations": "Anotações da visualização:",
|
||||
"chat.chatInput.previewContext": "Contexto da visualização:",
|
||||
"chat.chatInput.previewContextRemove": "Remover contexto da visualização",
|
||||
|
||||
@@ -6,6 +6,14 @@ export const dict: Record<I18nKey, string> = {
|
||||
'terminalView.actions.attachSelection': 'Прикріпити вибраний вивід',
|
||||
'terminalView.actions.restart': 'Перезапустити термінал',
|
||||
'chat.message.terminalContext': '{terminal}, рядки {start}-{end}',
|
||||
'chat.message.context.codeComment': 'Коментар до {file}, рядки {start}-{end}',
|
||||
'chat.message.context.codeCommentLine': 'Коментар до {file}, рядок {line}',
|
||||
'chat.message.context.chatQuote': 'Цитата з попереднього повідомлення',
|
||||
'chat.chatInput.chatQuoteContext': 'Цитати з чату',
|
||||
'chat.chatInput.chatQuoteContextRemove': 'Прибрати цитати з чату',
|
||||
'chat.message.context.browserAnnotation': 'Анотація браузера ({page})',
|
||||
'chat.message.context.prComment': 'Коментар PR GitHub ({label})',
|
||||
'chat.message.context.prCheck': 'Невдала перевірка PR GitHub ({label})',
|
||||
'chat.chatInput.terminalContext': '{terminal}, рядки {start}-{end}',
|
||||
'chat.chatInput.terminalContextRemove': 'Видалити контекст термінала',
|
||||
'chat.chatInput.prCommentContext': 'Коментарі PR',
|
||||
@@ -2160,9 +2168,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.textSelection.toast.addToNotesFailed": "Не вдалося додати до нотаток",
|
||||
"chat.textSelection.toast.addToNotesSuccess": "Вибраний текст додано до нотаток",
|
||||
"chat.textSelection.toast.addToNotesSummaryFailed": "Не вдалося підсумувати виділення, виділений текст додано до нотаток",
|
||||
"chat.textSelection.actions.addToChat": "Додати в чат",
|
||||
"chat.textSelection.actions.addToInput": "Додати в поле вводу",
|
||||
"chat.textSelection.actions.comment": "Коментувати",
|
||||
"chat.textSelection.title.commentOnSelection": "Коментувати виділене",
|
||||
"chat.textSelection.comment.placeholder": "Додайте коментар за бажанням...",
|
||||
"chat.textSelection.comment.attach": "Прикріпити",
|
||||
"chat.textSelection.actions.newSession": "Нова сесія",
|
||||
"chat.textSelection.actions.copy": "Копіювати",
|
||||
"chat.textSelection.actions.addToNotes": "Додати до нотаток",
|
||||
"chat.textSelection.title.addToCurrentChat": "Додати до поточного чату",
|
||||
"chat.textSelection.title.newSessionWithSelection": "Створити нову сесію із виділенням",
|
||||
@@ -2260,8 +2271,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.chatInput.toast.togglePermissionAutoAcceptFailed": "Не вдалося ввімкнути автоматичне прийняття дозволів",
|
||||
"chat.chatInput.reviewComments": "Коментарі рев’ю:",
|
||||
"chat.chatInput.reviewCommentsRemove": "Прибрати коментарі рев’ю",
|
||||
"chat.chatInput.devServerLogs": "Логи Dev Server:",
|
||||
"chat.chatInput.devServerLogsRemove": "Прибрати логи Dev Server",
|
||||
"chat.chatInput.previewAnnotations": "Анотації перегляду:",
|
||||
"chat.chatInput.previewContext": "Контекст перегляду:",
|
||||
"chat.chatInput.previewContextRemove": "Прибрати контекст перегляду",
|
||||
|
||||
@@ -6,6 +6,14 @@ export const dict: Record<I18nKey, string> = {
|
||||
'terminalView.actions.attachSelection': '附加所选输出',
|
||||
'terminalView.actions.restart': '重启终端',
|
||||
'chat.message.terminalContext': '{terminal},第 {start}-{end} 行',
|
||||
'chat.message.context.codeComment': '对 {file} 第 {start}-{end} 行的评论',
|
||||
'chat.message.context.codeCommentLine': '对 {file} 第 {line} 行的评论',
|
||||
'chat.message.context.chatQuote': '引用自先前的消息',
|
||||
'chat.chatInput.chatQuoteContext': '聊天引用',
|
||||
'chat.chatInput.chatQuoteContextRemove': '移除聊天引用',
|
||||
'chat.message.context.browserAnnotation': '浏览器标注({page})',
|
||||
'chat.message.context.prComment': 'GitHub PR 评论({label})',
|
||||
'chat.message.context.prCheck': '失败的 GitHub PR 检查({label})',
|
||||
'chat.chatInput.terminalContext': '{terminal},第 {start}-{end} 行',
|
||||
'chat.chatInput.terminalContextRemove': '移除终端上下文',
|
||||
'chat.chatInput.prCommentContext': 'PR 评论',
|
||||
@@ -2148,9 +2156,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.textSelection.toast.addToNotesFailed': '添加到笔记失败',
|
||||
'chat.textSelection.toast.addToNotesSuccess': '已将选中文本添加到笔记',
|
||||
'chat.textSelection.toast.addToNotesSummaryFailed': '无法总结所选内容,已将所选文本添加到笔记',
|
||||
'chat.textSelection.actions.addToChat': '添加到聊天',
|
||||
'chat.textSelection.actions.addToInput': '添加到输入框',
|
||||
'chat.textSelection.actions.comment': '评论',
|
||||
'chat.textSelection.title.commentOnSelection': '评论所选内容',
|
||||
'chat.textSelection.comment.placeholder': '添加可选评论...',
|
||||
'chat.textSelection.comment.attach': '附加',
|
||||
'chat.textSelection.actions.newSession': '新建会话',
|
||||
'chat.textSelection.actions.copy': '复制',
|
||||
'chat.textSelection.actions.addToNotes': '添加到笔记',
|
||||
'chat.textSelection.title.addToCurrentChat': '添加到当前聊天',
|
||||
'chat.textSelection.title.newSessionWithSelection': '使用选中内容创建新会话',
|
||||
@@ -2260,8 +2271,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.chatInput.toast.togglePermissionAutoAcceptFailed': '切换权限自动接受失败',
|
||||
'chat.chatInput.reviewComments': '审查评论:',
|
||||
'chat.chatInput.reviewCommentsRemove': '移除审查评论',
|
||||
'chat.chatInput.devServerLogs': 'Dev Server 日志:',
|
||||
'chat.chatInput.devServerLogsRemove': '移除 Dev Server 日志',
|
||||
'chat.chatInput.previewAnnotations': '预览注释:',
|
||||
'chat.chatInput.previewContext': '预览上下文:',
|
||||
'chat.chatInput.previewContextRemove': '移除预览上下文',
|
||||
|
||||
@@ -6,6 +6,14 @@ export const dict: Record<I18nKey, string> = {
|
||||
'terminalView.actions.attachSelection': '附加所選輸出',
|
||||
'terminalView.actions.restart': '重新啟動終端',
|
||||
'chat.message.terminalContext': '{terminal},第 {start}-{end} 行',
|
||||
'chat.message.context.codeComment': '對 {file} 第 {start}-{end} 行的評論',
|
||||
'chat.message.context.codeCommentLine': '對 {file} 第 {line} 行的評論',
|
||||
'chat.message.context.chatQuote': '引用自先前的訊息',
|
||||
'chat.chatInput.chatQuoteContext': '聊天引用',
|
||||
'chat.chatInput.chatQuoteContextRemove': '移除聊天引用',
|
||||
'chat.message.context.browserAnnotation': '瀏覽器標註({page})',
|
||||
'chat.message.context.prComment': 'GitHub PR 留言({label})',
|
||||
'chat.message.context.prCheck': '失敗的 GitHub PR 檢查({label})',
|
||||
'chat.chatInput.terminalContext': '{terminal},第 {start}-{end} 行',
|
||||
'chat.chatInput.terminalContextRemove': '移除終端上下文',
|
||||
'chat.chatInput.prCommentContext': 'PR 留言',
|
||||
@@ -2152,9 +2160,12 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.textSelection.toast.addToNotesFailed': '加入筆記失敗',
|
||||
'chat.textSelection.toast.addToNotesSuccess': '已將選取文字加入筆記',
|
||||
'chat.textSelection.toast.addToNotesSummaryFailed': '無法總結所選內容,已將所選文字加入筆記',
|
||||
'chat.textSelection.actions.addToChat': '加入聊天',
|
||||
'chat.textSelection.actions.addToInput': '加入輸入框',
|
||||
'chat.textSelection.actions.comment': '留言',
|
||||
'chat.textSelection.title.commentOnSelection': '對所選內容留言',
|
||||
'chat.textSelection.comment.placeholder': '新增選填留言...',
|
||||
'chat.textSelection.comment.attach': '附加',
|
||||
'chat.textSelection.actions.newSession': '新增會話',
|
||||
'chat.textSelection.actions.copy': '複製',
|
||||
'chat.textSelection.actions.addToNotes': '加入筆記',
|
||||
'chat.textSelection.title.addToCurrentChat': '加入目前聊天',
|
||||
'chat.textSelection.title.newSessionWithSelection': '使用選取內容建立新會話',
|
||||
@@ -2264,8 +2275,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.chatInput.toast.togglePermissionAutoAcceptFailed': '切換權限自動接受失敗',
|
||||
'chat.chatInput.reviewComments': '審查留言:',
|
||||
'chat.chatInput.reviewCommentsRemove': '移除審查留言',
|
||||
'chat.chatInput.devServerLogs': 'Dev Server 日誌:',
|
||||
'chat.chatInput.devServerLogsRemove': '移除 Dev Server 日誌',
|
||||
'chat.chatInput.previewAnnotations': '預覽註釋:',
|
||||
'chat.chatInput.previewContext': '預覽上下文:',
|
||||
'chat.chatInput.previewContextRemove': '移除預覽上下文',
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import type { InlineCommentDraft } from '@/stores/useInlineCommentDraftStore';
|
||||
import {
|
||||
CONTEXT_METADATA_KEY,
|
||||
contextPayloadFromDraft,
|
||||
createContextPart,
|
||||
formatContextText,
|
||||
readContextPart,
|
||||
type ContextPartPayload,
|
||||
} from './contextParts';
|
||||
|
||||
const draft = (overrides: Partial<InlineCommentDraft> = {}): InlineCommentDraft => ({
|
||||
id: 'icd-1',
|
||||
sessionKey: 's1',
|
||||
source: 'diff',
|
||||
fileLabel: 'src/app.ts',
|
||||
startLine: 3,
|
||||
endLine: 5,
|
||||
side: 'modified',
|
||||
code: 'const x = 1;',
|
||||
language: 'ts',
|
||||
text: 'fix this',
|
||||
createdAt: 1,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('model-facing text', () => {
|
||||
test('diff comments keep the pre-metadata wording, including the side', () => {
|
||||
expect(formatContextText(contextPayloadFromDraft(draft())))
|
||||
.toBe('Comment on `src/app.ts` lines 3-5 (modified):\n```ts\nconst x = 1;\n```\n\nfix this');
|
||||
});
|
||||
|
||||
test('file and plan comments omit the side', () => {
|
||||
expect(formatContextText(contextPayloadFromDraft(draft({ source: 'file', side: undefined }))))
|
||||
.toBe('Comment on `src/app.ts` lines 3-5:\n```ts\nconst x = 1;\n```\n\nfix this');
|
||||
});
|
||||
|
||||
test('terminal selections keep the terminal_context envelope', () => {
|
||||
const payload = contextPayloadFromDraft(draft({
|
||||
source: 'terminal',
|
||||
fileLabel: 'Terminal 1',
|
||||
terminalId: 'term-1',
|
||||
language: '',
|
||||
startLine: 12,
|
||||
endLine: 13,
|
||||
code: 'npm run build\nok',
|
||||
text: '',
|
||||
}));
|
||||
expect(formatContextText(payload)).toBe([
|
||||
'<terminal_context>',
|
||||
'- Terminal 1 lines 12-13:',
|
||||
' 12 | npm run build',
|
||||
' 13 | ok',
|
||||
'</terminal_context>',
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
test('annotations send the prompt, with user text appended when present', () => {
|
||||
const base = draft({ source: 'preview-annotation', fileLabel: 'https://app.dev', code: 'prompt body', text: '' });
|
||||
expect(formatContextText(contextPayloadFromDraft(base))).toBe('prompt body');
|
||||
expect(formatContextText(contextPayloadFromDraft({ ...base, text: 'also this' })))
|
||||
.toBe('prompt body\n\nalso this');
|
||||
});
|
||||
|
||||
test('chat quotes send the fragment as a blockquote with the comment below', () => {
|
||||
expect(formatContextText(contextPayloadFromDraft(draft({ source: 'chat-quote', fileLabel: 'msg_1', code: 'first line\nsecond line', text: 'why so?' }))))
|
||||
.toBe('Comment on this fragment of an earlier message in this conversation:\n> first line\n> second line\n\nwhy so?');
|
||||
});
|
||||
|
||||
test('PR comments and checks keep their attachment wording', () => {
|
||||
expect(formatContextText(contextPayloadFromDraft(draft({ source: 'pr-comment', fileLabel: 'octo/repo#7', code: 'the comment', text: '' }))))
|
||||
.toBe('Attached GitHub PR comment (octo/repo#7):\n\nthe comment');
|
||||
expect(formatContextText(contextPayloadFromDraft(draft({ source: 'pr-check', fileLabel: 'CI / build', code: 'boom', text: 'why?' }))))
|
||||
.toBe('Attached failed GitHub PR check (CI / build):\n```\nboom\n```\n\nwhy?');
|
||||
});
|
||||
});
|
||||
|
||||
describe('round-trip through part metadata', () => {
|
||||
const asPart = (payload: ContextPartPayload, text?: string) => ({
|
||||
type: 'text',
|
||||
...createContextPart(payload, text),
|
||||
});
|
||||
|
||||
test('every draft-based kind survives create → read unchanged', () => {
|
||||
const payloads = [
|
||||
contextPayloadFromDraft(draft()),
|
||||
contextPayloadFromDraft(draft({ source: 'plan', side: undefined })),
|
||||
contextPayloadFromDraft(draft({ source: 'terminal', terminalId: 'term-1', language: '' })),
|
||||
contextPayloadFromDraft(draft({ source: 'preview-annotation' })),
|
||||
contextPayloadFromDraft(draft({ source: 'pr-comment' })),
|
||||
contextPayloadFromDraft(draft({ source: 'pr-check' })),
|
||||
contextPayloadFromDraft(draft({ source: 'chat-quote', fileLabel: 'msg_1' })),
|
||||
];
|
||||
for (const payload of payloads) {
|
||||
expect(readContextPart(asPart(payload))).toEqual(payload);
|
||||
}
|
||||
});
|
||||
|
||||
test('github references carry picker-built text and structured identity', () => {
|
||||
const payload: ContextPartPayload = { kind: 'github-issue', number: 3, title: 'Bug', url: 'https://x/issues/3' };
|
||||
const part = asPart(payload, 'GitHub issue context (JSON)\n{}');
|
||||
expect(part.text).toBe('GitHub issue context (JSON)\n{}');
|
||||
expect(readContextPart(part)).toEqual(payload);
|
||||
});
|
||||
|
||||
test('non-text parts, missing metadata, and malformed payloads read as null', () => {
|
||||
expect(readContextPart({ type: 'file', metadata: {} })).toBeNull();
|
||||
expect(readContextPart({ type: 'text' })).toBeNull();
|
||||
expect(readContextPart({ type: 'text', metadata: { [CONTEXT_METADATA_KEY]: { kind: 'nope' } } })).toBeNull();
|
||||
expect(readContextPart({
|
||||
type: 'text',
|
||||
metadata: { [CONTEXT_METADATA_KEY]: { kind: 'terminal', terminalId: 1, terminalLabel: 'x', startLine: 1, endLine: 1, output: '' } },
|
||||
})).toBeNull();
|
||||
expect(readContextPart({
|
||||
type: 'text',
|
||||
metadata: { [CONTEXT_METADATA_KEY]: { kind: 'github-issue', number: 0, title: 't', url: 'u' } },
|
||||
})).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* Structured context attached to an outgoing message.
|
||||
*
|
||||
* Every user-attached context item — an inline code comment, a terminal
|
||||
* selection, a browser annotation, a GitHub PR comment or failed check, a
|
||||
* linked issue or PR — is sent as its own synthetic text part. The part's
|
||||
* `text` is what the model reads; the part's `metadata[CONTEXT_METADATA_KEY]`
|
||||
* carries the same information structured, so the timeline can render the
|
||||
* context as a dedicated block after the message round-trips through the
|
||||
* OpenCode server (which persists part metadata verbatim).
|
||||
*
|
||||
* This module owns both directions: building the part at send time and
|
||||
* parsing the metadata back at render time. Keeping them together is what
|
||||
* guarantees they cannot drift apart.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { TextPart } from '@opencode-ai/sdk/v2';
|
||||
import type { InlineCommentDraft } from '@/stores/useInlineCommentDraftStore';
|
||||
import { appendTerminalContexts } from './terminalContext';
|
||||
|
||||
export const CONTEXT_METADATA_KEY = 'openchamberContext';
|
||||
|
||||
export type CodeCommentContext = {
|
||||
kind: 'code-comment';
|
||||
source: 'diff' | 'file' | 'plan';
|
||||
fileLabel: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
side?: 'original' | 'modified';
|
||||
language: string;
|
||||
code: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type TerminalContextPayload = {
|
||||
kind: 'terminal';
|
||||
terminalId: string;
|
||||
terminalLabel: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
output: string;
|
||||
};
|
||||
|
||||
type BrowserAnnotationContext = {
|
||||
kind: 'browser-annotation';
|
||||
pageUrl: string;
|
||||
/** The full annotation prompt shown to the model. */
|
||||
prompt: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type PrCommentContext = {
|
||||
kind: 'pr-comment';
|
||||
label: string;
|
||||
body: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type PrCheckContext = {
|
||||
kind: 'pr-check';
|
||||
label: string;
|
||||
output: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type GitHubIssueContext = {
|
||||
kind: 'github-issue';
|
||||
number: number;
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type ChatQuoteContext = {
|
||||
kind: 'chat-quote';
|
||||
/** The message the quote came from, when known. */
|
||||
messageId?: string;
|
||||
quote: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type GitHubPrContext = {
|
||||
kind: 'github-pr';
|
||||
number: number;
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type ContextPartPayload =
|
||||
| CodeCommentContext
|
||||
| TerminalContextPayload
|
||||
| BrowserAnnotationContext
|
||||
| PrCommentContext
|
||||
| PrCheckContext
|
||||
| ChatQuoteContext
|
||||
| GitHubIssueContext
|
||||
| GitHubPrContext;
|
||||
|
||||
export type ContextPartMetadata = { [K in typeof CONTEXT_METADATA_KEY]: ContextPartPayload };
|
||||
|
||||
export type ContextPart = {
|
||||
text: string;
|
||||
synthetic: true;
|
||||
metadata: ContextPartMetadata;
|
||||
};
|
||||
|
||||
/**
|
||||
* The model-facing text for a context payload. The wording intentionally
|
||||
* matches what OpenChamber sent before parts carried metadata, so model
|
||||
* behavior does not change with the transport format.
|
||||
*/
|
||||
export function formatContextText(payload: ContextPartPayload): string {
|
||||
switch (payload.kind) {
|
||||
case 'code-comment': {
|
||||
const range = `lines ${payload.startLine}-${payload.endLine}`;
|
||||
const sideNote = payload.source === 'diff' && payload.side ? ` (${payload.side})` : '';
|
||||
return `Comment on \`${payload.fileLabel}\` ${range}${sideNote}:\n\`\`\`${payload.language}\n${payload.code}\n\`\`\`\n\n${payload.text}`;
|
||||
}
|
||||
case 'terminal':
|
||||
return appendTerminalContexts('', [{
|
||||
terminalId: payload.terminalId,
|
||||
terminalLabel: payload.terminalLabel,
|
||||
startLine: payload.startLine,
|
||||
endLine: payload.endLine,
|
||||
text: payload.output,
|
||||
}]);
|
||||
case 'browser-annotation':
|
||||
return payload.text ? `${payload.prompt}\n\n${payload.text}` : payload.prompt;
|
||||
case 'pr-comment':
|
||||
return `Attached GitHub PR comment (${payload.label}):\n\n${payload.body}${payload.text ? `\n\n${payload.text}` : ''}`;
|
||||
case 'chat-quote': {
|
||||
const quoted = payload.quote.split('\n').map((line) => `> ${line}`).join('\n');
|
||||
return `Comment on this fragment of an earlier message in this conversation:\n${quoted}${payload.text ? `\n\n${payload.text}` : ''}`;
|
||||
}
|
||||
case 'pr-check':
|
||||
return `Attached failed GitHub PR check (${payload.label}):\n\`\`\`\n${payload.output}\n\`\`\`${payload.text ? `\n\n${payload.text}` : ''}`;
|
||||
case 'github-issue':
|
||||
case 'github-pr':
|
||||
// Linked issues/PRs carry server-fetched context text built by
|
||||
// their pickers; there is no default text to derive here.
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the synthetic part for one context payload. `text` overrides the
|
||||
* derived text; github-issue/github-pr payloads require it because their
|
||||
* model-facing context is fetched by the picker, not derived from metadata.
|
||||
*/
|
||||
export function createContextPart(payload: ContextPartPayload, text?: string): ContextPart {
|
||||
const resolvedText = text ?? formatContextText(payload);
|
||||
return {
|
||||
text: resolvedText,
|
||||
synthetic: true,
|
||||
metadata: { [CONTEXT_METADATA_KEY]: payload },
|
||||
};
|
||||
}
|
||||
|
||||
/** Map a composer context draft to its structured payload. */
|
||||
export function contextPayloadFromDraft(draft: InlineCommentDraft): ContextPartPayload {
|
||||
switch (draft.source) {
|
||||
case 'terminal':
|
||||
return {
|
||||
kind: 'terminal',
|
||||
terminalId: draft.terminalId ?? '',
|
||||
terminalLabel: draft.fileLabel,
|
||||
startLine: draft.startLine,
|
||||
endLine: draft.endLine,
|
||||
output: draft.code,
|
||||
};
|
||||
case 'preview-annotation':
|
||||
return {
|
||||
kind: 'browser-annotation',
|
||||
pageUrl: draft.fileLabel,
|
||||
prompt: draft.code,
|
||||
text: draft.text,
|
||||
};
|
||||
case 'pr-comment':
|
||||
return { kind: 'pr-comment', label: draft.fileLabel, body: draft.code, text: draft.text };
|
||||
case 'pr-check':
|
||||
return { kind: 'pr-check', label: draft.fileLabel, output: draft.code, text: draft.text };
|
||||
case 'chat-quote': {
|
||||
const payload: ChatQuoteContext = { kind: 'chat-quote', quote: draft.code, text: draft.text };
|
||||
if (draft.fileLabel) payload.messageId = draft.fileLabel;
|
||||
return payload;
|
||||
}
|
||||
case 'diff':
|
||||
case 'file':
|
||||
case 'plan': {
|
||||
const payload: CodeCommentContext = {
|
||||
kind: 'code-comment',
|
||||
source: draft.source,
|
||||
fileLabel: draft.fileLabel,
|
||||
startLine: draft.startLine,
|
||||
endLine: draft.endLine,
|
||||
language: draft.language,
|
||||
code: draft.code,
|
||||
text: draft.text,
|
||||
};
|
||||
if (draft.source === 'diff' && draft.side) payload.side = draft.side;
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read-back: parsing part metadata at the display boundary
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const contextPayloadSchema = z.discriminatedUnion('kind', [
|
||||
z.object({
|
||||
kind: z.literal('code-comment'),
|
||||
source: z.enum(['diff', 'file', 'plan']),
|
||||
fileLabel: z.string(),
|
||||
startLine: z.number(),
|
||||
endLine: z.number(),
|
||||
side: z.enum(['original', 'modified']).optional(),
|
||||
language: z.string(),
|
||||
code: z.string(),
|
||||
text: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('terminal'),
|
||||
terminalId: z.string(),
|
||||
terminalLabel: z.string(),
|
||||
startLine: z.number(),
|
||||
endLine: z.number(),
|
||||
output: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('browser-annotation'),
|
||||
pageUrl: z.string(),
|
||||
prompt: z.string(),
|
||||
text: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('pr-comment'),
|
||||
label: z.string(),
|
||||
body: z.string(),
|
||||
text: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('pr-check'),
|
||||
label: z.string(),
|
||||
output: z.string(),
|
||||
text: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('chat-quote'),
|
||||
messageId: z.string().optional(),
|
||||
quote: z.string(),
|
||||
text: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('github-issue'),
|
||||
number: z.number().int().positive(),
|
||||
title: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('github-pr'),
|
||||
number: z.number().int().positive(),
|
||||
title: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
/** The subset of a message part that context read-back inspects. */
|
||||
export type ContextCarrierPart = { type: string } & Pick<TextPart, 'metadata'>;
|
||||
|
||||
/**
|
||||
* Read the structured context payload from a message part, if it carries one.
|
||||
* The part comes from the server or an optimistic insert, so the payload is
|
||||
* schema-validated before it is trusted.
|
||||
*/
|
||||
export function readContextPart(part: ContextCarrierPart): ContextPartPayload | null {
|
||||
if (part.type !== 'text') return null;
|
||||
const parsed = contextPayloadSchema.safeParse(part.metadata?.[CONTEXT_METADATA_KEY]);
|
||||
return parsed.success ? parsed.data : null;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import type { InlineCommentDraft } from '@/stores/useInlineCommentDraftStore';
|
||||
import { appendTerminalContexts } from './terminalContext';
|
||||
|
||||
/**
|
||||
* Format a single inline comment draft into the standard message format
|
||||
* used by diff, plan, and file viewers
|
||||
*/
|
||||
function formatInlineCommentDraft(draft: InlineCommentDraft): string {
|
||||
const { fileLabel, startLine, endLine, side, language, code, text } = draft;
|
||||
|
||||
// Diff format includes side (original/modified)
|
||||
if (draft.source === 'diff' && side) {
|
||||
return `Comment on \`${fileLabel}\` lines ${startLine}-${endLine} (${side}):\n\`\`\`${language}\n${code}\n\`\`\`\n\n${text}`;
|
||||
}
|
||||
|
||||
if (draft.source === 'preview-console') {
|
||||
return `Attached preview context from \`${fileLabel}\`:\n\`\`\`${language}\n${code}\n\`\`\`\n\n${text}`;
|
||||
}
|
||||
|
||||
if (draft.source === 'preview-annotation') {
|
||||
return text ? `${code}\n\n${text}` : code;
|
||||
}
|
||||
|
||||
if (draft.source === 'pr-comment') {
|
||||
return `Attached GitHub PR comment (${fileLabel}):\n\n${code}${text ? `\n\n${text}` : ''}`;
|
||||
}
|
||||
|
||||
if (draft.source === 'pr-check') {
|
||||
return `Attached failed GitHub PR check (${fileLabel}):\n\`\`\`\n${code}\n\`\`\`${text ? `\n\n${text}` : ''}`;
|
||||
}
|
||||
|
||||
// Plan and file format (no side)
|
||||
return `Comment on \`${fileLabel}\` lines ${startLine}-${endLine}:\n\`\`\`${language}\n${code}\n\`\`\`\n\n${text}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format multiple inline comment drafts into a single string
|
||||
* with each comment separated by a blank line
|
||||
*/
|
||||
function formatInlineCommentDrafts(drafts: InlineCommentDraft[]): string {
|
||||
if (drafts.length === 0) return '';
|
||||
|
||||
if (drafts.every((draft) => draft.source === 'preview-annotation')) {
|
||||
return drafts.map(formatInlineCommentDraft).join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
return drafts.map(formatInlineCommentDraft).join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Append inline comment drafts to an existing message text
|
||||
* If the text is empty, returns just the formatted comments
|
||||
* Otherwise, appends comments after a blank line separator
|
||||
*/
|
||||
export function appendInlineComments(text: string, drafts: InlineCommentDraft[]): string {
|
||||
if (drafts.length === 0) return text;
|
||||
const terminalDrafts = drafts.filter((draft) => draft.source === 'terminal');
|
||||
const otherDrafts = drafts.filter((draft) => draft.source !== 'terminal');
|
||||
const withComments = otherDrafts.length > 0
|
||||
? (text.trim() ? `${text}\n\n${formatInlineCommentDrafts(otherDrafts)}` : formatInlineCommentDrafts(otherDrafts))
|
||||
: text;
|
||||
if (terminalDrafts.length > 0) {
|
||||
return appendTerminalContexts(withComments, terminalDrafts.map((draft) => ({
|
||||
terminalId: draft.language,
|
||||
terminalLabel: draft.fileLabel,
|
||||
startLine: draft.startLine,
|
||||
endLine: draft.endLine,
|
||||
text: draft.code,
|
||||
})));
|
||||
}
|
||||
return withComments;
|
||||
}
|
||||
@@ -120,4 +120,26 @@ describe("filterSyntheticParts", () => {
|
||||
]
|
||||
expect(filterSyntheticParts(parts)).toEqual(parts)
|
||||
})
|
||||
|
||||
test("keeps synthetic parts carrying user context metadata alongside user text", () => {
|
||||
const userPart = createTextPart("1", "user prompt")
|
||||
const contextPart = {
|
||||
...createTextPart("2", "Comment on `x.ts` lines 1-2:\n```ts\ncode\n```\n\nfix", true),
|
||||
metadata: {
|
||||
openchamberContext: {
|
||||
kind: "code-comment",
|
||||
source: "diff",
|
||||
fileLabel: "x.ts",
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
language: "ts",
|
||||
code: "code",
|
||||
text: "fix",
|
||||
},
|
||||
},
|
||||
}
|
||||
const plainSynthetic = createTextPart("3", "instructions", true)
|
||||
expect(filterSyntheticParts([userPart, contextPart, plainSynthetic]))
|
||||
.toEqual([userPart, contextPart])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Part } from "@opencode-ai/sdk/v2";
|
||||
|
||||
import { readContextPart } from "./contextParts";
|
||||
|
||||
const GITHUB_ISSUE_CONTEXT_PREFIX = 'GitHub issue context (JSON)';
|
||||
const GITHUB_PR_CONTEXT_PREFIX = 'GitHub pull request context (JSON)';
|
||||
|
||||
@@ -39,6 +41,13 @@ export const filterSyntheticParts = (parts: Part[] | undefined): Part[] => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// User-attached context (inline comments, terminal selections, and
|
||||
// such) is synthetic transport-wise but is user content that renders
|
||||
// as its own context block.
|
||||
if (readContextPart(part)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const text = (part as { text?: unknown }).text;
|
||||
if (typeof text !== 'string') {
|
||||
return false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ContextPartMetadata } from '@/lib/messages/contextParts';
|
||||
import { createOpencodeClient, OpencodeClient } from "@opencode-ai/sdk/v2";
|
||||
import type { PermissionV2Request, PermissionV2Effect, PermissionV2Source } from "@opencode-ai/sdk/v2/client";
|
||||
import type { FilesAPI } from "../api/types";
|
||||
@@ -792,6 +793,7 @@ class OpencodeService {
|
||||
additionalParts?: Array<{
|
||||
text: string;
|
||||
synthetic?: boolean;
|
||||
metadata?: ContextPartMetadata;
|
||||
files?: Array<FileInputLite>;
|
||||
}>;
|
||||
messageId?: string;
|
||||
@@ -842,11 +844,10 @@ class OpencodeService {
|
||||
if (params.additionalParts && params.additionalParts.length > 0) {
|
||||
for (const additional of params.additionalParts) {
|
||||
if (additional.text && additional.text.trim()) {
|
||||
parts.push({
|
||||
type: 'text',
|
||||
text: additional.text,
|
||||
...(additional.synthetic ? { synthetic: true } : {}),
|
||||
});
|
||||
const additionalTextPart: TextPartInput = { type: 'text', text: additional.text };
|
||||
if (additional.synthetic) additionalTextPart.synthetic = true;
|
||||
if (additional.metadata) additionalTextPart.metadata = additional.metadata;
|
||||
parts.push(additionalTextPart);
|
||||
}
|
||||
if (additional.files && additional.files.length > 0) {
|
||||
for (const file of additional.files) {
|
||||
@@ -1193,7 +1194,7 @@ class OpencodeService {
|
||||
options?: {
|
||||
id?: string;
|
||||
save?: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
metadata?: ContextPartMetadata;
|
||||
source?: PermissionV2Source;
|
||||
agent?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user