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
39 changes: 29 additions & 10 deletions extensions/github1s-ai/assets/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ summary:focus-visible,
.settings-card-actions,
.settings-meta,
.history-row,
.history-actions,
.history-title-editor,
.history-confirm {
display: flex;
align-items: center;
Expand Down Expand Up @@ -235,7 +237,7 @@ summary:focus-visible,
box-sizing: border-box;
display: flex;
min-width: 0;
align-items: flex-start;
align-items: center;
gap: 6px;
padding: 8px;
border: 1px solid var(--vscode-widget-border, transparent);
Expand Down Expand Up @@ -475,7 +477,8 @@ summary:focus-visible,
margin: 0;
}

.settings-control {
.settings-control,
.history-title-input {
width: 100%;
min-width: 0;
padding: 6px 7px;
Expand All @@ -485,7 +488,8 @@ summary:focus-visible,
border-radius: 3px;
}

.settings-control:focus-visible {
.settings-control:focus-visible,
.history-title-input:focus-visible {
border-color: var(--vscode-focusBorder);
outline: none;
}
Expand Down Expand Up @@ -554,9 +558,11 @@ summary:focus-visible,
}

.history-row {
box-sizing: border-box;
min-width: 0;
height: 46px;
gap: 4px;
padding: 6px 8px;
padding: 5px 8px;
border: 1px solid transparent;
border-radius: 4px;
}
Expand Down Expand Up @@ -591,6 +597,17 @@ summary:focus-visible,
white-space: nowrap;
}

.history-title-editor {
min-width: 0;
flex: 1;
gap: 4px;
}

.history-title-input {
height: 24px;
padding: 2px 6px;
}

.history-empty {
margin: 12px 0;
color: var(--vscode-descriptionForeground);
Expand All @@ -617,14 +634,16 @@ summary:focus-visible,
text-overflow: ellipsis;
}

