Skip to content
Open
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
37 changes: 37 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,48 @@ export function activate(context: vscode.ExtensionContext) {
// Set transport on Claude Agent Service
claudeAgentService.setTransport(transport);

// 监听选区变化并通知 WebView
const sendSelectionUpdate = (editor?: vscode.TextEditor) => {
const selection = editor?.selection;
if (!editor || !selection || selection.isEmpty || editor.document.uri.scheme !== 'file') {
transport.send({
type: 'request',
requestId: Math.random().toString(36).slice(2),
request: { type: 'selection_changed', selection: null }
});
return;
}
const document = editor.document;
transport.send({
type: 'request',
requestId: Math.random().toString(36).slice(2),
request: {
type: 'selection_changed',
selection: {
filePath: document.uri.fsPath,
startLine: selection.start.line + 1,
endLine: selection.end.line + 1,
startColumn: selection.start.character,
endColumn: selection.end.character
}
}
});
};

const selectionDisposable = vscode.window.onDidChangeTextEditorSelection((event) => {
sendSelectionUpdate(event.textEditor);
});
const activeEditorDisposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
sendSelectionUpdate(editor);
});

// Start message loop
claudeAgentService.start();

// Register disposables
context.subscriptions.push(webviewProvider);
context.subscriptions.push(selectionDisposable);
context.subscriptions.push(activeEditorDisposable);
context.subscriptions.push(
vscode.commands.registerCommand('claudix.openSettings', async () => {
await instantiationService.invokeFunction(accessorInner => {
Expand Down
20 changes: 8 additions & 12 deletions src/webview/src/composables/useRuntime.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { onMounted, onUnmounted, watch } from 'vue';
import { onMounted, onUnmounted } from 'vue';
import { signal, effect } from 'alien-signals';
import { EventEmitter } from '../utils/events';
import { ConnectionManager } from '../core/ConnectionManager';
Expand All @@ -23,18 +23,13 @@ export function useRuntime(): RuntimeInstance {
const appContext = new AppContext(connectionManager);

// 创建 alien-signal 用于 SessionContext
// AppContext.currentSelection 是 Vue Ref,但 SessionContext 需要 alien-signal
// AppContext.currentSelection 是 alien-signal,SessionContext 也需要 alien-signal
const currentSelectionSignal = signal<SelectionRange | undefined>(undefined);

// 双向同步 Vue Ref ↔ Alien Signal
// Vue Ref → Alien Signal
watch(
() => appContext.currentSelection(),
(newValue) => {
currentSelectionSignal(newValue);
},
{ immediate: true }
);
// 使用 alien-signals 的 effect 同步信号(Vue watch 无法追踪 alien-signal)
const cleanupSelectionSync = effect(() => {
currentSelectionSignal(appContext.currentSelection());
});

const sessionStore = new SessionStore(connectionManager, {
commandRegistry: appContext.commandRegistry,
Expand All @@ -47,7 +42,7 @@ export function useRuntime(): RuntimeInstance {
});

selectionEvents.add((selection) => {
appContext.currentSelection(selection);
appContext.currentSelection(selection ?? undefined);
});

// SessionStore 内部的 effect 会自动监听 connection 建立并拉取会话列表
Expand Down Expand Up @@ -122,6 +117,7 @@ export function useRuntime(): RuntimeInstance {
// 清理命令注册
slashCommandDisposers.forEach(dispose => dispose());
cleanupSlashCommands();
cleanupSelectionSync();

connectionManager.close();
});
Expand Down
11 changes: 10 additions & 1 deletion src/webview/src/core/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export class Session {
async send(
input: string,
attachments: AttachmentPayload[] = [],
includeSelection = false
includeSelection = true
): Promise<void> {
const connection = await this.getConnection();

Expand All @@ -207,6 +207,15 @@ export class Session {
// 启动 channel(确保已带上当前 thinkingLevel)
await this.launchClaude();

if (includeSelection && !isSlash) {
try {
const selection = await connection.getCurrentSelection();
this.selection(selection?.selection ?? undefined);
} catch (error) {
console.warn('[Session] Failed to fetch current selection', error);
}
}

const shouldIncludeSelection = includeSelection && !isSlash;
let selectionPayload: SelectionRange | undefined;

Expand Down
54 changes: 54 additions & 0 deletions src/webview/src/pages/ChatPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
:on-resolve="handleResolvePermission"
data-permission-panel="1"
/>
<div v-if="selectionInfo" class="selection-indicator">
<span class="codicon codicon-note selection-indicator-icon"></span>
<span class="selection-indicator-text">{{ selectionInfo }}</span>
</div>
<ChatInputBox
:show-progress="true"
:progress-percentage="progressPercentage"
Expand Down Expand Up @@ -142,6 +146,18 @@
const permissionRequestsLen = computed(() => permissionRequests.value.length);
const pendingPermission = computed(() => permissionRequests.value[0] as any);
const platform = computed(() => runtime.appContext.platform);
const selectionInfo = computed(() => {
const sel = session.value?.selection.value;
if (!sel?.filePath) return '';
const fileName = sel.filePath.split(/[\\/]/).pop() || sel.filePath;
const start = sel.startLine;
const end = sel.endLine ?? sel.startLine;
if (start && end) {
const lineCount = Math.max(1, end - start + 1);
return `${fileName} #${start}-${end} (${lineCount} 行)`;
}
return fileName;
});

// 注册命令:permissionMode.toggle(在下方定义函数后再注册)

Expand Down Expand Up @@ -515,9 +531,47 @@

/* 输入区域容器 */
.inputContainer {
position: relative;
padding: 8px 12px 12px;
}

.selection-indicator {
position: absolute;
bottom: 100%;
left: 12px;
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
padding: 4px 8px;
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
background: var(--vscode-editor-background);
color: var(--vscode-textLink-foreground);
font-size: 11px;
line-height: 1.4;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
z-index: 10;
}

.selection-indicator-icon {
font-size: 12px;
}

.selection-indicator-text {
font-family: var(
--app-monospace-font-family,
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
'Liberation Mono',
'Courier New',
monospace
);
}

/* 底部对话框区域钉在底部 */
.main > :last-child {
flex-shrink: 0;
Expand Down