Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 111 additions & 10 deletions extensions/github1s-ai/assets/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
summary:focus-visible {
summary:focus-visible,
[tabindex='0']:focus-visible {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 1px;
}
Expand Down Expand Up @@ -701,13 +702,18 @@ summary:focus-visible {
}

.user-request {
max-width: 88%;
min-width: 0;
padding: 8px 10px;
background: var(--vscode-chat-requestBubbleBackground, var(--vscode-list-hoverBackground));
border: 1px solid var(--vscode-chat-requestBubbleBorder, transparent);
border-radius: 9px;
}

.message-user > .user-request,
.message-user > .tooltip-anchor {
max-width: 88%;
}

.markdown > :first-child {
margin-top: 0;
}
Expand Down Expand Up @@ -1230,11 +1236,48 @@ summary:focus-visible {
gap: 6px;
}

.recent-files-action {
position: relative;
}

.recent-files-badge {
position: absolute;
top: 3px;
right: 3px;
display: grid;
width: 8px;
height: 8px;
place-items: center;
/* An opaque base hides the icon when the input background is translucent. */
background-color: var(--vscode-sideBar-background);
background-image: linear-gradient(var(--vscode-input-background), var(--vscode-input-background));
border-radius: 50%;
font-size: 8px;
font-weight: 600;
line-height: 1;
pointer-events: none;
}

.recent-files-action[aria-pressed='false'] .recent-files-badge {
box-sizing: border-box;
border: 1px solid currentColor;
transform: scale(0.9);
}

.recent-files-action[aria-pressed='false'] .recent-files-badge::after {
width: 1px;
height: 6px;
background: currentColor;
transform: rotate(45deg);
content: '';
}

/* Attachments */

.attachment-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 3px;
padding-bottom: 4px;
}
Expand Down Expand Up @@ -1262,11 +1305,12 @@ summary:focus-visible {
line-height: 1.2;
}

.attachment-name {
gap: 3px;
}

