Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use rules in auto-edit #6930

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions agent/src/AgentFixupControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export class AgentFixupControls extends FixupCodeLenses {
instruction: task.instruction?.toString().trim(),
model: task.model.toString().trim(),
originalText: task.original,
rules: task.rules,
}
}
}
4 changes: 3 additions & 1 deletion agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,8 @@ export class Agent extends MessageHandler implements ExtensionClient {
}

this.workspace.workspaceRootUri = clientInfo.workspaceRootUri
? vscode.Uri.parse(clientInfo.workspaceRootUri).with({ scheme: 'file' })
? // TODO!(sqs): why was this previously set to always be file scheme?
vscode.Uri.parse(clientInfo.workspaceRootUri)
: vscode.Uri.from({
scheme: 'file',
path: clientInfo.workspaceRootPath ?? undefined,
Expand Down Expand Up @@ -1148,6 +1149,7 @@ export class Agent extends MessageHandler implements ExtensionClient {
range: vscodeRange(params.range),
intent: 'edit',
mode: params.mode,
rules: params.rules,
}

if (!this.fixups) return Promise.reject()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
REMOTE_DIRECTORY_PROVIDER_URI,
REMOTE_FILE_PROVIDER_URI,
REMOTE_REPOSITORY_PROVIDER_URI,
RULES_PROVIDER_URI,
SYMBOL_CONTEXT_MENTION_PROVIDER,
WEB_PROVIDER_URI,
displayLineRange,
Expand All @@ -18,6 +19,7 @@ import {
import { clsx } from 'clsx'
import {
ArrowRightIcon,
BookCheckIcon,
BoxIcon,
DatabaseIcon,
ExternalLinkIcon,
Expand Down Expand Up @@ -178,6 +180,7 @@ export const iconForProvider: Record<
[REMOTE_FILE_PROVIDER_URI]: FileIcon,
[REMOTE_DIRECTORY_PROVIDER_URI]: FolderGitIcon,
[WEB_PROVIDER_URI]: LinkIcon,
[RULES_PROVIDER_URI]: BookCheckIcon,
}

export const iconForItem: Record<
Expand Down
2 changes: 1 addition & 1 deletion lib/prompt-editor/src/nodes/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function tooltipForContextItem(item: SerializedContextItem): string | und
return item.mention.data.tooltip
}
if (item.type === 'openctx') {
return item.uri
return item.description ?? item.uri
}
return undefined
}
3 changes: 2 additions & 1 deletion lib/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"ollama": "^0.5.1",
"re2js": "^0.4.1",
"semver": "^7.5.4",
"vscode-uri": "^3.0.8"
"vscode-uri": "^3.0.8",
"yaml": "^2.3.4"
},
"devDependencies": {
"@sourcegraph/cody-context-filters-test-dataset": "^1.0.0",
Expand Down
2 changes: 1 addition & 1 deletion lib/shared/src/common/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { URI } from 'vscode-uri'

import { isWindows as _isWindows, isMacOS } from './platform'

interface PathFunctions {
export interface PathFunctions {
/**
* All but the last element of path, or "." if that would be the empty path.
*/
Expand Down
42 changes: 28 additions & 14 deletions lib/shared/src/context/openctx/api.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
import type { Client } from '@openctx/client'
import type { Observable } from 'observable-fns'
import type * as vscode from 'vscode'
import { fromLateSetSource, shareReplay, storeLastValue } from '../../misc/observable'

type OpenCtxController = Pick<
export type OpenCtxController = Pick<
Client<vscode.Range>,
'meta' | 'metaChanges' | 'mentions' | 'mentionsChanges' | 'items'
> & {}
>

interface OpenCtx {
controller?: OpenCtxController
disposable?: vscode.Disposable
}
const _openctxController = fromLateSetSource<OpenCtxController>()

export const openCtx: OpenCtx = {}
export const openctxController: Observable<OpenCtxController> = _openctxController.observable.pipe(
shareReplay({ shouldCountRefs: false })
)

/**
* Set the handle to the OpenCtx. If there is an existing handle it will be
* disposed and replaced.
* Set the observable that will be used to provide the global {@link openctxController}.
*/
export function setOpenCtx({ controller, disposable }: OpenCtx): void {
const { disposable: oldDisposable } = openCtx
export function setOpenCtxControllerObservable(input: Observable<OpenCtxController>): void {
_openctxController.setSource(input)
}

openCtx.controller = controller
openCtx.disposable = disposable
const { value: syncValue } = storeLastValue(openctxController)

oldDisposable?.dispose()
/**
* The current OpenCtx controller. Callers should use {@link openctxController} instead so that
* they react to changes. This function is provided for old call sites that haven't been updated
* to use an Observable.
*
* Callers should take care to avoid race conditions and prefer observing {@link openctxController}.
*
* Throws if the OpenCtx controller is not yet set.
*/
export function currentOpenCtxController(): OpenCtxController {
if (!syncValue.isSet) {
throw new Error('OpenCtx controller is not initialized')
}
return syncValue.last
}

export const REMOTE_REPOSITORY_PROVIDER_URI = 'internal-remote-repository-search'
Expand All @@ -32,3 +45,4 @@ export const REMOTE_DIRECTORY_PROVIDER_URI = 'internal-remote-directory-search'
export const WEB_PROVIDER_URI = 'internal-web-provider'
export const GIT_OPENCTX_PROVIDER_URI = 'internal-git-openctx-provider'
export const CODE_SEARCH_PROVIDER_URI = 'internal-code-search-provider'
export const RULES_PROVIDER_URI = 'internal-rules-provider'
6 changes: 3 additions & 3 deletions lib/shared/src/context/openctx/context.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { URI } from 'vscode-uri'
import { type ContextItemOpenCtx, ContextItemSource } from '../../codebase-context/messages'
import { openCtx } from './api'
import { currentOpenCtxController } from './api'

// getContextForChatMessage returns context items for a given chat message from the OpenCtx providers.
export const getContextForChatMessage = async (
message: string,
signal?: AbortSignal
): Promise<ContextItemOpenCtx[]> => {
try {
const openCtxClient = openCtx.controller
const openCtxClient = currentOpenCtxController()
if (!openCtxClient) {
return []
}
Expand Down Expand Up @@ -52,7 +52,7 @@ export const getContextForChatMessage = async (
content: item.ai?.content || '',
provider: 'openctx',
source: ContextItemSource.User, // To indicate that this is a user-added item.
}) as ContextItemOpenCtx
}) satisfies ContextItemOpenCtx
)
} catch {
return []
Expand Down
2 changes: 2 additions & 0 deletions lib/shared/src/experimentation/FeatureFlagProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export enum FeatureFlag {
* Whether the user will see the CTA about upgrading to Sourcegraph Teams
*/
SourcegraphTeamsUpgradeCTA = 'teams-upgrade-available-cta',

Rules = 'rules',
}

const ONE_HOUR = 60 * 60 * 1000
Expand Down
15 changes: 13 additions & 2 deletions lib/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,14 +352,17 @@ export * from './token'
export * from './token/constants'
export * from './configuration'
export {
setOpenCtx,
openCtx,
setOpenCtxControllerObservable,
openctxController,
type OpenCtxController,
REMOTE_REPOSITORY_PROVIDER_URI,
REMOTE_FILE_PROVIDER_URI,
REMOTE_DIRECTORY_PROVIDER_URI,
WEB_PROVIDER_URI,
GIT_OPENCTX_PROVIDER_URI,
CODE_SEARCH_PROVIDER_URI,
currentOpenCtxController,
RULES_PROVIDER_URI,
} from './context/openctx/api'
export * from './context/openctx/context'
export * from './lexicalEditor/editorState'
Expand Down Expand Up @@ -409,3 +412,11 @@ export { isS2, isWorkspaceInstance } from './sourcegraph-api/environments'
export { createGitDiff } from './editor/create-git-diff'

export { serialize, deserialize } from './lexicalEditor/atMentionsSerializer'

export { type Rule, isRuleFilename, ruleTitle, parseRuleFile, ruleSearchPaths } from './rules/rules'
export {
type CandidateRule,
type RuleProvider,
createRuleService,
type RuleService,
} from './rules/service'
31 changes: 15 additions & 16 deletions lib/shared/src/mentions/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { MetaResult } from '@openctx/client'
import { Observable, map } from 'observable-fns'
import { openCtx } from '../context/openctx/api'
import { distinctUntilChanged } from '../misc/observable'
import { type Observable, map } from 'observable-fns'
import { openctxController } from '../context/openctx/api'
import { distinctUntilChanged, switchMap } from '../misc/observable'

/**
* Props required by context item providers to return possible context items.
Expand Down Expand Up @@ -73,18 +73,17 @@ export function openCtxProviderMetadata(
}

function openCtxMentionProviders(): Observable<ContextMentionProviderMetadata[]> {
const controller = openCtx.controller
if (!controller) {
return Observable.of([])
}

return controller.metaChanges({}, {}).pipe(
map(providers =>
providers
.filter(provider => !!provider.mentions)
.map(openCtxProviderMetadata)
.sort((a, b) => (a.title > b.title ? 1 : -1))
),
distinctUntilChanged()
return openctxController.pipe(
switchMap(c =>
c.metaChanges({}, {}).pipe(
map(providers =>
providers
.filter(provider => !!provider.mentions)
.map(openCtxProviderMetadata)
.sort((a, b) => (a.title > b.title ? 1 : -1))
),
distinctUntilChanged()
)
)
)
}
11 changes: 11 additions & 0 deletions lib/shared/src/misc/observable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
fromVSCodeEvent,
lifecycle,
memoizeLastValue,
merge,
observableOfSequence,
observableOfTimedSequence,
promiseFactoryToObservable,
Expand Down Expand Up @@ -321,6 +322,16 @@ describe('fromLateSetSource', () => {
})
})

describe('merge', { timeout: 500 }, () => {
test('emits values from all inputs', async () => {
vi.useRealTimers()
const input1 = observableOfTimedSequence(0, 'A', 10, 'B', 10, 'C')
const input2 = observableOfTimedSequence(5, 'x', 10, 'y', 15, 'z')
const observable = merge(input1, input2)
expect(await allValuesFrom(observable)).toEqual(['A', 'x', 'B', 'y', 'C', 'z'])
})
})

describe('combineLatest', { timeout: 500 }, () => {
afterEach(() => {
vi.useRealTimers()
Expand Down
27 changes: 27 additions & 0 deletions lib/shared/src/misc/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,33 @@ export const EMPTY = new Observable<never>(observer => {
*/
export const NEVER: Observable<never> = new Observable<never>(() => {})

/**
* Merge all {@link Observable}s into a single {@link Observable} that emits each value emitted by
* any of the input observables.
*/
export function merge<T extends unknown[]>(
...observables: { [K in keyof T]: Observable<T[K]> }
): Observable<T[number]> {
return new Observable<T[number]>(observer => {
let completed = 0
const subscriptions = observables.map(observable =>
observable.subscribe({
next: value => observer.next(value),
error: err => observer.error(err),
complete: () => {
completed++
if (completed === observables.length) {
observer.complete()
}
},
})
)
return () => {
unsubscribeAll(subscriptions)
}
})
}

/**
* Combine the latest values from multiple {@link Observable}s into a single {@link Observable} that
* emits only after all input observables have emitted once.
Expand Down
23 changes: 19 additions & 4 deletions lib/shared/src/misc/rpc/webviewAPI.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Observable } from 'observable-fns'
import { type Observable, map } from 'observable-fns'
import type { AuthStatus, ModelsData, ResolvedConfiguration, UserProductSubscription } from '../..'
import type { SerializedPromptEditorState } from '../..'
import type { ChatMessage, UserLocalHistory } from '../../chat/transcript/messages'
Expand Down Expand Up @@ -132,9 +132,24 @@ export function createExtensionAPI(
hydratePromptMessage: promptText =>
hydratePromptMessage(promptText, staticDefaultContext?.initialContext),
setChatModel: proxyExtensionAPI(messageAPI, 'setChatModel'),
defaultContext: staticDefaultContext
? () => Observable.of(staticDefaultContext)
: proxyExtensionAPI(messageAPI, 'defaultContext'),
defaultContext: () =>
proxyExtensionAPI(messageAPI, 'defaultContext')().pipe(
map(result =>
staticDefaultContext
? ({
...result,
corpusContext: [
...result.corpusContext,
...staticDefaultContext.corpusContext,
],
initialContext: [
...result.initialContext,
...staticDefaultContext.initialContext,
],
} satisfies DefaultContext)
: result
)
),
detectIntent: proxyExtensionAPI(messageAPI, 'detectIntent'),
promptsMigrationStatus: proxyExtensionAPI(messageAPI, 'promptsMigrationStatus'),
startPromptsMigration: proxyExtensionAPI(messageAPI, 'startPromptsMigration'),
Expand Down
15 changes: 15 additions & 0 deletions lib/shared/src/prompt/prompt-string.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dedent from 'dedent'
import type * as vscode from 'vscode'
import { URI } from 'vscode-uri'
import type { ChatMessage, SerializedChatMessage } from '../chat/transcript/messages'
import type { ContextItem } from '../codebase-context/messages'
import type { ContextFiltersProvider } from '../cody-ignore/context-filters-provider'
Expand All @@ -17,6 +18,7 @@ import { createGitDiff } from '../editor/create-git-diff'
import { displayPath, displayPathWithLines } from '../editor/displayPath'
import { getEditorInsertSpaces, getEditorTabSize } from '../editor/utils'
import { logDebug } from '../logger'
import { type Rule, ruleTitle } from '../rules/rules'
import { telemetryRecorder } from '../telemetry-v2/singleton'

// This module is designed to encourage, and to some degree enforce, safe
Expand Down Expand Up @@ -397,6 +399,19 @@ export class PromptString {
}
}

public static fromRule(rule: Rule): {
title: PromptString
description: PromptString | null
instruction: PromptString
} {
const ref = [URI.parse(rule.uri)]
return {
title: internal_createPromptString(ruleTitle(rule), ref),
description: rule.description ? internal_createPromptString(rule.description, ref) : null,
instruction: internal_createPromptString(rule.instruction ?? '', ref),
}
}

// 🚨 Use this function only for user-generated queries.
// TODO: Can we detect if the user is pasting in content from a document?
public static unsafe_fromUserQuery(string: string): PromptString {
Expand Down
5 changes: 5 additions & 0 deletions lib/shared/src/rules/__testdata__/my.rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: My rule
---

My instruction
Loading
Loading