.history-delete {
opacity: 0;
.history-actions {
display: none;
flex: none;
gap: 2px;
}

.history-row:hover .history-delete,
.history-row:focus-within .history-delete,
.history-row-active .history-delete {
opacity: 1;
.history-row:hover .history-actions,
.history-row:focus-within .history-actions,
.history-row-active .history-actions {
display: flex;
}

.ghost-button.history-confirm-delete {
Expand Down
26 changes: 14 additions & 12 deletions extensions/github1s-ai/src/common/protocol.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isPlainObject } from 'lodash-es';

import { exactString, hasExactKeys } from '@/helpers/validate';
import { exactString, hasExactKeys, isNonBlankString, isNonEmptyString } from '@/helpers/validate';

import {
isContextAttachmentAction,
Expand Down Expand Up @@ -54,6 +54,7 @@ type AllViewEvents =
| { type: 'chat.setIncludeRecentFiles'; enabled: boolean }
| { type: 'chat.cancel' }
| { type: 'history.selectConversation'; id: string }
| { type: 'history.renameConversation'; id: string; title: string }
| { type: 'history.deleteConversation'; id: string }
| { type: 'markdown.highlight'; requestId: string; languageId: string; source: string }
| { type: 'settings.selectModelConfig'; id: string }
Expand Down Expand Up @@ -90,30 +91,33 @@ export const parseViewEvent = (value: unknown): ViewEvent | undefined => {
return hasExactKeys(event, ['type']) ? { type: event.type } : undefined;

case 'app.openFile':
return exactString(event, 'source') ? { type: event.type, source: event.source as string } : undefined;
return exactString(event, 'source') ? { type: event.type, source: event.source } : undefined;

case 'chat.send':
return hasExactKeys(event, ['type', 'text']) && typeof event.text === 'string'
? { type: event.type, text: event.text }
: undefined;
return exactString(event, 'text') ? { type: event.type, text: event.text } : undefined;

case 'chat.setEditingMessageId':
if (hasExactKeys(event, ['type'])) return { type: event.type };
return exactString(event, 'id') ? { type: event.type, id: event.id as string } : undefined;
return exactString(event, 'id') ? { type: event.type, id: event.id } : undefined;

case 'chat.setIncludeRecentFiles':
return hasExactKeys(event, ['type', 'enabled']) && typeof event.enabled === 'boolean'
? { type: event.type, enabled: event.enabled }
: undefined;

case 'history.renameConversation':
return hasExactKeys(event, ['type', 'id', 'title']) && isNonEmptyString(event.id) && isNonBlankString(event.title)
? { type: event.type, id: event.id, title: event.title }
: undefined;

case 'chat.retry':
case 'chat.fork':
case 'chat.removeContextAttachment':
case 'history.selectConversation':
case 'history.deleteConversation':
case 'settings.selectModelConfig':
case 'settings.deleteModelConfig':
return exactString(event, 'id') ? { type: event.type, id: event.id as string } : undefined;
return exactString(event, 'id') ? { type: event.type, id: event.id } : undefined;

case 'chat.runQuickAction':
return exactString(event, 'action') && isQuickAction(event.action)
Expand All @@ -133,10 +137,8 @@ export const parseViewEvent = (value: unknown): ViewEvent | 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 ||
!isNonEmptyString(event.requestId) ||
!isNonEmptyString(event.languageId) ||
typeof event.source !== 'string' ||
event.source.length > MAX_SYNTAX_HIGHLIGHT_SOURCE_LENGTH
) {
Expand All @@ -158,7 +160,7 @@ export const parseViewEvent = (value: unknown): ViewEvent | undefined => {
? { type: event.type, config: event.config }
: undefined;
case 'settings.saveMcpConfig':
return exactString(event, 'json') ? { type: event.type, json: event.json as string } : undefined;
return exactString(event, 'json') ? { type: event.type, json: event.json } : undefined;
default:
return undefined;
}
Expand Down
21 changes: 21 additions & 0 deletions extensions/github1s-ai/src/controllers/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ export class HistoryController extends Controller {
});
}

@Controller.handler('history.renameConversation')
async handleRenameConversation(event: ViewEvent<'history.renameConversation'>): Promise<void> {
const title = event.title.trim();
if (!title) return;
try {
await this.stores.conversations.update(event.id, { title });
} catch (error) {
const detail = error instanceof Error ? error.message : 'Unknown error.';
await this.stores.runtime.setIn('history.notice', {
severity: 'error',
message: `Unable to rename the conversation. ${detail}`,
});
return;
}
const runtime = await this.stores.runtime.get();
if (runtime.chat.conversation?.id === event.id) {
await this.stores.runtime.setIn('chat.conversation.title', title);
}
await this.stores.runtime.setIn('history.notice', undefined);
}

@Controller.handler('history.deleteConversation')
async handleDeleteConversation(event: ViewEvent<'history.deleteConversation'>): Promise<void> {
const selected = (await this.stores.runtime.get()).chat.conversation?.id === event.id;
Expand Down
2 changes: 1 addition & 1 deletion extensions/github1s-ai/src/controllers/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export class ConversationRunner {
private async setCurrentConversation(conversation: Conversation, signal?: AbortSignal): Promise<void> {
const runtime = await this.stores.runtime.get();
if (runtime.chat.conversation?.id !== conversation.id || signal?.aborted) return;
await this.stores.runtime.setIn('chat.conversation', conversation);
await this.stores.runtime.setIn('chat.conversation', { ...conversation, title: runtime.chat.conversation.title });
}

private async withPreparation(
Expand Down
9 changes: 8 additions & 1 deletion extensions/github1s-ai/src/helpers/validate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
export const exactString = (value: Record<string, unknown>, key: string): boolean =>
export const exactString = <Key extends string>(
value: Record<string, unknown>,
key: Key,
): value is Record<string, unknown> & Record<Key, string> =>
hasExactKeys(value, ['type', key]) && typeof value[key] === 'string';

export const hasExactKeys = (value: Record<string, unknown>, keys: readonly string[]): boolean =>
Object.keys(value).length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));

export const isNonEmptyString = (value: unknown): value is string => typeof value === 'string' && value.length > 0;

export const isNonBlankString = (value: unknown): value is string => isNonEmptyString(value) && value.trim().length > 0;
8 changes: 8 additions & 0 deletions extensions/github1s-ai/src/webview/components/Composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ export const Composer = ({ state, active, post, message: editedMessage }: Compos
};

const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && event.altKey && !event.isComposing) {
event.preventDefault();
const input = event.currentTarget as HTMLTextAreaElement;
// Use a native editing command to preserve undo history.
document.execCommand('insertText', false, '\n');
setDraft(input.value);
return;
}
if (!shouldSubmitComposer(event)) return;
event.preventDefault();
submit();
Expand Down
Loading
Loading