Skip to content

Commit cecdaf0

Browse files
committed
Implement persistent text storage for pasted text and website contents
- Refactor pasted text handling in the save/open handlers and symbol replacers to use `extension_context.globalStorageUri.fsPath + '/pasted-text'` instead of `os.tmpdir()`, replace the MD5 hash with a `Date.now()` timestamp for the file ID and `#PastedText` symbol, and add garbage collection to the save handler that deletes files in that directory with a timestamp older than 1 year (31536000000 ms). - treat parsed websites similarly - the one year garbage collection threshold should be kept in `apps/editor/src/constants/values.ts` - pasted text/websites storage should be abstracted into a shared interace/util - [Pasted text] should return id (so accepting in param is not necessary) - the 'website' symbol should have website's title in it, not the generic 'Website' - rename 'persistent storage' to 'persistent text storage' - in parsed website, don't place URL. When displaying such website, in the titlebar, use it's title from the symbol. - in parsed website preview: don't print page title in heading. in constructed prompt: place the website text in a markdown code block (```markdown...) prefixed by the title in **bold text** (taken from the symbol) - analyze latest changes: [Changes] - keep storing url (before ) in the symbol and restore ability to open the website from preview action button. - simplify `apps/editor/src/views/prompt/backend/utils/persistent-text-storage.ts` by storing pasted text and websites to unified folder in the storage (no need to store them in separate `pasted-text` and `websites` folders). Use folder name `prompt-text`. - simplify file names, e.g. [Pasted text] to 1792446329395.txt - instead of timestamps, use sha256 hashes for filenames. Garbage collect old files based on file metadata. - shorten these hashes to reasonable lengths [Pasted text] ├── apps │ └── editor │ ├── package.json │ └── src │ ├── commands │ │ └── save-context-command │ │ ├── actions │ │ │ └── save-to-workspace-state.ts │ │ └── save-context-command.ts │ ├── constants │ │ └── values.ts │ ├── features │ │ └── context-restoration │ │ └── utils │ │ └── global-storage-utils.ts │ ├── utils │ │ ├── files-collector.ts │ │ ├── normalize-path.ts │ │ └── prompt-builder.ts │ └── views │ └── prompt │ ├── backend │ │ ├── message-handlers │ │ │ ├── handle-open-prompt-pasted-text.ts │ │ │ ├── handle-open-website.ts │ │ │ ├── handle-paste-url.ts │ │ │ ├── handle-save-prompt-pasted-text.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ └── build-prompt-payload.ts │ │ ├── prompt-view-provider.ts │ │ └── utils │ │ ├── persistent-text-storage.ts │ │ ├── symbols │ │ │ ├── pasted-text │ │ │ │ └── replace-pasted-text-symbol.ts │ │ │ ├── replace-symbols.ts │ │ │ └── website │ │ │ └── replace-website-symbol.ts │ │ └── website-fetcher.ts │ ├── frontend │ │ ├── Main │ │ │ ├── Main.tsx │ │ │ └── MainView │ │ │ └── MainView.tsx │ │ ├── Prompt.tsx │ │ └── hooks │ │ └── use-prompt │ │ └── use-prompt.ts │ └── types │ └── messages.ts └── packages ├── shared │ └── src │ └── utils │ └── simplify-prompt-symbols │ └── simplify-prompt-symbols.ts └── ui └── src └── components └── editor └── common └── prompts ├── PromptField │ ├── PromptField.tsx │ └── hooks │ └── use-handlers │ ├── handle-paste.ts │ └── utils.ts └── shared └── symbols ├── get-highlighted-text.tsx ├── position-mapping.ts ├── reconstruct-raw-value.ts └── use-symbol-deletion.ts
1 parent 3d2a608 commit cecdaf0

21 files changed

Lines changed: 260 additions & 147 deletions

File tree

‎apps/editor/src/views/prompt/backend/message-handlers/handle-open-prompt-pasted-text.ts‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import * as os from 'os'
2-
import * as path from 'path'
31
import * as vscode from 'vscode'
2+
import { OpenPromptPastedTextMessage } from '../../types/messages'
3+
import { PromptViewProvider } from '../prompt-view-provider'
4+
import { PersistentTextStorage } from '../utils/persistent-text-storage'
45

5-
export const handle_open_prompt_pasted_text = async (message: {
6-
hash: string
7-
}) => {
6+
export const handle_open_prompt_pasted_text = async (
7+
prompt_view_provider: PromptViewProvider,
8+
message: OpenPromptPastedTextMessage
9+
) => {
810
try {
9-
const hash = message.hash
10-
const filename = `cwc-paste-${hash}.txt`
11-
const file_path = path.join(os.tmpdir(), filename)
11+
const storage = new PersistentTextStorage(
12+
prompt_view_provider.extension_context
13+
)
14+
const file_path = storage.get_file_path(message.id)
1215

1316
const doc = await vscode.workspace.openTextDocument(file_path)
1417
await vscode.window.showTextDocument(doc)

‎apps/editor/src/views/prompt/backend/message-handlers/handle-open-website.ts‎

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,50 @@
11
import * as vscode from 'vscode'
22
import { OpenWebsiteMessage } from '../../types/messages'
33
import * as fs from 'fs'
4-
import { get_website_file_path } from '../utils/website-fetcher'
54
import { marked } from 'marked'
5+
import { PromptViewProvider } from '../prompt-view-provider'
6+
import { PersistentTextStorage } from '../utils/persistent-text-storage'
67

78
let current_preview_url: string | undefined
89

910
export const get_current_preview_url = () => current_preview_url
1011

11-
export const handle_open_website = async (message: OpenWebsiteMessage) => {
12-
const file_path = get_website_file_path(message.url)
12+
export const handle_open_website = async (
13+
prompt_view_provider: PromptViewProvider,
14+
message: OpenWebsiteMessage
15+
) => {
16+
let id: string | undefined
17+
let title = 'Website'
1318

14-
if (fs.existsSync(file_path)) {
15-
const markdown = fs.readFileSync(file_path, 'utf8')
16-
const html = await marked.parse(markdown)
19+
const escaped_url = message.url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
20+
const instructions = prompt_view_provider.current_instructions
21+
const symbol_regex = new RegExp(
22+
`#Website\\(${escaped_url} ([a-fA-F0-9]+):\\d+(?: "((?:\\\\.|[^"\\\\])*)")?\\)`
23+
)
24+
25+
let match = instructions.match(symbol_regex)
26+
if (!match) {
27+
for (const task of prompt_view_provider.completed_tasks) {
28+
match = task.match(symbol_regex)
29+
if (match) break
30+
}
31+
}
1732

