-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcopilot-buffer.el
More file actions
537 lines (484 loc) · 23.2 KB
/
Copy pathcopilot-buffer.el
File metadata and controls
537 lines (484 loc) · 23.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
;;; copilot-buffer.el --- Buffer memory and attention layer for Copilot AI OS -*- lexical-binding: t -*-
;; Buffer Memory Architecture:
;; - Buffers = Memory segments (L1/L2/L3/disk hierarchy)
;; - Windows = Attention (what user is focused on)
;; - Registers = Saved cognitive contexts
;; - copilot-buffer-mode = AI permissions per buffer
;;; Code:
(require 'copilot-sdk)
;;; --- Buffer Registry ---
(defvar copilot-buffer-registry (make-hash-table :test 'equal)
"Registry tracking AI-relevant buffer metadata.
Maps buffer-name → plist with :type, :level, :last-accessed, :agent-id, :permissions.")
(defvar copilot-buffer--type-patterns
'((:org-task . (lambda (buf)
(and (buffer-file-name buf)
(string-suffix-p ".org" (buffer-file-name buf))
(with-current-buffer buf
(save-excursion
(goto-char (point-min))
(re-search-forward "^\\*+ \\(TODO\\|DONE\\|WAIT\\|NEXT\\)" nil t))))))
(:org-roam . (lambda (buf)
(and (buffer-file-name buf)
(string-match-p "/roam/" (buffer-file-name buf)))))
(:journal . (lambda (buf)
(and (buffer-file-name buf)
(string-match-p "/journal/" (buffer-file-name buf)))))
(:agenda . (lambda (buf)
(string-match-p "\\*Org Agenda\\*" (buffer-name buf))))
(:elfeed . (lambda (buf)
(string-match-p "\\*elfeed" (buffer-name buf))))
(:chat . (lambda (buf)
(string-match-p "\\*copilot" (buffer-name buf))))
(:scratch . (lambda (buf)
(string-match-p "\\*copilot-scratch" (buffer-name buf))))
(:dired . (lambda (buf)
(eq (buffer-local-value 'major-mode buf) 'dired-mode)))
(:source . (lambda (buf)
(and (buffer-file-name buf)
(not (string-suffix-p ".org" (buffer-file-name buf)))))))
"Patterns to auto-detect buffer type. Checked in order, first match wins.")
(defun copilot-buffer--detect-type (buffer)
"Detect the AI-relevant type of BUFFER."
(catch 'found
(dolist (pattern copilot-buffer--type-patterns)
(when (funcall (cdr pattern) buffer)
(throw 'found (car pattern))))
:unknown))
(defun copilot-buffer--detect-level (buffer)
"Detect the memory hierarchy level of BUFFER.
:L1 = current buffer, :L2 = visible in window, :L3 = loaded but hidden."
(cond
((eq buffer (current-buffer)) :L1)
((get-buffer-window buffer t) :L2) ; visible in any frame
(t :L3)))
(defun copilot-buffer-update-registry ()
"Update the buffer registry with current buffer states.
Called on buffer-list-update-hook."
(let ((live-buffers '()))
(dolist (buf (buffer-list))
(unless (string-prefix-p " " (buffer-name buf)) ; skip hidden buffers
(let ((name (buffer-name buf)))
(push name live-buffers)
(puthash name
(list :type (copilot-buffer--detect-type buf)
:level (copilot-buffer--detect-level buf)
:file (buffer-file-name buf)
:mode (buffer-local-value 'major-mode buf)
:modified (buffer-modified-p buf)
:size (buffer-size buf)
:last-accessed (current-time)
:ai-aware (buffer-local-value 'copilot-buffer-mode buf))
copilot-buffer-registry))))
;; Remove stale entries
(maphash (lambda (key _val)
(unless (member key live-buffers)
(remhash key copilot-buffer-registry)))
copilot-buffer-registry)))
;;; --- copilot-buffer-mode: AI-Aware Minor Mode ---
(defvar-local copilot-buffer--ai-permission :read
"AI permission level for this buffer.
:read = AI can read, :read-write = AI can read and write,
:observe = AI gets change notifications only.")
(defvar copilot-buffer--change-log nil
"Recent buffer changes tracked by copilot-buffer-mode.
List of (buffer-name timestamp change-summary) entries.")
(defun copilot-buffer--after-change (beg end old-len)
"Track changes in AI-aware buffers.
Called from `after-change-functions' when `copilot-buffer-mode' is active."
(when copilot-buffer-mode
(let ((change-text (buffer-substring-no-properties
beg (min end (+ beg 100)))))
(push (list (buffer-name) (current-time)
(format "%s chars at pos %d" (- end beg) beg)
(truncate-string-to-width change-text 50))
copilot-buffer--change-log)
;; Keep only last 50 changes
(when (> (length copilot-buffer--change-log) 50)
(setq copilot-buffer--change-log
(seq-take copilot-buffer--change-log 50))))))
(defvar copilot-buffer-mode-map
(let ((map (make-sparse-keymap)))
map)
"Keymap for `copilot-buffer-mode'.")
;;;###autoload
(define-minor-mode copilot-buffer-mode
"Minor mode marking a buffer as AI-aware.
When enabled, AI agents can read this buffer and receive change notifications.
Use `copilot-buffer--ai-permission' to control access level."
:lighter " 🤖"
:keymap copilot-buffer-mode-map
(if copilot-buffer-mode
(progn
(add-hook 'after-change-functions #'copilot-buffer--after-change nil t)
(copilot-buffer-update-registry))
(remove-hook 'after-change-functions #'copilot-buffer--after-change t)))
(defun copilot-buffer-set-permission (permission)
"Set AI PERMISSION level for current buffer.
PERMISSION is one of: :read, :read-write, :observe."
(interactive
(list (intern (completing-read "AI Permission: "
'(":read" ":read-write" ":observe")))))
(setq-local copilot-buffer--ai-permission permission)
(message "AI permission set to %s for %s" permission (buffer-name)))
;;; --- Attention Tracking ---
(defvar copilot-buffer--attention-history nil
"History of window configuration changes (attention shifts).")
(defun copilot-buffer--track-attention (&rest _)
"Track window configuration changes as attention shifts."
(let ((visible (mapcar (lambda (w) (buffer-name (window-buffer w)))
(window-list))))
(push (list (current-time) visible) copilot-buffer--attention-history)
;; Keep last 20
(when (> (length copilot-buffer--attention-history) 20)
(setq copilot-buffer--attention-history
(seq-take copilot-buffer--attention-history 20)))))
;;; --- Context Saving/Restoring via Registers ---
(defun copilot-buffer-save-context (register)
"Save current window configuration to REGISTER as a named context."
(interactive "cSave context to register: ")
(window-configuration-to-register register)
(message "Saved attention context to register '%c'" register))
(defun copilot-buffer-restore-context (register)
"Restore window configuration from REGISTER."
(interactive "cRestore context from register: ")
(jump-to-register register)
(message "Restored attention context from register '%c'" register))
;;; --- Scratch (Working Memory) Buffers ---
(defvar copilot-buffer--scratch-buffers nil
"List of active AI scratch buffer names.")
(defun copilot-buffer-scratch-create (name)
"Create a named AI scratch buffer in org-mode for working memory.
NAME is used as the buffer name."
(interactive "sScratch buffer name: ")
(let* ((buf-name (format "*copilot-scratch: %s*" name))
(buf (get-buffer-create buf-name)))
(with-current-buffer buf
(org-mode)
(copilot-buffer-mode 1)
(setq-local copilot-buffer--ai-permission :read-write)
(when (= (buffer-size) 0)
(insert (format "#+title: AI Scratch: %s\n" name))
(insert (format "#+date: %s\n" (format-time-string "%Y-%m-%d")))
(insert "#+filetags: :scratch:ai:\n\n")))
(push buf-name copilot-buffer--scratch-buffers)
(switch-to-buffer-other-window buf)
buf))
(defun copilot-buffer-scratch-persist (name)
"Save scratch buffer NAME to org-roam/fleeting/ as a permanent note."
(interactive
(list (completing-read "Persist scratch: " copilot-buffer--scratch-buffers)))
(let* ((buf-name (if (string-prefix-p "*copilot-scratch:" name) name
(format "*copilot-scratch: %s*" name)))
(buf (get-buffer buf-name))
(fleeting-dir (expand-file-name "~/org/roam/fleeting/"))
(slug (replace-regexp-in-string "[^a-zA-Z0-9-]" "-"
(downcase name)))
(target (expand-file-name (format "%s-%s.org"
(format-time-string "%Y%m%d%H%M%S")
slug)
fleeting-dir)))
(unless buf (error "Scratch buffer not found: %s" name))
(with-current-buffer buf
(write-file target)
(when (fboundp 'org-roam-db-update-file)
(org-roam-db-update-file target)))
(setq copilot-buffer--scratch-buffers
(delete buf-name copilot-buffer--scratch-buffers))
(message "Persisted scratch to %s" target)))
(defun copilot-buffer-scratch-promote (name target-dir)
"Promote a fleeting scratch note to a PARA category.
TARGET-DIR is projects, areas, or resources."
(interactive
(list (read-string "Scratch name: ")
(completing-read "Promote to: " '("projects" "areas" "resources"))))
(let* ((fleeting-dir (expand-file-name "~/org/roam/fleeting/"))
(files (directory-files fleeting-dir t (regexp-quote name)))
(source (car files))
(target (expand-file-name
(file-name-nondirectory source)
(expand-file-name (concat "~/org/roam/" target-dir "/")))))
(if source
(progn
(rename-file source target)
(when (fboundp 'org-roam-db-update-file)
(org-roam-db-update-file target))
(message "Promoted %s to %s" (file-name-nondirectory source) target-dir))
(error "No fleeting note found matching: %s" name))))
;;; --- Tool Registration ---
(require 'copilot-sdk-tools)
(defun copilot-buffer--tool-snapshot (_args)
"Tool handler: get snapshot of all open buffers with AI-relevant metadata."
(copilot-buffer-update-registry)
(let ((entries '()))
(maphash (lambda (name props)
(push (list :name name
:type (plist-get props :type)
:level (plist-get props :level)
:file (plist-get props :file)
:mode (symbol-name (plist-get props :mode))
:modified (plist-get props :modified)
:size (plist-get props :size)
:ai-aware (plist-get props :ai-aware))
entries))
copilot-buffer-registry)
(let ((l1 (seq-filter (lambda (e) (eq (plist-get e :level) :L1)) entries))
(l2 (seq-filter (lambda (e) (eq (plist-get e :level) :L2)) entries))
(l3 (seq-filter (lambda (e) (eq (plist-get e :level) :L3)) entries)))
(format "Buffer Snapshot (%d total):\n\n== L1 (Current Focus) ==\n%s\n\n== L2 (Visible/Attention) ==\n%s\n\n== L3 (Loaded/Warm Cache) ==\n%s"
(length entries)
(copilot-buffer--format-entries l1)
(copilot-buffer--format-entries l2)
(copilot-buffer--format-entries l3)))))
(defun copilot-buffer--tool-context (_args)
"Tool handler: get rich context about the current buffer environment."
(copilot-buffer-update-registry)
(let* ((current (buffer-name))
(current-file (buffer-file-name))
(current-mode (symbol-name major-mode))
(visible (mapcar (lambda (w)
(let ((buf (window-buffer w)))
(format " %s [%s] %s"
(buffer-name buf)
(with-current-buffer buf
(symbol-name major-mode))
(if (buffer-file-name buf)
(abbreviate-file-name (buffer-file-name buf))
"(no file)"))))
(window-list)))
(recent-changes (seq-take copilot-buffer--change-log 10))
(attention (car copilot-buffer--attention-history))
(type-counts (make-hash-table)))
(maphash (lambda (_name props)
(let ((type (plist-get props :type)))
(puthash type (1+ (or (gethash type type-counts) 0)) type-counts)))
copilot-buffer-registry)
(format "=== Buffer Context ===\n\nCurrent: %s [%s] %s\n\nVisible Windows (%d):\n%s\n\nBuffer Types:\n%s\n\nRecent Changes (%d):\n%s\n\nLast Attention Shift: %s"
current current-mode (or current-file "(no file)")
(length visible)
(mapconcat #'identity visible "\n")
(let ((type-str ""))
(maphash (lambda (k v) (setq type-str (concat type-str (format " %s: %d\n" k v)))) type-counts)
type-str)
(length recent-changes)
(if recent-changes
(mapconcat (lambda (c) (format " [%s] %s: %s"
(format-time-string "%H:%M:%S" (nth 1 c))
(nth 0 c) (nth 2 c)))
recent-changes "\n")
" (none)")
(if attention
(format "%s — buffers: %s"
(format-time-string "%H:%M:%S" (car attention))
(mapconcat #'identity (cadr attention) ", "))
"(no data)"))))
(defun copilot-buffer--tool-attention (_args)
"Tool handler: get the current attention pattern."
(let ((windows '()))
(walk-windows
(lambda (w)
(let* ((buf (window-buffer w))
(name (buffer-name buf))
(type (copilot-buffer--detect-type buf))
(edges (window-edges w))
(width (- (nth 2 edges) (nth 0 edges)))
(height (- (nth 3 edges) (nth 1 edges))))
(push (format " [%dx%d at (%d,%d)] %s (%s) %s"
width height (nth 0 edges) (nth 1 edges)
name type
(or (buffer-file-name buf) ""))
windows)))
nil t)
(format "Attention Pattern:\n%s\n\nDomains visible: %s"
(mapconcat #'identity (nreverse windows) "\n")
(mapconcat #'symbol-name
(delete-dups
(mapcar (lambda (w) (copilot-buffer--detect-type (window-buffer w)))
(window-list)))
", "))))
(defun copilot-buffer--tool-scratch-create (args)
"Tool handler: create an AI scratch buffer."
(let ((name (or (alist-get 'name args) (alist-get "name" args nil nil #'equal))))
(unless name (error "Name is required"))
(copilot-buffer-scratch-create name)
(format "Created scratch buffer: *copilot-scratch: %s*" name)))
(defun copilot-buffer--tool-scratch-persist (args)
"Tool handler: persist a scratch buffer to org-roam/fleeting/."
(let ((name (or (alist-get 'name args) (alist-get "name" args nil nil #'equal))))
(unless name (error "Name is required"))
(copilot-buffer-scratch-persist name)
(format "Persisted scratch '%s' to roam/fleeting/" name)))
(defun copilot-buffer-register-tools ()
"Register buffer memory and attention tools with the Copilot SDK."
(copilot-sdk-tools-register
"buffer-snapshot"
"Get a snapshot of all open buffers with their AI-relevant metadata.
Returns buffer type (org-task, org-roam, journal, source, chat, scratch, etc.),
memory level (L1=current, L2=visible, L3=hidden), modification status, and size."
(copilot-sdk-tools--make-schema nil)
#'copilot-buffer--tool-snapshot)
(copilot-sdk-tools-register
"buffer-context"
"Get rich context about the current buffer environment.
Includes all visible buffers, window layout, recent changes, and attention state.
This is the AI's 'peripheral vision' — awareness of the full workspace."
(copilot-sdk-tools--make-schema nil)
#'copilot-buffer--tool-context)
(copilot-sdk-tools-register
"buffer-attention"
"Get the current attention pattern — which buffers are visible, their layout,
and what domains they represent. Use this to understand what the user is focused on."
(copilot-sdk-tools--make-schema nil)
#'copilot-buffer--tool-attention)
(copilot-sdk-tools-register
"scratch-create"
"Create an AI scratch buffer for working memory.
Scratch buffers are org-mode buffers for multi-step reasoning, planning, and drafting.
They can be persisted to org-roam/fleeting/ if valuable."
(copilot-sdk-tools--make-schema
(list (cons 'name (copilot-sdk-tools--string-prop "Name for the scratch buffer")))
'("name"))
#'copilot-buffer--tool-scratch-create)
(copilot-sdk-tools-register
"scratch-persist"
"Save an AI scratch buffer to org-roam/fleeting/ as a permanent note.
The scratch buffer content is written to a timestamped org file."
(copilot-sdk-tools--make-schema
(list (cons 'name (copilot-sdk-tools--string-prop "Name of the scratch buffer to persist")))
'("name"))
#'copilot-buffer--tool-scratch-persist))
;;; --- Utility Functions ---
(defun copilot-buffer--format-entries (entries)
"Format buffer ENTRIES for display."
(if entries
(mapconcat (lambda (e)
(format " %s [%s] %s%s"
(plist-get e :name)
(plist-get e :type)
(if (plist-get e :modified) "(modified) " "")
(if (plist-get e :ai-aware) " 🤖" "")))
entries "\n")
" (none)"))
;;; --- Auto Context Injection ---
(defvar copilot-buffer--auto-context-active nil
"Non-nil when buffer auto-context advice is installed.")
(defun copilot-buffer--any-ai-aware-p ()
"Return non-nil if any registered buffer has `copilot-buffer-mode' enabled."
(catch 'found
(maphash (lambda (_name props)
(when (plist-get props :ai-aware)
(throw 'found t)))
copilot-buffer-registry)
nil))
(defun copilot-buffer-context-string ()
"Return a concise string describing current buffer context for AI injection.
Includes the focus buffer, visible buffers, and recent attention shifts.
Returns nil if no meaningful context is available."
(copilot-buffer-update-registry)
(let* (;; Focus buffer: most recent non-chat, non-internal buffer
(focus-buf (catch 'found
(dolist (buf (buffer-list))
(let ((name (buffer-name buf)))
(unless (or (string-prefix-p " " name)
(string-match-p "\\*copilot" name)
(string-match-p "\\*Quick" name))
(throw 'found buf))))))
(focus-name (when focus-buf (buffer-name focus-buf)))
(focus-props (when focus-name
(gethash focus-name copilot-buffer-registry)))
(any-ai-aware (copilot-buffer--any-ai-aware-p))
(visible '())
(recent-names '())
(parts '()))
;; Focus buffer line (always shown)
(when (and focus-buf focus-props)
(push (format "[Buffer: %s (%s, %s)]"
focus-name
(symbol-name (plist-get focus-props :mode))
(substring (symbol-name (plist-get focus-props :level))
1))
parts))
;; Visible (L2) buffers, respecting copilot-buffer-mode filter
(maphash (lambda (name props)
(when (and (eq (plist-get props :level) :L2)
(not (equal name focus-name))
(not (string-prefix-p " " name))
(not (string-match-p "\\*copilot" name))
(or (not any-ai-aware)
(plist-get props :ai-aware)))
(push (format "%s (%s)"
name
(substring
(symbol-name (plist-get props :type)) 1))
visible)))
copilot-buffer-registry)
(when visible
(push (format "[Visible: %s]"
(mapconcat #'identity visible ", "))
parts))
;; Recent attention (unique buffer names from last 3 history entries)
(let ((seen (make-hash-table :test 'equal)))
(dolist (entry (seq-take copilot-buffer--attention-history 3))
(dolist (name (cadr entry))
(unless (or (gethash name seen)
(string-prefix-p " " name)
(string-match-p "\\*copilot" name))
(puthash name t seen)
(push name recent-names)))))
(setq recent-names (nreverse recent-names))
(when (> (length recent-names) 1)
(push (format "[Recent: %s]"
(mapconcat #'identity
(seq-take recent-names 5) " → "))
parts))
(when parts
(mapconcat #'identity (nreverse parts) "\n"))))
(defun copilot-buffer--augment-auto-context (orig-fn)
"Advice to include buffer context in auto-context injection.
Wraps `copilot-chat--gather-editor-context' to append buffer
registry and attention information."
(let ((base-context (funcall orig-fn))
(buffer-ctx (copilot-buffer-context-string)))
(cond
((and base-context buffer-ctx)
(concat base-context "\n" buffer-ctx))
(buffer-ctx buffer-ctx)
(t base-context))))
(defun copilot-buffer--augment-quick-prompt (orig-fn prompt &optional continuep)
"Advice to include buffer context in quick prompts.
Wraps `copilot-quick--send' to prepend buffer context on new queries."
(if continuep
(funcall orig-fn prompt continuep)
(let ((ctx (copilot-buffer-context-string)))
(if ctx
(funcall orig-fn (concat ctx "\n\n" prompt) continuep)
(funcall orig-fn prompt continuep)))))
(defun copilot-buffer-enable-auto-context ()
"Install advice to auto-inject buffer context into chat and quick sessions.
Idempotent — safe to call multiple times."
(unless copilot-buffer--auto-context-active
(when (fboundp 'copilot-chat--gather-editor-context)
(advice-add 'copilot-chat--gather-editor-context
:around #'copilot-buffer--augment-auto-context))
(when (fboundp 'copilot-quick--send)
(advice-add 'copilot-quick--send
:around #'copilot-buffer--augment-quick-prompt))
(setq copilot-buffer--auto-context-active t)))
(defun copilot-buffer-disable-auto-context ()
"Remove buffer auto-context advice.
Reverses the effect of `copilot-buffer-enable-auto-context'."
(advice-remove 'copilot-chat--gather-editor-context
#'copilot-buffer--augment-auto-context)
(advice-remove 'copilot-quick--send
#'copilot-buffer--augment-quick-prompt)
(setq copilot-buffer--auto-context-active nil))
;;; --- Initialization ---
(defun copilot-buffer-init ()
"Initialize the buffer memory and attention layer."
(add-hook 'buffer-list-update-hook #'copilot-buffer-update-registry)
(add-hook 'window-configuration-change-hook #'copilot-buffer--track-attention)
(copilot-buffer-register-tools)
(copilot-buffer-update-registry)
(copilot-buffer-enable-auto-context))
(provide 'copilot-buffer)
;;; copilot-buffer.el ends here