.attachment-label {
display: inline-flex;
height: 14px;
min-width: 0;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
line-height: 14px;
Expand Down Expand Up @@ -1387,6 +1431,62 @@ body.vscode-high-contrast-light .seti-file-icon {

/* Shared controls */

.attachment-name,
.tooltip-anchor {
display: inline-flex;
min-width: 0;
align-items: center;
}

.tooltip-anchor {
max-width: 100%;
}

.tooltip-anchor > :disabled {
pointer-events: none;
}

.tooltip {
position: fixed;
z-index: 100;
box-sizing: border-box;
width: max-content;
max-width: min(320px, calc(100vw - 16px));
max-height: calc(100vh - 16px);
overflow: auto;
padding: 4px 6px;
color: var(--vscode-editorHoverWidget-foreground, var(--vscode-foreground));
background: var(--vscode-editorHoverWidget-background, var(--vscode-sideBar-background));
border: 1px solid var(--vscode-editorHoverWidget-border, var(--vscode-widget-border, transparent));
border-radius: 4px;
box-shadow: 0 2px 8px var(--vscode-widget-shadow, rgba(0, 0, 0, 0.2));
font-size: 11px;
line-height: 1.45;
white-space: pre-wrap;
overflow-wrap: anywhere;
}

.tooltip-content {
display: grid;
gap: 2px;
white-space: normal;
}

.tooltip-content strong {
font-weight: 600;
}

.tooltip-content ul {
margin: 0;
padding: 0 0 0 14px;
list-style: disc;
white-space: pre-wrap;
}

.tooltip-hint {
color: var(--vscode-descriptionForeground);
}

.icon {
width: 16px;
height: 16px;
Expand Down Expand Up @@ -1485,6 +1585,10 @@ body.vscode-high-contrast-light .seti-file-icon {
white-space: nowrap;
}

.model-config-picker > .tooltip-anchor {
width: 100%;
}

.model-config-picker:hover .model-config-select:not(:disabled),
.model-config-select:focus {
background-color: var(--vscode-toolbar-hoverBackground, var(--vscode-list-hoverBackground));
Expand All @@ -1508,10 +1612,6 @@ body.vscode-high-contrast-light .seti-file-icon {
transform: rotate(90deg);
}

.model-config-select:disabled + .model-config-chevron {
opacity: 0.6;
}

.submit-button {
display: inline-flex;
width: 24px;
Expand Down Expand Up @@ -1635,7 +1735,8 @@ body.vscode-high-contrast-light .seti-file-icon {
.assistant-activity-body,
.settings-security,
.settings-card,
.settings-control {
.settings-control,
.tooltip {
border-color: CanvasText;
}
}
3 changes: 3 additions & 0 deletions extensions/github1s-ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"icon": "assets/extension.png",
"private": true,
"browser": "./dist/extension",
"activationEvents": [
"onStartupFinished"
],
"engines": {
"vscode": "^1.132.0"
},
Expand Down
2 changes: 1 addition & 1 deletion extensions/github1s-ai/src/chat-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
type MarkdownHighlightRequest,
type ViewMessage,
} from '@/common/protocol';
import { currentFile, currentSelection } from '@/contexts';
import { Controllers } from '@/controllers';
import { currentFile, currentSelection } from '@/controllers/context';
import { computeSyntaxHighlighting, onDidChangeSyntaxHighlighting } from '@/helpers/highlighting';

interface ChatHtmlOptions {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
import { isPlainObject } from 'lodash-es';

import { hasExactKeys } from '@/helpers/validate';

export type ContextAttachmentAction = 'selectFile' | 'currentFile' | 'currentSelection';

export interface ContextAttachmentDescriptor {
export interface ContextReference {
source: string;
}

export interface ContextAttachmentDescriptor extends ContextReference {
id: string;
type: 'file' | 'selection';
label: string;
source: string;
languageId?: string;
}

export interface ContextAttachment extends ContextAttachmentDescriptor {
content: string;
}

export const contextReferencePath = ({ source }: ContextReference): string => {
try {
// GitHub1s workspaces are rooted at the repository's URI path '/'.
return decodeURIComponent(new URL(source).pathname).replace(/^\//, '');
} catch {
return source;
}
};

export const fileName = (path: string): string =>
path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1);

export const isContextReference = (value: unknown): value is ContextReference => {
if (!isPlainObject(value)) return false;
const reference = value as Record<string, unknown>;
return hasExactKeys(reference, ['source']) && typeof reference.source === 'string' && reference.source.length > 0;
};

export const isContextAttachmentAction = (value: unknown): value is ContextAttachmentAction =>
value === 'selectFile' || value === 'currentFile' || value === 'currentSelection';

Expand All @@ -31,3 +54,9 @@ export const isContextAttachmentDescriptor = (value: unknown): value is ContextA
(descriptor.languageId === undefined || typeof descriptor.languageId === 'string')
);
};

export const isContextAttachment = (value: unknown): value is ContextAttachment => {
if (!isPlainObject(value)) return false;
const { content, ...descriptor } = value as Record<string, unknown>;
return typeof content === 'string' && isContextAttachmentDescriptor(descriptor);
};
78 changes: 35 additions & 43 deletions extensions/github1s-ai/src/common/conversation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { jsonSchema, validateUIMessages, type LanguageModelUsage, type UIMessage } from 'ai';
import { isPlainObject } from 'lodash-es';

import type { ContextAttachment } from '@/contexts/types';
import { isContextAttachment, isContextReference, type ContextAttachment, type ContextReference } from './context';

export interface ConversationSummary {
id: string;
Expand All @@ -21,6 +21,7 @@ export interface ConversationMessageMetadata {

export type ConversationMessageData = {
attachments: ContextAttachment[];
recentFiles: ContextReference[];
};

type BaseConversationMessage = UIMessage<ConversationMessageMetadata, ConversationMessageData>;
Expand All @@ -37,22 +38,18 @@ export const createUserMessage = (
turnId: string,
text: string,
attachments: readonly ContextAttachment[],
): ConversationMessage => ({
id: turnId,
role: 'user',
metadata: { turnId, status: 'completed' },
parts: [
...(attachments.length > 0
? [
{
type: 'data-attachments' as const,
data: attachments.map((attachment) => ({ ...attachment })),
},
]
: []),
{ type: 'text', text },
],
});
recentFiles: readonly ContextReference[] = [],
): ConversationMessage => {
const parts: ConversationMessage['parts'] = [];
if (recentFiles.length > 0) {
parts.push({ type: 'data-recentFiles', data: recentFiles.map((reference) => ({ ...reference })) });
}
if (attachments.length > 0) {
parts.push({ type: 'data-attachments', data: attachments.map((attachment) => ({ ...attachment })) });
}
parts.push({ type: 'text', text });
return { id: turnId, role: 'user', metadata: { turnId, status: 'completed' }, parts };
};

export const createAssistantMessage = (id: string, turnId: string): ConversationMessage => ({
id,
Expand Down Expand Up @@ -110,10 +107,10 @@ const attachmentsSchema = jsonSchema<ContextAttachment[]>(
additionalProperties: false,
required: ['id', 'type', 'label', 'source', 'content'],
properties: {
id: { type: 'string' },
id: { type: 'string', minLength: 1 },
type: { enum: ['file', 'selection'] },
label: { type: 'string' },
source: { type: 'string' },
source: { type: 'string', minLength: 1 },
languageId: { type: 'string' },
content: { type: 'string' },
},
Expand All @@ -127,11 +124,29 @@ const attachmentsSchema = jsonSchema<ContextAttachment[]>(
},
);

const recentFilesSchema = jsonSchema<ContextReference[]>(
{
type: 'array',
items: {
type: 'object',
additionalProperties: false,
required: ['source'],
properties: { source: { type: 'string', minLength: 1 } },
},
},
{
validate: (value) =>
Array.isArray(value) && value.every(isContextReference)
? { success: true, value }
: { success: false, error: new Error('Invalid recent files') },
},
);

export const validateConversationMessages = (messages: unknown): Promise<ConversationMessage[]> =>
validateUIMessages<ConversationMessage>({
messages,
metadataSchema,
dataSchemas: { attachments: attachmentsSchema },
dataSchemas: { attachments: attachmentsSchema, recentFiles: recentFilesSchema },
});

const isConversationMessageMetadata = (value: unknown): value is ConversationMessageMetadata => {
Expand All @@ -149,26 +164,3 @@ const isConversationMessageMetadata = (value: unknown): value is ConversationMes
(metadata.error === undefined || typeof metadata.error === 'string')
);
};

const isContextAttachment = (value: unknown): value is ContextAttachment => {
if (!isPlainObject(value)) return false;
const attachment = value as Record<string, unknown>;
return (
Object.keys(attachment).every(
(key) =>
key === 'id' ||
key === 'type' ||
key === 'label' ||
key === 'source' ||
key === 'languageId' ||
key === 'content',
) &&
typeof attachment.id === 'string' &&
attachment.id.length > 0 &&
(attachment.type === 'file' || attachment.type === 'selection') &&
typeof attachment.label === 'string' &&
typeof attachment.source === 'string' &&
(attachment.languageId === undefined || typeof attachment.languageId === 'string') &&
typeof attachment.content === 'string'
);
};
Loading
Loading