18-
const url_obj = new URL(message.url)
19-
let hostname = url_obj.hostname
20-
if (hostname.startsWith('www.')) {
21-
hostname = hostname.substring(4)
33+
if (match) {
34+
id = match[1]
35+
if (match[2]) {
36+
title = match[2].replace(/\\"/g, '"').replace(/\\\\/g, '\\')
2237
}
23-
const title = `${hostname}${url_obj.pathname == '/' ? '' : url_obj.pathname}`
38+
}
39+
40+
const storage = new PersistentTextStorage(
41+
prompt_view_provider.extension_context
42+
)
43+
const file_path = id ? storage.get_file_path(id) : ''
44+
45+
if (id && fs.existsSync(file_path)) {
46+
const markdown = fs.readFileSync(file_path, 'utf8')
47+
const html = await marked.parse(markdown)
2448

2549
const webview_panel = vscode.window.createWebviewPanel(
2650
'websitePreview',
@@ -78,7 +102,7 @@ export const handle_open_website = async (message: OpenWebsiteMessage) => {
78102
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/vs2015.min.css">
79103
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
80104
<script>document.addEventListener('DOMContentLoaded', () => hljs.highlightAll());</script>
81-
<title>${message.url}</title>
105+
<title>${title}</title>
82106
<style>
83107
body {
84108
line-height: 1.5;
@@ -101,6 +125,10 @@ export const handle_open_website = async (message: OpenWebsiteMessage) => {
101125
</html>
102126
`
103127
} else {
104-
vscode.env.openExternal(vscode.Uri.parse(message.url))
128+
if (message.url.startsWith('http')) {
129+
vscode.env.openExternal(vscode.Uri.parse(message.url))
130+
} else {
131+
vscode.window.showErrorMessage('Website file not found.')
132+
}
105133
}
106134
}

‎apps/editor/src/views/prompt/backend/message-handlers/handle-paste-url.ts‎

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import * as vscode from 'vscode'
22
import { PasteUrlMessage } from '../../types/messages'
33
import { PromptViewProvider } from '../prompt-view-provider'
4-
import * as fs from 'fs'
5-
import {
6-
fetch_and_save_website,
7-
get_website_file_path
8-
} from '../utils/website-fetcher'
4+
import { fetch_website } from '../utils/website-fetcher'
5+
import { PersistentTextStorage } from '../utils/persistent-text-storage'
96

107
export const handle_paste_url = async (
118
prompt_view_provider: PromptViewProvider,
@@ -23,13 +20,9 @@ export const handle_paste_url = async (
2320
}
2421

2522
try {
26-
const url = message.url
27-
const file_path = get_website_file_path(url)
28-
29-
if (fs.existsSync(file_path)) {
30-
prompt_view_provider.add_text_at_cursor_position(`#Website(${url})`)
31-
return
32-
}
23+
const storage = new PersistentTextStorage(
24+
prompt_view_provider.extension_context
25+
)
3326

3427
const abort_controller = new AbortController()
3528
prompt_view_provider.api_call_abort_controller = abort_controller
@@ -41,15 +34,40 @@ export const handle_paste_url = async (
4134
})
4235

4336
let content: string | null = null
37+
let title: string = ''
38+
let id: string | null = null
4439
try {
45-
content = await fetch_and_save_website(url, abort_controller.signal)
40+
const result = await fetch_website(message.url, abort_controller.signal)
41+
if (result) {
42+
content = result.content
43+
title = result.title
44+
id = await storage.save(content)
45+
}
4646
} finally {
4747
prompt_view_provider.send_message({ command: 'HIDE_PROGRESS' })
4848
prompt_view_provider.api_call_abort_controller = null
4949
}
5050

51-
if (content) {
52-
prompt_view_provider.add_text_at_cursor_position(`#Website(${url})`)
51+
if (content && id) {
52+
const token_count = Math.ceil(content.length / 4)
53+
54+
let title_part = ''
55+
if (title) {
56+
title_part = ` "${title.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
57+
} else {
58+
try {
59+
const parsed = new URL(message.url)
60+
let hostname = parsed.hostname
61+
if (hostname.startsWith('www.')) {
62+
hostname = hostname.slice(4)
63+
}
64+
title_part = ` "${hostname}"`
65+
} catch {}
66+
}
67+
68+
prompt_view_provider.add_text_at_cursor_position(
69+
`#Website(${message.url} ${id}:${token_count}${title_part})`
70+
)
5371
} else {
5472
await paste_as_text()
5573
}

‎apps/editor/src/views/prompt/backend/message-handlers/handle-save-prompt-pasted-text.ts‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
1-
import * as crypto from 'crypto'
2-
import * as fs from 'fs'
3-
import * as os from 'os'
4-
import * as path from 'path'
51
import * as vscode from 'vscode'
62
import { SavePromptPastedTextMessage } from '../../types/messages'
73
import { PromptViewProvider } from '../prompt-view-provider'
4+
import { PersistentTextStorage } from '../utils/persistent-text-storage'
85

96
export const handle_save_prompt_pasted_text = async (
107
prompt_view_provider: PromptViewProvider,
118
message: SavePromptPastedTextMessage
129
) => {
1310
try {
14-
const hash = crypto.createHash('md5').update(message.text).digest('hex')
15-
const filename = `cwc-paste-${hash}.txt`
16-
const file_path = path.join(os.tmpdir(), filename)
17-
await fs.promises.writeFile(file_path, message.text, 'utf-8')
11+
const storage = new PersistentTextStorage(
12+
prompt_view_provider.extension_context
13+
)
14+
const id = await storage.save(message.text)
15+
1816
const token_count = Math.ceil(message.text.length / 4)
1917
prompt_view_provider.add_text_at_cursor_position(
20-
`#PastedText(${hash}:${token_count})`
18+
`#PastedText(${id}:${token_count})`
2119
)
2220
} catch (error) {
2321
vscode.window.showErrorMessage(`Failed to save pasted text: ${error}`)

‎apps/editor/src/views/prompt/backend/prompt-view-provider.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -864,11 +864,11 @@ export class PromptViewProvider implements vscode.WebviewViewProvider {
864864
} else if (message.command == 'SAVE_PROMPT_PASTED_TEXT') {
865865
await handle_save_prompt_pasted_text(this, message)
866866
} else if (message.command == 'OPEN_PROMPT_PASTED_TEXT') {
867-
await handle_open_prompt_pasted_text(message)
867+
await handle_open_prompt_pasted_text(this, message)
868868
} else if (message.command == 'PASTE_URL') {
869869
await handle_paste_url(this, message)
870870
} else if (message.command == 'OPEN_WEBSITE') {
871-
await handle_open_website(message)
871+
await handle_open_website(this, message)
872872
} else if (message.command == 'SET_RECORDING_STATE') {
873873
await handle_voice_input(this, message)
874874
} else if (message.command == 'GET_SETUP_PROGRESS') {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as fs from 'fs'
2+
import * as path from 'path'
3+
import * as vscode from 'vscode'
4+
import * as crypto from 'crypto'
5+
6+
export const GARBAGE_COLLECTION_THRESHOLD = 7884000000 // 3 months
7+
8+
export class PersistentTextStorage {
9+
private dir_path: string
10+
11+
constructor(extension_context: vscode.ExtensionContext) {
12+
this.dir_path = path.join(
13+
extension_context.globalStorageUri.fsPath,
14+
'prompt-text'
15+
)
16+
}
17+
18+
async save(content: string): Promise<string> {
19+
const id = crypto
20+
.createHash('sha256')
21+
.update(content)
22+
.digest('hex')
23+
.substring(0, 16)
24+
if (!fs.existsSync(this.dir_path)) {
25+
await fs.promises.mkdir(this.dir_path, { recursive: true })
26+
}
27+
const filename = `${id}.txt`
28+
const file_path = path.join(this.dir_path, filename)
29+
30+
if (fs.existsSync(file_path)) {
31+
const now = new Date()
32+
await fs.promises.utimes(file_path, now, now)
33+
} else {
34+
await fs.promises.writeFile(file_path, content, 'utf-8')
35+
}
36+
37+
await this.garbage_collect()
38+
39+
return id
40+
}
41+
42+
async load(id: string): Promise<string> {
43+
const filename = `${id}.txt`
44+
const file_path = path.join(this.dir_path, filename)
45+
return await fs.promises.readFile(file_path, 'utf-8')
46+
}
47+
48+
get_file_path(id: string): string {
49+
const filename = `${id}.txt`
50+
return path.join(this.dir_path, filename)
51+
}
52+
53+
private async garbage_collect() {
54+
try {
55+
if (!fs.existsSync(this.dir_path)) {
56+
return
57+
}
58+
const files = await fs.promises.readdir(this.dir_path)
59+
const threshold_time = Date.now() - GARBAGE_COLLECTION_THRESHOLD
60+
for (const file of files) {
61+
if (file.endsWith('.txt')) {
62+
const file_path = path.join(this.dir_path, file)
63+
try {
64+
const stats = await fs.promises.stat(file_path)
65+
if (stats.mtimeMs < threshold_time) {
66+
await fs.promises.unlink(file_path)
67+
}
68+
} catch (err) {
69+
// ignore
70+
}
71+
}
72+
}
73+
} catch (gc_error) {
74+
console.error(
75+
`Failed to garbage collect files in ${this.dir_path}:`,
76+
gc_error
77+
)
78+
}
79+
}
80+
}

‎apps/editor/src/views/prompt/backend/utils/symbols/pasted-text/replace-pasted-text-symbol.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import * as fs from 'fs'
2-
import * as os from 'os'
3-
import * as path from 'path'
1+
import * as vscode from 'vscode'
2+
import { PersistentTextStorage } from '../../persistent-text-storage'
43

54
export const replace_pasted_text_symbol = async (params: {
65
instructions: string[]
6+
extension_context: vscode.ExtensionContext
77
}): Promise<string[]> => {
8+
const storage = new PersistentTextStorage(params.extension_context)
9+
810
return Promise.all(
911
params.instructions.map(async (instruction) => {
1012
const regex = /#PastedText\(([a-fA-F0-9]+)(?::\d+)?\)/g
@@ -17,11 +19,9 @@ export const replace_pasted_text_symbol = async (params: {
1719

1820
const replacements = await Promise.all(
1921
matches.map(async (match) => {
20-
const hash = match[1]
21-
const filename = `cwc-paste-${hash}.txt`
22-
const file_path = path.join(os.tmpdir(), filename)
22+
const id = match[1]
2323
try {
24-
const content = await fs.promises.readFile(file_path, 'utf-8')
24+
const content = await storage.load(id)
2525
return {
2626
content,
2727
success: true

‎apps/editor/src/views/prompt/backend/utils/symbols/replace-symbols.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,15 @@ export const replace_symbols = async (params: {
7575

7676
if (processed_instructions.some((i) => i.includes('#PastedText('))) {
7777
processed_instructions = await replace_pasted_text_symbol({
78-
instructions: processed_instructions
78+
instructions: processed_instructions,
79+
extension_context: params.extension_context
7980
})
8081
}
8182

8283
if (processed_instructions.some((i) => i.includes('#Website('))) {
8384
processed_instructions = await replace_website_symbol({
8485
instructions: processed_instructions,
86+
extension_context: params.extension_context,
8587
symbols_cache: params.symbols_cache
8688
})
8789
}

0 commit comments

Comments
 (0)