Skip to content

Commit c460e86

Browse files
committed
Add undo/redo functionality for context selection and reorder prompt types
1 parent 1c50f29 commit c460e86

8 files changed

Lines changed: 168 additions & 13 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Code Web Chat is a free and open-source AI coding toolkit. You can use CWC in **
3232

3333
## Introduction
3434

35-
With its focus on context engineering, CWC proposes a non-agentic workflow, where all the necessary for the task files, includnig useful examples, are picked with engineer's intent. Such guided model is easy to prompt and as accurate as it can get. It's a perfect companion for all kinds of coding tasks; from mass renamings to features.
35+
With its focus on context engineering, CWC proposes a non-agentic workflow, where all the necessary for the task files, includnig useful examples, are picked with engineer's intent. Such guided model is easy to prompt and as accurate as it can get. The tool a perfect companion for all kinds of coding tasks; from mass renamings to complex features.
3636

3737
> [!TIP]
3838
> Feeling lost in the codebase? Pick context roughly, switch prompt type to _Prune context_, include your task and AI will help narrowing it down to reasonable size.
@@ -118,7 +118,8 @@ Meaningful summaries of changes adhering to your style.
118118
- `Code Web Chat: Remove File from Context` - Search and remove file (or parent folder via file action) from the context.
119119
- `Code Web Chat: Copy Context` - Copy XML-formatted checked files from the Workspace view to the clipboard.
120120
- `Code Web Chat: Copy Context of Open Editors` - Copy XML-formatted checked files from the Open Editors view to the clipboard.
121-
- `Code Web Chat: Find Paths in Clipboard` - Select files based on paths found in the clipboard text.
121+
- `Code Web Chat: Undo Context Selection` - Undo the last context change.
122+
- `Code Web Chat: Redo Context Selection` - Redo the last undone context change.
122123

123124
## Contributing
124125

‎packages/shared/src/types/prompt-types.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ export type WebPromptType =
55
| 'prune-context'
66
| 'no-context'
77

8-
export type ApiPromptType = 'edit-context' | 'code-at-cursor' | 'prune-context'
8+
export type ApiPromptType = 'edit-context' | 'prune-context' | 'code-at-cursor'

‎packages/vscode/package.json‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@
106106
"category": "Code Web Chat",
107107
"icon": "$(copy)"
108108
},
109+
{
110+
"command": "codeWebChat.undoContextSelection",
111+
"title": "Undo Context Selection",
112+
"category": "Code Web Chat"
113+
},
114+
{
115+
"command": "codeWebChat.redoContextSelection",
116+
"title": "Redo Context Selection",
117+
"category": "Code Web Chat"
118+
},
109119
{
110120
"command": "codeWebChat.duplicateWorkspace",
111121
"title": "Duplicate Workspace in New Window",
@@ -535,22 +545,22 @@
535545
{
536546
"command": "codeWebChat.checkParentFolder",
537547
"when": "view == codeWebChatViewOpenEditors && viewItem == openEditor",
538-
"group": "1_modification@3"
548+
"group": "2_modification@1"
539549
},
540550
{
541551
"command": "codeWebChat.referenceInPrompt",
542552
"when": "view == codeWebChatViewOpenEditors && viewItem == openEditor",
543-
"group": "1_modification@4"
553+
"group": "2_modification@2"
544554
},
545555
{
546556
"command": "codeWebChat.revealInExplorer",
547557
"when": "view == codeWebChatViewOpenEditors && viewItem == openEditor",
548-
"group": "1_modification@5"
558+
"group": "2_modification@3"
549559
},
550560
{
551561
"command": "codeWebChat.setRange",
552562
"when": "view == codeWebChatViewOpenEditors && viewItem == openEditor",
553-
"group": "1_modification@6"
563+
"group": "2_modification@4"
554564
},
555565
{
556566
"command": "codeWebChat.openInIntegratedTerminal",

‎packages/vscode/src/context/context-initialization.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,11 @@ export const context_initialization = async (
164164
}
165165

166166
const shared_state = SharedFileState.get_instance()
167-
shared_state.set_providers(workspace_provider, open_editors_provider)
167+
shared_state.set_providers(
168+
workspace_provider,
169+
open_editors_provider,
170+
websites_provider
171+
)
168172

169173
workspace_provider.load_checked_files_state()
170174
websites_provider.load_checked_websites_state()

‎packages/vscode/src/context/providers/websites/websites-provider.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,22 @@ export class WebsitesProvider
152152
}
153153
}
154154

