@consultancy-outfit/ai-draftsman-cli 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ fooaeahnnnhnbdhidpmaepmdmbnjllbk
@@ -0,0 +1,315 @@
1
+ // AI Draftsman for Chrome — service worker.
2
+ //
3
+ // Connects to the native messaging host (com.aidraftsman.browser). The host is a small process Chrome starts for us;
4
+ // the AI Draftsman CLI and desktop app talk to it, and it forwards their browser requests here as
5
+ // { id, action, args } and expects back { id, ok: true, result } | { id, ok: false, error }.
6
+ // Actions: list_tabs, navigate, read_page, click, type.
7
+
8
+ const HOST = 'com.aidraftsman.browser';
9
+ const MAX_TEXT = 20000;
10
+ const MAX_ELEMENTS = 150;
11
+ const LOG_LIMIT = 100;
12
+
13
+ let port = null;
14
+ let connected = false;
15
+ /** When true the user pressed Stop: every request is refused until they resume. */
16
+ let paused = false;
17
+
18
+ chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {});
19
+
20
+ async function log(kind, text) {
21
+ const { log: entries = [] } = await chrome.storage.session.get('log');
22
+ entries.push({ t: Date.now(), kind, text });
23
+ await chrome.storage.session.set({ log: entries.slice(-LOG_LIMIT) });
24
+ }
25
+
26
+ async function setState() {
27
+ await chrome.storage.session.set({ state: { connected, paused } });
28
+ }
29
+
30
+ function connect() {
31
+ if (port) return;
32
+ try {
33
+ port = chrome.runtime.connectNative(HOST);
34
+ } catch (e) {
35
+ port = null;
36
+ return;
37
+ }
38
+ port.onMessage.addListener(onRequest);
39
+ port.onDisconnect.addListener(() => {
40
+ const reason = chrome.runtime.lastError?.message;
41
+ port = null;
42
+ connected = false;
43
+ setState();
44
+ enqueue(async () => {
45
+ const chat = await getChat();
46
+ if (!chat.running) return;
47
+ chat.running = false;
48
+ const reply = chat.messages[chat.messages.length - 1];
49
+ if (reply?.role === 'assistant') reply.error = 'Connection to AI Draftsman was lost.';
50
+ await saveChat(chat);
51
+ });
52
+ log('error', reason ? `Host disconnected: ${reason}` : 'Host disconnected.');
53
+ });
54
+ connected = true;
55
+ port.postMessage({ type: 'hello', version: chrome.runtime.getManifest().version });
56
+ setState();
57
+ log('info', 'Connected to the AI Draftsman bridge.');
58
+ }
59
+
60
+ // A service worker sleeps; the alarm wakes it and reconnects if the host is gone.
61
+ chrome.alarms.create('reconnect', { periodInMinutes: 0.5 });
62
+ chrome.alarms.onAlarm.addListener(() => connect());
63
+ chrome.runtime.onStartup.addListener(connect);
64
+ chrome.runtime.onInstalled.addListener(connect);
65
+ connect();
66
+
67
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
68
+ if (message?.type === 'pause') {
69
+ paused = !!message.value;
70
+ setState();
71
+ log('info', paused ? 'Stopped by you. Browser actions are refused.' : 'Resumed.');
72
+ sendResponse({ ok: true });
73
+ } else if (message?.type === 'chat_send') {
74
+ enqueue(() => chatSend(String(message.text || ''), message.mode));
75
+ sendResponse({ ok: true });
76
+ } else if (message?.type === 'chat_stop') {
77
+ enqueue(async () => {
78
+ const chat = await getChat();
79
+ if (chat.running && port) port.postMessage({ type: 'chat_stop', id: chat.chatId });
80
+ });
81
+ sendResponse({ ok: true });
82
+ } else if (message?.type === 'chat_clear') {
83
+ enqueue(async () => {
84
+ const chat = await getChat();
85
+ if (chat.running && port) port.postMessage({ type: 'chat_stop', id: chat.chatId });
86
+ await saveChat({ messages: [], running: false, session: '', mode: chat.mode, chatId: '' });
87
+ });
88
+ sendResponse({ ok: true });
89
+ } else if (message?.type === 'reconnect') {
90
+ connect();
91
+ sendResponse({ ok: true });
92
+ }
93
+ return false;
94
+ });
95
+
96
+ // ---- chat -----------------------------------------------------------------------------------------------------
97
+ // The transcript lives in storage.session so the side panel can close and reopen without losing it.
98
+ // chat = { messages: [{role:'user', text} | {role:'assistant', text, steps:[{id,label,state}], error?}],
99
+ // running, session, mode, chatId }
100
+
101
+ async function getChat() {
102
+ const { chat } = await chrome.storage.session.get('chat');
103
+ return chat || { messages: [], running: false, session: '', mode: 'read', chatId: '' };
104
+ }
105
+ const saveChat = (chat) => chrome.storage.session.set({ chat });
106
+ /** Chat updates read-modify-write storage; run them one at a time so streamed events cannot overwrite each other. */
107
+ let chatQueue = Promise.resolve();
108
+ const enqueue = (fn) => (chatQueue = chatQueue.then(fn, fn));
109
+
110
+ async function chatSend(text, mode) {
111
+ const chat = await getChat();
112
+ if (chat.running || !text.trim()) return;
113
+ chat.mode = mode === 'act' ? 'act' : 'read';
114
+ chat.messages.push({ role: 'user', text });
115
+ if (!port || !connected) {
116
+ chat.messages.push({ role: 'assistant', text: '', steps: [], error: 'Not connected to AI Draftsman. Run `ad browser install`, then reload the extension.' });
117
+ return saveChat(chat);
118
+ }
119
+ chat.messages.push({ role: 'assistant', text: '', steps: [] });
120
+ chat.running = true;
121
+ chat.chatId = `chat-${Date.now()}`;
122
+ await saveChat(chat);
123
+ port.postMessage({ type: 'chat', id: chat.chatId, text, mode: chat.mode, resume: chat.session || undefined });
124
+ await log('info', `Chat: ${text.slice(0, 60)}`);
125
+ }
126
+
127
+ async function onChatEvent(message) {
128
+ const chat = await getChat();
129
+ if (message.id !== chat.chatId) return;
130
+ const reply = chat.messages[chat.messages.length - 1];
131
+ if (!reply || reply.role !== 'assistant') return;
132
+ const e = message.event || {};
133
+ if (e.kind === 'text') reply.text = reply.text ? `${reply.text}\n\n${e.text}` : e.text;
134
+ else if (e.kind === 'tool') reply.steps.push({ id: e.id, label: e.label, state: 'running' });
135
+ else if (e.kind === 'tool_result') {
136
+ const step = reply.steps.find((s) => s.id === e.id);
137
+ if (step) step.state = e.error ? 'error' : 'done';
138
+ } else if (e.kind === 'session') chat.session = e.sessionId;
139
+ else if (e.kind === 'done') {
140
+ chat.running = false;
141
+ if (!reply.text && e.text) reply.text = e.text;
142
+ } else if (e.kind === 'error') {
143
+ chat.running = false;
144
+ reply.error = e.text;
145
+ }
146
+ await saveChat(chat);
147
+ }
148
+
149
+ async function onRequest(request) {
150
+ if (request?.type === 'chat_event') return enqueue(() => onChatEvent(request));
151
+ const { id, action, args = {} } = request || {};
152
+ if (typeof id !== 'string') return;
153
+ const reply = (body) => port?.postMessage({ id, ...body });
154
+ if (paused) {
155
+ await log('error', `Refused ${action}: stopped by you.`);
156
+ return reply({ ok: false, error: 'The user stopped AI Draftsman in the browser. Ask them to resume it.' });
157
+ }
158
+ try {
159
+ const result = await run(action, args);
160
+ reply({ ok: true, result });
161
+ } catch (e) {
162
+ const message = e instanceof Error ? e.message : String(e);
163
+ await log('error', `${action} failed: ${message}`);
164
+ reply({ ok: false, error: message });
165
+ }
166
+ }
167
+
168
+ async function targetTab(args) {
169
+ if (Number.isInteger(args.tab_id)) return chrome.tabs.get(args.tab_id);
170
+ const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
171
+ if (!tab) throw new Error('No active tab. Open a page in Chrome first.');
172
+ return tab;
173
+ }
174
+
175
+ function assertScriptable(tab) {
176
+ if (!/^https?:/i.test(tab.url || '')) {
177
+ throw new Error(`Tab ${tab.id} is ${tab.url || 'blank'}, which AI Draftsman cannot read. Navigate it to an http(s) page first.`);
178
+ }
179
+ }
180
+
181
+ async function mark(tab) {
182
+ await chrome.action.setBadgeBackgroundColor({ color: '#d97706', tabId: tab.id });
183
+ await chrome.action.setBadgeText({ text: 'AI', tabId: tab.id });
184
+ }
185
+
186
+ async function waitForLoad(tabId, timeoutMs = 15000) {
187
+ const start = Date.now();
188
+ for (;;) {
189
+ const tab = await chrome.tabs.get(tabId);
190
+ if (tab.status === 'complete') return tab;
191
+ if (Date.now() - start > timeoutMs) return tab;
192
+ await new Promise((r) => setTimeout(r, 120));
193
+ }
194
+ }
195
+
196
+ async function run(action, args) {
197
+ switch (action) {
198
+ case 'list_tabs': {
199
+ const tabs = await chrome.tabs.query({});
200
+ await log('read', `Listed ${tabs.length} tabs`);
201
+ return tabs.map((t) => ({ tab_id: t.id, active: t.active, title: t.title, url: t.url }));
202
+ }
203
+ case 'navigate': {
204
+ let tab;
205
+ if (Number.isInteger(args.tab_id)) tab = await chrome.tabs.update(args.tab_id, { url: args.url });
206
+ else tab = await chrome.tabs.create({ url: args.url });
207
+ await mark(tab);
208
+ const done = await waitForLoad(tab.id);
209
+ await log('act', `Opened ${args.url}`);
210
+ return { tab_id: done.id, title: done.title, url: done.url };
211
+ }
212
+ case 'read_page': {
213
+ const tab = await targetTab(args);
214
+ assertScriptable(tab);
215
+ await mark(tab);
216
+ const [{ result }] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: pageSnapshot, args: [MAX_TEXT, MAX_ELEMENTS] });
217
+ await log('read', `Read "${result.title}" (${result.elements} elements)`);
218
+ return `tab_id: ${tab.id}\ntitle: ${result.title}\nurl: ${result.url}\n\n${result.text}\n\nInteractive elements:\n${result.list}`;
219
+ }
220
+ case 'click': {
221
+ const tab = await targetTab(args);
222
+ assertScriptable(tab);
223
+ await mark(tab);
224
+ const [{ result }] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: pageClick, args: [args.ref] });
225
+ if (!result.ok) throw new Error(result.error);
226
+ await log('act', `Clicked [${args.ref}] ${result.label}`);
227
+ return `Clicked [${args.ref}] ${result.label}. Read the page again before the next step.`;
228
+ }
229
+ case 'type': {
230
+ const tab = await targetTab(args);
231
+ assertScriptable(tab);
232
+ await mark(tab);
233
+ const [{ result }] = await chrome.scripting.executeScript({
234
+ target: { tabId: tab.id },
235
+ func: pageType,
236
+ args: [args.ref, String(args.text ?? ''), !!args.submit],
237
+ });
238
+ if (!result.ok) throw new Error(result.error);
239
+ await log('act', `Typed into [${args.ref}] ${result.label}`);
240
+ return `Typed into [${args.ref}] ${result.label}.${args.submit ? ' Pressed Enter.' : ''} Read the page again before the next step.`;
241
+ }
242
+ default:
243
+ throw new Error(`Unknown action "${action}".`);
244
+ }
245
+ }
246
+
247
+ // ---- functions below run INSIDE the page (executeScript serialises them: no closures) -----------------------------
248
+
249
+ function pageSnapshot(maxText, maxElements) {
250
+ const visible = (el) => {
251
+ const r = el.getBoundingClientRect();
252
+ const s = getComputedStyle(el);
253
+ return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none';
254
+ };
255
+ for (const old of document.querySelectorAll('[data-ad-ref]')) old.removeAttribute('data-ad-ref');
256
+ const selector = 'a[href],button,input:not([type=hidden]),select,textarea,[role=button],[role=link],[role=tab],[contenteditable=true]';
257
+ const lines = [];
258
+ let n = 0;
259
+ for (const el of document.querySelectorAll(selector)) {
260
+ if (n >= maxElements) break;
261
+ if (!visible(el) || el.disabled) continue;
262
+ const tag = el.tagName.toLowerCase();
263
+ const type = tag === 'input' ? el.type || 'text' : el.getAttribute('role') || tag;
264
+ const label = (el.getAttribute('aria-label') || el.innerText || el.value || el.placeholder || el.title || el.name || '').trim().replace(/\s+/g, ' ').slice(0, 80);
265
+ el.setAttribute('data-ad-ref', String(n));
266
+ const extra = tag === 'a' ? ` -> ${new URL(el.href, location.href).pathname}` : type === 'password' ? ' (password field: do not fill)' : '';
267
+ lines.push(`[${n}] ${type} "${label}"${extra}`);
268
+ n++;
269
+ }
270
+ const text = (document.body?.innerText || '').replace(/\n{3,}/g, '\n\n').trim();
271
+ return {
272
+ title: document.title,
273
+ url: location.href,
274
+ text: text.length > maxText ? text.slice(0, maxText) + `\n… [truncated ${text.length - maxText} characters]` : text,
275
+ list: lines.join('\n') || '(none)',
276
+ elements: n,
277
+ };
278
+ }
279
+
280
+ function pageClick(ref) {
281
+ const el = document.querySelector(`[data-ad-ref="${ref}"]`);
282
+ if (!el) return { ok: false, error: `No element [${ref}] on this page. Call read_page again to get fresh refs.` };
283
+ const label = (el.getAttribute('aria-label') || el.innerText || el.value || '').trim().replace(/\s+/g, ' ').slice(0, 60);
284
+ el.scrollIntoView({ block: 'center' });
285
+ el.focus?.();
286
+ el.click();
287
+ return { ok: true, label };
288
+ }
289
+
290
+ function pageType(ref, text, submit) {
291
+ const el = document.querySelector(`[data-ad-ref="${ref}"]`);
292
+ if (!el) return { ok: false, error: `No element [${ref}] on this page. Call read_page again to get fresh refs.` };
293
+ if (el instanceof HTMLInputElement && el.type === 'password') {
294
+ return { ok: false, error: 'This is a password field. AI Draftsman never types into password fields; ask the user to enter it.' };
295
+ }
296
+ const label = (el.getAttribute('aria-label') || el.placeholder || el.name || el.tagName).toString().slice(0, 60);
297
+ el.scrollIntoView({ block: 'center' });
298
+ el.focus();
299
+ if (el.isContentEditable) {
300
+ el.textContent = text;
301
+ } else if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
302
+ // Frameworks track the value through the native setter; assigning .value directly can be ignored.
303
+ const proto = el instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
304
+ Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, text);
305
+ } else {
306
+ return { ok: false, error: `Element [${ref}] is not something you can type into.` };
307
+ }
308
+ el.dispatchEvent(new Event('input', { bubbles: true }));
309
+ el.dispatchEvent(new Event('change', { bubbles: true }));
310
+ if (submit) {
311
+ if (el.form?.requestSubmit) el.form.requestSubmit();
312
+ else el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
313
+ }
314
+ return { ok: true, label };
315
+ }
Binary file
Binary file
Binary file
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="48 -6 64 64" width="64" height="64" role="img" aria-label="AI Draftsman">
2
+ <defs>
3
+ <linearGradient id="face" x1="0" y1="16" x2="0" y2="36" gradientUnits="userSpaceOnUse">
4
+ <stop offset="0" stop-color="#6C3FCB"/>
5
+ <stop offset="1" stop-color="#F5A623"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect x="48" y="-6" width="64" height="64" rx="14" fill="#17112B"/>
9
+ <rect x="55.75" y="11.9" width="48.5" height="28.2" rx="14.1" fill="none" stroke="#FFFFFF" stroke-width="5.3"/>
10
+ <circle cx="89.62" cy="25.99" r="10" fill="url(#face)"/>
11
+ <circle cx="86" cy="24" r="3" fill="#FFFFFF"/>
12
+ <circle cx="93.24" cy="24" r="3" fill="#FFFFFF"/>
13
+ </svg>
@@ -0,0 +1,34 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "AI Draftsman for Chrome",
4
+ "version": "0.1.0",
5
+ "description": "Lets the AI Draftsman CLI and desktop app use your Chrome: read the page, click, type, open links. Concept build.",
6
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtdqIlcOylVwytn5cwb4dVC70QNneoUFFCvhDepF20owMMJ1EoY6iCMUxqpymIF9KWkFbC8uuODNX7rfuWdA9Rpn7z/t40WCCI1M6UHahAflI28bOmvLe8Q1ie7KBhM3MUCuDuEckjbMuAotTz9qLqA2bB1BgyPrEZqOLHQHsp7Hu7iP51biYiMIPQzcRW3j8DI/LqYEkSXY9k6fqCGoLR7yl/pnLxx9y8WWxG3mHOYS28J+WQwr6ma+we5ijboUZ20TVQKncFWK7COguKEuC8ZzGguYnxlKmMFR5Fsu46zFz+CZVCjh9IjgPGrqKw44XmvpQKmCb1OEPT7bfS8VJKwIDAQAB",
7
+ "minimum_chrome_version": "116",
8
+ "permissions": [
9
+ "nativeMessaging",
10
+ "tabs",
11
+ "scripting",
12
+ "sidePanel",
13
+ "storage",
14
+ "alarms"
15
+ ],
16
+ "host_permissions": [
17
+ "http://*/*",
18
+ "https://*/*"
19
+ ],
20
+ "background": {
21
+ "service_worker": "background.js"
22
+ },
23
+ "side_panel": {
24
+ "default_path": "sidepanel.html"
25
+ },
26
+ "action": {
27
+ "default_title": "AI Draftsman for Chrome"
28
+ },
29
+ "icons": {
30
+ "16": "icons/16.png",
31
+ "48": "icons/48.png",
32
+ "128": "icons/128.png"
33
+ }
34
+ }
@@ -0,0 +1,104 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>AI Draftsman for Chrome</title>
7
+ <style>
8
+ :root { --bg:#faf8ff; --fg:#1d1633; --dim:#6b6385; --card:#fff; --line:#e4ddf5; --brand:#6c3fcb; --brand-soft:#ece6fb; --amber:#d98a0b; --ok:#15803d; --bad:#b91c1c; }
9
+ @media (prefers-color-scheme: dark) { :root { --bg:#120e20; --fg:#efeaff; --dim:#a79fc4; --card:#1c1630; --line:#2f2750; --brand:#a78bfa; --brand-soft:#2b2150; --amber:#f5a623; --ok:#4ade80; --bad:#f87171; } }
10
+ * { box-sizing: border-box; }
11
+ html, body { height:100%; }
12
+ body { margin:0; background:var(--bg); color:var(--fg); font:14px/1.45 system-ui, sans-serif; display:flex; flex-direction:column; }
13
+ header { padding:12px 16px 0; }
14
+ h1 { font-size:15px; margin:0; display:flex; align-items:center; gap:8px; }
15
+ .wordmark { flex:none; height:20px; width:auto; color:var(--fg); }
16
+ .conn { margin-left:auto; display:flex; align-items:center; gap:6px; font-size:12px; font-weight:400; color:var(--dim); }
17
+ .dot { width:9px; height:9px; border-radius:50%; background:var(--bad); flex:none; }
18
+ .dot.on { background:var(--ok); }
19
+ .tabs { display:flex; gap:4px; margin-top:10px; border-bottom:1px solid var(--line); }
20
+ .tab { background:none; border:0; border-bottom:2px solid transparent; color:var(--dim); padding:8px 12px; font:inherit; cursor:pointer; }
21
+ .tab[aria-selected="true"] { color:var(--fg); border-bottom-color:var(--brand); font-weight:600; }
22
+ button:focus-visible, textarea:focus-visible, select:focus-visible { outline:2px solid var(--brand); outline-offset:2px; }
23
+ main { flex:1; min-height:0; display:flex; flex-direction:column; }
24
+ [hidden] { display:none !important; }
25
+
26
+ #chatView { flex:1; min-height:0; display:flex; flex-direction:column; }
27
+ #thread { flex:1; overflow-y:auto; padding:14px 16px; display:flex; flex-direction:column; gap:12px; }
28
+ .empty { color:var(--dim); font-size:13px; }
29
+ .suggest { display:flex; flex-direction:column; gap:6px; margin-top:10px; }
30
+ .suggest button { text-align:left; background:var(--card); border:1px solid var(--line); color:var(--fg); border-radius:8px; padding:8px 10px; font:inherit; cursor:pointer; }
31
+ .msg { max-width:92%; white-space:pre-wrap; overflow-wrap:anywhere; }
32
+ .msg.user { align-self:flex-end; background:var(--brand-soft); border-radius:12px 12px 2px 12px; padding:8px 12px; }
33
+ .msg.assistant { align-self:flex-start; }
34
+ .steps { display:flex; flex-direction:column; gap:4px; margin-bottom:6px; }
35
+ .step { font-size:12px; color:var(--dim); display:flex; gap:6px; align-items:center; }
36
+ .step::before { content:""; width:8px; height:8px; border-radius:50%; background:var(--dim); flex:none; }
37
+ .step.running::before { background:var(--amber); animation:pulse 1s ease-in-out infinite; }
38
+ .step.done::before { background:var(--ok); } .step.error::before { background:var(--bad); }
39
+ .err { color:var(--bad); font-size:13px; }
40
+ .typing { color:var(--dim); font-size:13px; }
41
+ @keyframes pulse { 50% { opacity:.3; } }
42
+ @media (prefers-reduced-motion: reduce) { .step.running::before { animation:none; } }
43
+
44
+ form { border-top:1px solid var(--line); padding:10px 16px 12px; background:var(--bg); }
45
+ .composer { display:flex; gap:8px; align-items:flex-end; }
46
+ textarea { flex:1; resize:none; min-height:38px; max-height:120px; font:inherit; color:var(--fg); background:var(--card); border:1px solid var(--line); border-radius:10px; padding:8px 10px; }
47
+ .send { border:1px solid var(--brand); background:var(--brand); color:#fff; border-radius:10px; padding:8px 14px; font:inherit; cursor:pointer; }
48
+ .send.stop { background:transparent; color:var(--brand); }
49
+ .opts { display:flex; justify-content:space-between; align-items:center; gap:8px; margin-top:8px; font-size:12px; color:var(--dim); flex-wrap:wrap; }
50
+ select { font:inherit; font-size:12px; color:var(--fg); background:var(--card); border:1px solid var(--line); border-radius:6px; padding:3px 6px; }
51
+ .link { background:none; border:0; color:var(--dim); font:inherit; font-size:12px; cursor:pointer; text-decoration:underline; padding:0; }
52
+
53
+ #activityView { padding:14px 16px; overflow-y:auto; }
54
+ .card { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:12px; margin-bottom:12px; }
55
+ .row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
56
+ .sub { color:var(--dim); font-size:12px; }
57
+ .btn { font:inherit; border:1px solid var(--brand); background:var(--brand); color:#fff; border-radius:8px; padding:6px 12px; cursor:pointer; }
58
+ .btn.ghost { background:transparent; color:var(--brand); }
59
+ ul { list-style:none; margin:8px 0 0; padding:0; }
60
+ li { padding:6px 0; border-top:1px solid var(--line); font-size:13px; }
61
+ li:first-child { border-top:0; }
62
+ .t { color:var(--dim); font-size:11px; margin-right:6px; font-variant-numeric:tabular-nums; }
63
+ .k { font-size:11px; text-transform:uppercase; letter-spacing:.04em; margin-right:6px; color:var(--dim); }
64
+ li.act .k { color:var(--brand); } li.error .k { color:var(--bad); }
65
+ </style>
66
+ </head>
67
+ <body>
68
+ <header>
69
+ <h1><svg class="wordmark" viewBox="0 0 287 45" role="img" aria-label="AI Draftsman"><defs><linearGradient id="adg" x1="89.6201" y1="25.9941" x2="89.6201" y2="35.9941" gradientUnits="userSpaceOnUse"><stop stop-color="#6C3FCB"/><stop offset="1" stop-color="#F5A623"/></linearGradient></defs><path d="M247.697 42.532V37.7203C244.557 41.2428 240.648 43 235.967 43C231.541 43 227.762 41.3742 224.648 38.1227C221.549 34.8547 220 30.9873 220 26.5287C220 21.9881 221.615 18.1043 224.836 14.8773C228.082 11.6258 231.967 10 236.5 10C241.057 10 244.943 11.6258 248.164 14.8773C251.385 18.1043 253 21.9881 253 26.5287V42.5238H247.697V42.532ZM247.697 26.5369C247.697 23.4742 246.598 20.8467 244.393 18.6626C242.189 16.4538 239.557 15.3536 236.5 15.3536C233.467 15.3536 230.844 16.4538 228.639 18.6626C226.434 20.8467 225.336 23.4742 225.336 26.5369C225.336 29.5997 226.434 32.2354 228.639 34.4441C230.844 36.6283 233.467 37.7203 236.5 37.7203C239.533 37.7203 242.164 36.6283 244.393 34.4441C246.598 32.2354 247.697 29.5997 247.697 26.5369Z" fill="currentColor"/><path d="M28.1639 40.0897C24.959 43.366 21.0738 45 16.5 45C11.9426 45 8.05738 43.366 4.83607 40.0897C1.61475 36.7966 0 32.8332 0 28.1994C0 23.6494 1.54918 19.6943 4.64754 16.3342C7.7623 12.9992 11.541 11.3317 15.9672 11.3317C20.623 11.3317 24.5328 13.1501 27.6967 16.7867V1H33V28.1994C33 32.8332 31.3852 36.7966 28.1639 40.0897ZM27.6967 28.1994C27.6967 25.0739 26.5984 22.3925 24.3934 20.1636C22.1885 17.9095 19.5574 16.7867 16.5 16.7867C13.4672 16.7867 10.8443 17.9095 8.63934 20.1636C6.43443 22.3925 5.33607 25.0739 5.33607 28.1994C5.33607 31.3249 6.43443 34.0147 8.63934 36.2687C10.8443 38.4976 13.4672 39.6121 16.5 39.6121C19.5328 39.6121 22.1639 38.4976 24.3934 36.2687C26.5984 34.0147 27.6967 31.3249 27.6967 28.1994Z" fill="currentColor"/><path d="M120.158 7.91147C118.544 9.59142 117.742 11.6024 117.742 13.9609V16.1374H122.916V21.4917H117.742V43H112.599V21.4834H110V16.1291H112.599V13.9527C112.599 10.2783 113.95 7.03426 116.653 4.22055C119.355 1.40685 122.471 0 126 0V5.35431C123.735 5.36259 121.795 6.21497 120.158 7.91147Z" fill="currentColor"/><path d="M208.653 42.9917V22.9934C208.653 20.9445 207.983 19.1787 206.636 17.7044C205.273 16.1802 203.62 15.4223 201.669 15.4223C199.529 15.4223 197.835 16.1302 196.579 17.5462C195.322 18.9788 194.694 20.8029 194.694 23.0018V43H189.314V22.9934C189.314 20.7945 188.686 18.9788 187.43 17.5379C186.215 16.1219 184.529 15.4139 182.364 15.4139C180.388 15.4139 178.736 16.1719 177.397 17.6961C176.058 19.1787 175.38 20.9445 175.38 22.9851V42.9833H170V22.9934C170 19.4869 171.198 16.4467 173.587 13.8647C176.017 11.291 178.942 10 182.364 10C186.364 10 189.587 11.5492 192.017 14.656C194.405 11.5492 197.628 10 201.669 10C205.107 10 208.025 11.291 210.413 13.8647C212.802 16.4467 214 19.4869 214 22.9934V42.9917H208.653Z" fill="currentColor"/><path d="M281.626 42.9917V24.0762C281.626 21.6858 280.771 19.6535 279.071 17.9626C277.387 16.2718 275.364 15.4306 273.008 15.4306C270.628 15.4306 268.596 16.2802 266.921 17.9626C265.237 19.6535 264.399 21.6941 264.399 24.0762V43H259V24.0762C259 20.3614 260.41 17.0798 263.23 14.2479C266.05 11.416 269.309 10 273.017 10C276.699 10 279.95 11.416 282.77 14.2479C285.59 17.0798 287 20.3531 287 24.0762V43H281.626V42.9917Z" fill="currentColor"/><path d="M46.9112 17.9563C45.2296 19.6476 44.3929 21.6887 44.3929 24.0714V43H39V24.0798C39 20.364 40.4083 17.0815 43.2249 14.2489C46.0414 11.4163 49.297 10 53 10V15.4236C50.6225 15.4236 48.5929 16.2651 46.9112 17.9563Z" fill="currentColor"/><path d="M165 33.785C165 36.3474 164.055 38.5239 162.165 40.3143C160.275 42.1048 158.034 43 155.45 43C153.242 43 151.268 42.3347 149.529 40.996C147.765 39.6409 146.652 37.9326 146.184 35.8711L151.402 34.4667C151.486 34.943 151.653 35.3865 151.912 35.7807C152.172 36.175 152.473 36.5199 152.832 36.7992C153.192 37.0784 153.593 37.3001 154.045 37.4726C154.488 37.6369 154.956 37.7272 155.45 37.7272C156.596 37.7272 157.574 37.3412 158.377 36.5692C159.205 35.7972 159.614 34.8691 159.614 33.785C159.614 32.0767 158.435 30.6887 156.085 29.6292C154.028 28.6929 152.456 27.9701 151.36 27.4691C150.265 26.9681 149.654 26.6807 149.529 26.5986C147.171 24.9559 146 22.5495 146 19.3793C146 16.8168 146.937 14.6157 148.802 12.7842C150.691 10.9281 152.941 10 155.55 10C157.691 10 159.631 10.6735 161.371 12.0286C163.11 13.3591 164.222 15.0674 164.716 17.1536L159.464 18.5251C158.979 16.398 157.674 15.3385 155.55 15.3385C154.446 15.3385 153.468 15.7327 152.623 16.5294C151.82 17.3178 151.419 18.2705 151.419 19.3711C151.419 21.063 152.598 22.4345 154.948 23.494C156.989 24.4303 158.544 25.1531 159.623 25.6705C160.702 26.1797 161.32 26.4918 161.471 26.5903C163.829 28.2658 165 30.6558 165 33.785Z" fill="currentColor"/><path d="M137.322 16.1271V43H131.643V16.1271H126V10.7376H131.643V0H137.322V10.7459H143V16.1353H137.322V16.1271Z" fill="currentColor"/><path d="M102.189 14.2087C98.9044 10.899 94.9427 9.24414 90.2958 9.24414H69.9444C65.3226 9.24414 61.3609 10.899 58.0512 14.2087C54.7666 17.4933 53.1201 21.4466 53.1201 26.0685C53.1201 30.6068 54.6997 34.5433 57.859 37.8697C61.035 41.1794 64.888 42.8343 69.4012 42.8343C69.5851 42.8343 69.7606 42.8176 69.9444 42.8092H90.2958C94.8257 42.6755 98.6369 40.9036 101.713 37.4602V42.8982H107.12V26.0685C107.12 21.4466 105.482 17.4933 102.189 14.2087ZM98.3444 34.1171C96.0794 36.3403 93.3965 37.4518 90.2958 37.4518H69.9444C66.852 37.4518 64.1775 36.3403 61.9293 34.1171C59.681 31.8688 58.5611 29.1859 58.5611 26.0685C58.5611 22.951 59.6894 20.2765 61.9293 18.0533C64.1775 15.805 66.852 14.6851 69.9444 14.6851H90.2958C93.4133 14.6851 96.0961 15.8134 98.3444 18.0533C100.593 20.2765 101.713 22.951 101.713 26.0685C101.713 29.1859 100.593 31.8688 98.3444 34.1171Z" fill="currentColor"/><path d="M101.695 26.0357C101.695 29.1532 100.576 31.8361 98.3273 34.0843C96.0539 36.3075 93.3794 37.4191 90.2787 37.4191H69.9273C68.3811 37.4191 66.9352 37.1433 65.598 36.5833C64.2691 36.0317 63.0321 35.1959 61.9122 34.0843C59.6639 31.8361 58.5439 29.1532 58.5439 26.0357C58.5439 22.9182 59.6639 20.2437 61.9122 18.0206C64.1604 15.7723 66.8266 14.6523 69.9273 14.6523H90.2787C91.8332 14.6523 93.2875 14.9365 94.6247 15.4965C95.9703 16.0565 97.1989 16.8922 98.3273 18.0206C100.576 20.2437 101.695 22.9182 101.695 26.0357Z" fill="white"/><path d="M89.4551 35.9941C88.1093 35.9941 86.8567 35.7532 85.7225 35.2712C84.5715 34.7893 83.4965 34.0406 82.5317 33.0682C80.5765 31.0716 79.6201 28.7566 79.6201 25.9941C79.6201 23.2317 80.5681 20.9339 82.5317 18.9632C84.4953 16.9666 86.7636 15.9941 89.4551 15.9941H89.7513C91.0886 15.9941 92.3497 16.2437 93.5008 16.7256C94.6688 17.2248 95.7183 17.9477 96.7086 18.9546C98.6722 20.9253 99.6201 23.2317 99.6201 25.9941C99.6201 28.7566 98.6637 31.0716 96.7086 33.0682C94.728 35.0389 92.4513 35.9941 89.7513 35.9941H89.4551Z" fill="url(#adg)"/><circle cx="86" cy="24" r="3" fill="#fff"/><circle cx="93.2402" cy="24" r="3" fill="#fff"/></svg><span class="conn"><span id="dot" class="dot"></span><span id="status">Connecting…</span></span></h1>
70
+ <div class="tabs" role="tablist">
71
+ <button class="tab" role="tab" id="tabChat" aria-selected="true">Chat</button>
72
+ <button class="tab" role="tab" id="tabActivity" aria-selected="false">Activity</button>
73
+ </div>
74
+ </header>
75
+
76
+ <main>
77
+ <section id="chatView">
78
+ <div id="thread" aria-live="polite"></div>
79
+ <form id="form">
80
+ <div class="composer">
81
+ <textarea id="input" rows="1" placeholder="Ask AI Draftsman about this page…" aria-label="Message"></textarea>
82
+ <button type="submit" id="send" class="send">Send</button>
83
+ </div>
84
+ <div class="opts">
85
+ <label>Allow: <select id="mode"><option value="read">Read pages only</option><option value="act">Read and act</option></select></label>
86
+ <button type="button" class="link" id="clear">New chat</button>
87
+ </div>
88
+ </form>
89
+ </section>
90
+
91
+ <section id="activityView" hidden>
92
+ <div class="card">
93
+ <p class="sub" id="hint" style="margin:0 0 10px"></p>
94
+ <div class="row">
95
+ <button id="stop" class="btn">Stop AI Draftsman</button>
96
+ <button id="reconnect" class="btn ghost">Reconnect</button>
97
+ </div>
98
+ </div>
99
+ <div class="card"><strong>Activity</strong><ul id="log"></ul></div>
100
+ </section>
101
+ </main>
102
+ <script src="sidepanel.js"></script>
103
+ </body>
104
+ </html>
@@ -0,0 +1,157 @@
1
+ const $ = (id) => document.getElementById(id);
2
+
3
+ const SUGGESTIONS = ['What is on this page?', 'List my open tabs', 'Fill in the quote form with name Sam and quantity 4'];
4
+ let lastState = {};
5
+ let modeTouched = false;let lastChat = { messages: [], running: false, mode: 'read' };
6
+
7
+ // ---- tabs --------------------------------------------------------------------------------------------------------
8
+ function showTab(name) {
9
+ const chat = name === 'chat';
10
+ $('chatView').hidden = !chat;
11
+ $('activityView').hidden = chat;
12
+ $('tabChat').setAttribute('aria-selected', String(chat));
13
+ $('tabActivity').setAttribute('aria-selected', String(!chat));
14
+ }
15
+ $('tabChat').addEventListener('click', () => showTab('chat'));
16
+ $('tabActivity').addEventListener('click', () => showTab('activity'));
17
+
18
+ // ---- status + activity -------------------------------------------------------------------------------------------
19
+ function renderState(state, entries) {
20
+ const { connected = false, paused = false } = state || {};
21
+ $('dot').className = 'dot' + (connected && !paused ? ' on' : '');
22
+ $('status').textContent = paused ? 'Stopped by you' : connected ? 'Connected' : 'Not connected';
23
+ $('hint').textContent = paused
24
+ ? 'Every browser request is refused until you resume.'
25
+ : connected
26
+ ? 'The CLI, desktop app and this chat can use this browser. Stop blocks all of them.'
27
+ : 'Run `ad browser install`, then reload this extension.';
28
+ $('stop').textContent = paused ? 'Resume' : 'Stop AI Draftsman';
29
+ const list = $('log');
30
+ list.textContent = '';
31
+ if (!entries || entries.length === 0) {
32
+ const li = document.createElement('li');
33
+ li.className = 'sub';
34
+ li.textContent = 'Nothing yet.';
35
+ list.append(li);
36
+ return;
37
+ }
38
+ for (const e of entries.slice().reverse()) {
39
+ const li = document.createElement('li');
40
+ li.className = e.kind;
41
+ const t = document.createElement('span');
42
+ t.className = 't';
43
+ t.textContent = new Date(e.t).toLocaleTimeString();
44
+ const k = document.createElement('span');
45
+ k.className = 'k';
46
+ k.textContent = e.kind === 'act' ? 'did' : e.kind;
47
+ li.append(t, k, document.createTextNode(e.text));
48
+ list.append(li);
49
+ }
50
+ }
51
+
52
+ // ---- chat ---------------------------------------------------------------------------------------------------------
53
+ function renderChat(chat) {
54
+ lastChat = chat;
55
+ const thread = $('thread');
56
+ const nearBottom = thread.scrollHeight - thread.scrollTop - thread.clientHeight < 80;
57
+ thread.textContent = '';
58
+ if (chat.messages.length === 0) {
59
+ const empty = document.createElement('div');
60
+ empty.className = 'empty';
61
+ empty.textContent = 'Ask about the page you are on, or ask AI Draftsman to do something in your browser.';
62
+ const box = document.createElement('div');
63
+ box.className = 'suggest';
64
+ for (const s of SUGGESTIONS) {
65
+ const b = document.createElement('button');
66
+ b.type = 'button';
67
+ b.textContent = s;
68
+ b.addEventListener('click', () => send(s));
69
+ box.append(b);
70
+ }
71
+ thread.append(empty, box);
72
+ }
73
+ chat.messages.forEach((m, i) => {
74
+ const el = document.createElement('div');
75
+ el.className = `msg ${m.role}`;
76
+ if (m.role === 'user') {
77
+ el.textContent = m.text;
78
+ } else {
79
+ if (m.steps?.length) {
80
+ const steps = document.createElement('div');
81
+ steps.className = 'steps';
82
+ for (const s of m.steps) {
83
+ const row = document.createElement('div');
84
+ row.className = `step ${s.state}`;
85
+ row.textContent = s.label;
86
+ steps.append(row);
87
+ }
88
+ el.append(steps);
89
+ }
90
+ if (m.text) el.append(document.createTextNode(m.text));
91
+ if (m.error) {
92
+ const err = document.createElement('div');
93
+ err.className = 'err';
94
+ err.textContent = m.error;
95
+ el.append(err);
96
+ }
97
+ if (!m.text && !m.error && chat.running && i === chat.messages.length - 1) {
98
+ const typing = document.createElement('div');
99
+ typing.className = 'typing';
100
+ typing.textContent = 'Working…';
101
+ el.append(typing);
102
+ }
103
+ }
104
+ thread.append(el);
105
+ });
106
+ if (!modeTouched) $('mode').value = chat.mode || 'read';
107
+ const sendBtn = $('send');
108
+ sendBtn.textContent = chat.running ? 'Stop' : 'Send';
109
+ sendBtn.className = chat.running ? 'send stop' : 'send';
110
+ if (nearBottom || chat.running) thread.scrollTop = thread.scrollHeight;
111
+ }
112
+
113
+ function send(text) {
114
+ const value = String(text || '').trim();
115
+ if (!value || lastChat.running) return;
116
+ void chrome.runtime.sendMessage({ type: 'chat_send', text: value, mode: $('mode').value });
117
+ $('input').value = '';
118
+ autosize();
119
+ }
120
+
121
+ $('form').addEventListener('submit', (e) => {
122
+ e.preventDefault();
123
+ if (lastChat.running) void chrome.runtime.sendMessage({ type: 'chat_stop' });
124
+ else send($('input').value);
125
+ });
126
+ $('input').addEventListener('keydown', (e) => {
127
+ if (e.key === 'Enter' && !e.shiftKey) {
128
+ e.preventDefault();
129
+ $('form').requestSubmit();
130
+ }
131
+ });
132
+ function autosize() {
133
+ const el = $('input');
134
+ el.style.height = 'auto';
135
+ el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
136
+ }
137
+ $('input').addEventListener('input', autosize);
138
+ $('mode').addEventListener('change', () => (modeTouched = true));
139
+ $('clear').addEventListener('click', () => chrome.runtime.sendMessage({ type: 'chat_clear' }));
140
+
141
+ // ---- controls + sync ---------------------------------------------------------------------------------------------
142
+ $('stop').addEventListener('click', async () => {
143
+ const { state } = await chrome.storage.session.get('state');
144
+ await chrome.runtime.sendMessage({ type: 'pause', value: !state?.paused });
145
+ });
146
+ $('reconnect').addEventListener('click', () => chrome.runtime.sendMessage({ type: 'reconnect' }));
147
+
148
+ async function refresh() {
149
+ const { state, log, chat } = await chrome.storage.session.get(['state', 'log', 'chat']);
150
+ lastState = state || {};
151
+ renderState(state, log);
152
+ renderChat(chat || { messages: [], running: false, mode: 'read' });
153
+ }
154
+ chrome.storage.onChanged.addListener((_c, area) => {
155
+ if (area === 'session') void refresh();
156
+ });
157
+ void refresh();
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@consultancy-outfit/ai-draftsman-cli",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "AI Draftsman standalone CLI",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
8
+ "extension",
8
9
  "README.md"
9
10
  ],
10
11
  "bin": {