155+
set_checked_websites(urls: string[]): void {
156+
this._checked_websites.clear()
157+
158+
// Only check websites that are currently in the list
159+
const valid_urls = new Set(this._websites.map((w) => w.url))
160+
161+
for (const url of urls) {
162+
if (valid_urls.has(url)) {
163+
this._checked_websites.set(url, vscode.TreeItemCheckboxState.Checked)
164+
}
165+
}
166+
167+
this._onDidChangeCheckedWebsites.fire()
168+
this._onDidChangeTreeData.fire()
169+
}
170+
155171
clear_checks(): void {
156172
if (this._checked_websites.size == 0) {
157173
return

‎packages/vscode/src/context/shared-file-state.ts‎

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,32 @@ import {
66
FileItem
77
} from './providers/workspace/workspace-provider'
88
import { OpenEditorsProvider } from './providers/open-editors/open-editors-provider'
9+
import { WebsitesProvider } from './providers/websites/websites-provider'
10+
11+
type ContextState = {
12+
files: string[]
13+
websites: string[]
14+
}
915

1016
export class SharedFileState {
1117
private static _instance: SharedFileState
1218
private _on_did_change_checked_files = new vscode.EventEmitter<void>()
1319
readonly onDidChangeCheckedFiles = this._on_did_change_checked_files.event
1420

1521
private _workspace_provider?: WorkspaceProvider
22+
private _websites_provider?: WebsitesProvider
1623
private _open_editors_provider?: OpenEditorsProvider
1724
private _checked_files: Set<string> = new Set()
25+
private _checked_websites: Set<string> = new Set()
1826
// Track which view last unchecked a file
1927
private _unchecked_in_open_editors: Set<string> = new Set()
2028
private _unchecked_in_workspace: Set<string> = new Set()
2129
private _synchronizing_provider: 'workspace' | 'openEditors' | null = null
2230
private _is_synchronizing: boolean = false
2331
private _is_initialized: boolean = false
32+
private _is_undoing_redoing: boolean = false
33+
private _undo_stack: ContextState[] = []
34+
private _redo_stack: ContextState[] = []
2435

2536
static get_instance(): SharedFileState {
2637
if (!SharedFileState._instance) {
@@ -31,40 +42,136 @@ export class SharedFileState {
3142

3243
set_providers(
3344
workspace_provider: WorkspaceProvider,
34-
open_editors_provider: OpenEditorsProvider
45+
open_editors_provider: OpenEditorsProvider,
46+
websites_provider?: WebsitesProvider
3547
) {
3648
this._workspace_provider = workspace_provider
3749
this._open_editors_provider = open_editors_provider
50+
this._websites_provider = websites_provider
3851

3952
workspace_provider.onDidChangeCheckedFiles(() => {
40-
if (!this._is_synchronizing) {
53+
if (!this._is_synchronizing && !this._is_undoing_redoing) {
4154
this._synchronizing_provider = 'workspace'
4255
this.synchronize_state()
4356
this._synchronizing_provider = null
4457
}
4558
})
4659

4760
open_editors_provider.onDidChangeCheckedFiles(() => {
48-
if (!this._is_synchronizing) {
61+
if (!this._is_synchronizing && !this._is_undoing_redoing) {
4962
this._synchronizing_provider = 'openEditors'
5063
this.synchronize_state()
5164
this._synchronizing_provider = null
5265
}
5366
})
5467

68+
if (websites_provider) {
69+
websites_provider.onDidChangeCheckedWebsites(() => {
70+
if (!this._is_undoing_redoing) {
71+
this.handle_websites_change()
72+
}
73+
})
74+
}
75+
5576
// Initialize with a synchronization after a small delay to ensure both providers are ready
5677
setTimeout(() => {
5778
if (!this._is_initialized) {
5879
this.synchronize_state()
80+
if (this._websites_provider) {
81+
const websites = this._websites_provider
82+
.get_checked_websites()
83+
.map((w) => w.url)
84+
this._checked_websites = new Set(websites)
85+
}
5986
this._is_initialized = true
6087
}
6188
}, 1000)
6289
}
6390

91+
private push_state(files: string[], websites: string[]) {
92+
if (!this._is_initialized) return
93+
this._undo_stack.push({ files, websites })
94+
if (this._undo_stack.length > 50) this._undo_stack.shift()
95+
this._redo_stack = []
96+
}
97+
98+
private handle_websites_change() {
99+
if (this._is_undoing_redoing) return
100+
101+
const current_websites =
102+
this._websites_provider
103+
?.get_checked_websites()
104+
.map((w) => w.url)
105+
.sort() || []
106+
const prev_websites = Array.from(this._checked_websites).sort()
107+
108+
if (JSON.stringify(prev_websites) !== JSON.stringify(current_websites)) {
109+
this.push_state(
110+
Array.from(this._checked_files).sort(),
111+
Array.from(this._checked_websites).sort()
112+
)
113+
this._checked_websites = new Set(current_websites)
114+
}
115+
}
116+
117+
async undo() {
118+
if (this._undo_stack.length === 0) return
119+
120+
const current_state = {
121+
files: Array.from(this._checked_files).sort(),
122+
websites: Array.from(this._checked_websites).sort()
123+
}
124+
this._redo_stack.push(current_state)
125+
126+
const previous_state = this._undo_stack.pop()
127+
if (previous_state) {
128+
this._is_undoing_redoing = true
129+
await this.apply_state(previous_state)
130+
this._is_undoing_redoing = false
131+
}
132+
}
133+
134+
async redo() {
135+
if (this._redo_stack.length === 0) return
136+
137+
const current_state = {
138+
files: Array.from(this._checked_files).sort(),
139+
websites: Array.from(this._checked_websites).sort()
140+
}
141+
this._undo_stack.push(current_state)
142+
143+
const next_state = this._redo_stack.pop()
144+
if (next_state) {
145+
this._is_undoing_redoing = true
146+
await this.apply_state(next_state)
147+
this._is_undoing_redoing = false
148+
}
149+
}
150+
151+
async apply_state(state: ContextState) {
152+
if (this._workspace_provider) {
153+
await this._workspace_provider.set_checked_files(state.files)
154+
}
155+
if (this._open_editors_provider) {
156+
await this._open_editors_provider.set_checked_files(state.files)
157+
}
158+
if (this._websites_provider) {
159+
this._websites_provider.set_checked_websites(state.websites)
160+
}
161+
162+
this.update_checked_files_set()
163+
this._checked_websites = new Set(state.websites)
164+
165+
this._on_did_change_checked_files.fire()
166+
}
167+
64168
async synchronize_state() {
65169
if (!this._workspace_provider || !this._open_editors_provider) return
66170
if (this._is_synchronizing) return
67171

172+
const prev_files = Array.from(this._checked_files).sort()
173+
const prev_websites = Array.from(this._checked_websites).sort()
174+
68175
this._is_synchronizing = true
69176

70177
try {
@@ -144,6 +251,14 @@ export class SharedFileState {
144251
this.update_checked_files_set()
145252

146253
this._on_did_change_checked_files.fire()
254+
255+
const curr_files = Array.from(this._checked_files).sort()
256+
257+
if (!this._is_undoing_redoing && this._is_initialized) {
258+
if (JSON.stringify(prev_files) !== JSON.stringify(curr_files)) {
259+
this.push_state(prev_files, prev_websites)
260+
}
261+
}
147262
} finally {
148263
this._is_synchronizing = false
149264
}

‎packages/vscode/src/extension.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
migrate_saved_contexts_to_global_storage,
1111
migrate_token_cache_cleanup
1212
} from './migrations'
13+
import { SharedFileState } from './context/shared-file-state'
1314
import {
1415
apply_chat_response_command,
1516
add_file_to_context_command,
@@ -170,6 +171,14 @@ export async function activate(context: vscode.ExtensionContext) {
170171
}
171172
}
172173
),
174+
vscode.commands.registerCommand(
175+
'codeWebChat.undoContextSelection',
176+
async () => SharedFileState.get_instance().undo()
177+
),
178+
vscode.commands.registerCommand(
179+
'codeWebChat.redoContextSelection',
180+
async () => SharedFileState.get_instance().redo()
181+
),
173182
vscode.commands.registerCommand(
174183
'codeWebChat.openInIntegratedTerminal',
175184
async (item: any) => {

‎packages/vscode/src/views/panel/frontend/Main/MainView/prompt-type-labels.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export const WEB_PROMPT_TYPES = Object.keys(
1313

1414
export const api_prompt_type_labels: Record<ApiPromptType, string> = {
1515
'edit-context': 'Edit context',
16-
'code-at-cursor': 'Code at cursor',
17-
'prune-context': 'Prune context'
16+
'prune-context': 'Prune context',
17+
'code-at-cursor': 'Code at cursor'
1818
}
1919
export const API_PROMPT_TYPES = Object.keys(
2020
api_prompt_type_labels

0 commit comments

Comments
 (0)