agenter-app 1.1.0 → 1.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.
- package/README.md +166 -130
- package/cli.js +43 -0
- package/lib/memory.js +101 -0
- package/lib/terminal.js +127 -0
- package/lib/update.js +69 -0
- package/package.json +1 -1
- package/public/app.js +335 -13
- package/public/index.html +83 -9
- package/public/style.css +106 -10
- package/scripts/pick-folder.ps1 +85 -0
- package/server.js +142 -9
package/public/style.css
CHANGED
|
@@ -72,6 +72,15 @@
|
|
|
72
72
|
:root[data-accent="teal"] { --accent: #73cfcb; --accent-deep: #1d7a76; }
|
|
73
73
|
:root[data-accent="rose"] { --accent: #e58aa6; --accent-deep: #ad3a61; }
|
|
74
74
|
:root[data-accent="lavender"] { --accent: #b8acf2; --accent-deep: #6450c4; }
|
|
75
|
+
:root[data-accent="mint"] { --accent: #8fe0b4; --accent-deep: #1b7049; }
|
|
76
|
+
:root[data-accent="sky"] { --accent: #8cc4ef; --accent-deep: #27689f; }
|
|
77
|
+
:root[data-accent="indigo"] { --accent: #a0aef5; --accent-deep: #3f51bf; }
|
|
78
|
+
:root[data-accent="plum"] { --accent: #d49ae0; --accent-deep: #8b3a9b; }
|
|
79
|
+
:root[data-accent="magenta"] { --accent: #f08fd0; --accent-deep: #a82a83; }
|
|
80
|
+
:root[data-accent="coral"] { --accent: #f29a8b; --accent-deep: #b03f2b; }
|
|
81
|
+
:root[data-accent="amber"] { --accent: #eec15f; --accent-deep: #855b05; }
|
|
82
|
+
:root[data-accent="lemon"] { --accent: #e3d96b; --accent-deep: #6d640b; }
|
|
83
|
+
:root[data-accent="slate"] { --accent: #a9b8c6; --accent-deep: #4b5f72; }
|
|
75
84
|
|
|
76
85
|
* { box-sizing: border-box; }
|
|
77
86
|
html, body { height: 100%; margin: 0; }
|
|
@@ -101,7 +110,9 @@ kbd {
|
|
|
101
110
|
width: 300px; flex: none; display: flex; flex-direction: column; gap: 6px;
|
|
102
111
|
background: var(--sidebar); border-right: 1px solid var(--border); padding: 14px 10px 10px; min-height: 0;
|
|
103
112
|
}
|
|
104
|
-
.brand-row { display: flex; align-items: center;
|
|
113
|
+
.brand-row { display: flex; align-items: center; gap: 8px; padding: 0 6px 8px; }
|
|
114
|
+
.brand-row .brand { margin-right: auto; }
|
|
115
|
+
.nav-min-btn { padding: 4px; color: var(--muted); }
|
|
105
116
|
.brand { display: flex; align-items: center; gap: 9px; font-weight: 700; font-size: 15px; letter-spacing: -.01em; }
|
|
106
117
|
.logo { width: 22px; height: 22px; border-radius: 7px; background: var(--accent); position: relative; }
|
|
107
118
|
.logo::after { content: ""; position: absolute; inset: 6px; background: var(--accent-text); transform: rotate(45deg); border-radius: 2px; }
|
|
@@ -115,6 +126,7 @@ kbd {
|
|
|
115
126
|
}
|
|
116
127
|
.nav-item:hover { background: var(--hover); }
|
|
117
128
|
.nav-item.active { background: var(--panel); box-shadow: var(--shadow-sm); }
|
|
129
|
+
.nav-item:disabled { opacity: .6; cursor: progress; }
|
|
118
130
|
.count { margin-left: auto; min-width: 20px; padding: 0 6px; border-radius: 99px; background: var(--warn); color: #fff; font-size: 11.5px; font-weight: 700; text-align: center; line-height: 19px; }
|
|
119
131
|
|
|
120
132
|
.search { position: relative; display: flex; align-items: center; margin: 4px 0 2px; }
|
|
@@ -236,7 +248,8 @@ kbd {
|
|
|
236
248
|
.segmented button:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 1px; }
|
|
237
249
|
.segmented .ic { width: 14px; height: 14px; }
|
|
238
250
|
.accent-row { display: flex; align-items: center; gap: 7px; font-size: 13px; color: var(--text-2); }
|
|
239
|
-
.accent-row span { margin-right: auto; }
|
|
251
|
+
.accent-row > span { margin-right: auto; }
|
|
252
|
+
.swatches { display: grid; grid-template-columns: repeat(7, 16px); gap: 7px; }
|
|
240
253
|
.accent-row button {
|
|
241
254
|
width: 16px; height: 16px; border-radius: 4px; border: 0; padding: 0; cursor: pointer;
|
|
242
255
|
background: var(--sw); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .15);
|
|
@@ -276,16 +289,17 @@ kbd {
|
|
|
276
289
|
.btn.primary kbd { background: transparent; color: inherit; border-color: currentColor; opacity: .7; }
|
|
277
290
|
|
|
278
291
|
/* ---------- main ---------- */
|
|
279
|
-
|
|
292
|
+
/* Measured as a container: what matters is the room left beside the sidebar, not the window. */
|
|
293
|
+
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; container: main / inline-size; }
|
|
280
294
|
.main-head {
|
|
281
|
-
display: flex; align-items: center; gap: 12px; padding: 12px 24px; min-height: 64px;
|
|
295
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 12px 24px; min-height: 64px;
|
|
282
296
|
border-bottom: 1px solid var(--border); background: var(--panel);
|
|
283
297
|
}
|
|
284
|
-
.head-text { min-width: 0; flex: 1; }
|
|
298
|
+
.head-text { min-width: 0; flex: 1 1 180px; }
|
|
285
299
|
.head-title { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 16px; letter-spacing: -.01em; }
|
|
286
300
|
.head-title span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
287
301
|
.head-sub { font-size: 12.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
288
|
-
.head-actions { display: flex; align-items: center; gap: 8px;
|
|
302
|
+
.head-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; }
|
|
289
303
|
.menu-btn { display: none; }
|
|
290
304
|
.path-chip { cursor: pointer; border: 0; background: none; padding: 0; color: var(--muted); font-size: 12px; }
|
|
291
305
|
.path-chip:hover { color: var(--text); text-decoration: underline; }
|
|
@@ -318,6 +332,59 @@ dialog.new-dialog { max-width: 520px; }
|
|
|
318
332
|
.new-dialog .dlg-error { margin-top: 12px; font-size: 13px; color: var(--danger); }
|
|
319
333
|
.new-dialog .dlg-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
|
|
320
334
|
|
|
335
|
+
.new-dialog .folder-row { display: flex; gap: 8px; }
|
|
336
|
+
.new-dialog .folder-row .field { flex: 1; min-width: 0; }
|
|
337
|
+
.new-carry { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding: 7px 10px; border-radius: 8px; background: var(--accent-soft); font-size: 12.5px; }
|
|
338
|
+
.new-carry[hidden] { display: none; }
|
|
339
|
+
.new-carry span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
340
|
+
|
|
341
|
+
/* ---------- update bar ---------- */
|
|
342
|
+
.update-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; padding: 8px 24px; background: var(--accent-soft); border-bottom: 1px solid var(--border); font-size: 13px; }
|
|
343
|
+
.update-bar[hidden] { display: none; }
|
|
344
|
+
.update-bar .update-text { flex: 1 1 240px; min-width: 0; }
|
|
345
|
+
.update-bar .update-text code { font-size: 12px; }
|
|
346
|
+
|
|
347
|
+
/* ---------- more menu ---------- */
|
|
348
|
+
.more-menu { position: fixed; inset: auto; margin: 0; min-width: 200px; padding: 4px; border: 1px solid var(--border); border-radius: 10px; background: var(--panel); color: var(--text); box-shadow: var(--shadow); }
|
|
349
|
+
.more-menu button { display: flex; align-items: center; gap: 10px; width: 100%; padding: 7px 10px; border: 0; border-radius: 7px; background: none; cursor: pointer; font-size: 13px; text-align: left; }
|
|
350
|
+
.more-menu button:hover, .more-menu button:focus-visible { background: var(--hover); outline: none; }
|
|
351
|
+
.more-menu .ic { color: var(--muted); }
|
|
352
|
+
|
|
353
|
+
/* ---------- terminal ---------- */
|
|
354
|
+
.term { flex: none; height: min(36vh, 360px); min-height: 160px; display: flex; flex-direction: column; border-top: 1px solid var(--border); background: var(--code-bg); }
|
|
355
|
+
.term[hidden] { display: none; }
|
|
356
|
+
.term-bar { display: flex; align-items: center; gap: 6px; padding: 4px 8px 4px 14px; border-bottom: 1px solid var(--border); background: var(--panel); }
|
|
357
|
+
.term-title { font-size: 12px; font-weight: 600; flex: none; }
|
|
358
|
+
.term-cwd { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-size: 12px; }
|
|
359
|
+
.term-bar .btn.icon { padding: 4px; }
|
|
360
|
+
.term-out { flex: 1; min-height: 0; overflow: auto; padding: 8px 14px; font-size: 12.5px; line-height: 1.45; outline: none; }
|
|
361
|
+
.term-line { display: flex; align-items: center; gap: 8px; padding: 6px 14px; border-top: 1px solid var(--border); }
|
|
362
|
+
.term-prompt { color: var(--accent-ink); flex: none; }
|
|
363
|
+
.term-input { flex: 1; min-width: 0; border: 0; outline: none; background: none; color: var(--text); padding: 0; }
|
|
364
|
+
.term-input::placeholder { color: var(--muted); }
|
|
365
|
+
|
|
366
|
+
/* ---------- memory ---------- */
|
|
367
|
+
dialog.mem-dialog { max-width: 860px; }
|
|
368
|
+
.mem-dialog[open] { display: flex; flex-direction: column; height: min(640px, calc(100vh - 48px)); }
|
|
369
|
+
.mem-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; min-width: 0; }
|
|
370
|
+
.mem-head h2 { margin: 0; flex: none; }
|
|
371
|
+
.mem-head span { color: var(--muted); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
372
|
+
.mem-body { flex: 1; min-height: 0; display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 14px; }
|
|
373
|
+
.mem-list { overflow-y: auto; }
|
|
374
|
+
.mem-group { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding: 10px 8px 4px; }
|
|
375
|
+
.mem-group:first-child { padding-top: 0; }
|
|
376
|
+
.mem-file { display: flex; align-items: center; gap: 6px; width: 100%; padding: 5px 8px; border: 0; border-radius: 7px; background: none; cursor: pointer; font-size: 13px; text-align: left; }
|
|
377
|
+
.mem-file span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
378
|
+
.mem-file:hover { background: var(--hover); }
|
|
379
|
+
.mem-file[aria-current="true"] { background: var(--accent-soft); }
|
|
380
|
+
.mem-file .new { margin-left: auto; flex: none; color: var(--muted); font-size: 11px; }
|
|
381
|
+
.mem-file .dirty { margin-left: auto; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--warn); }
|
|
382
|
+
.mem-edit { display: flex; flex-direction: column; gap: 8px; min-height: 0; min-width: 0; }
|
|
383
|
+
.mem-path { font-size: 11.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
384
|
+
.mem-edit textarea { flex: 1; min-height: 0; resize: none; line-height: 1.5; }
|
|
385
|
+
.mem-dialog .dlg-error { font-size: 13px; color: var(--danger); }
|
|
386
|
+
.mem-dialog .dlg-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
|
|
387
|
+
|
|
321
388
|
/* ---------- steering ---------- */
|
|
322
389
|
.kind-steer .reply-row .hint { white-space: normal; }
|
|
323
390
|
.link { border: 0; background: none; padding: 0; font: inherit; color: var(--accent-ink); text-decoration: underline; cursor: pointer; }
|
|
@@ -363,6 +430,9 @@ dialog.new-dialog { max-width: 520px; }
|
|
|
363
430
|
.convo { display: flex; flex-direction: column; gap: 10px; }
|
|
364
431
|
.msg-user { align-self: flex-end; max-width: 78%; background: var(--accent-soft); border: 1px solid transparent; padding: 9px 14px; border-radius: 14px 14px 4px 14px; white-space: pre-wrap; word-break: break-word; }
|
|
365
432
|
.msg-user.from-dash { border-color: var(--accent-ink); }
|
|
433
|
+
.carried { margin-top: 6px; font-size: 12.5px; color: var(--text-2); white-space: normal; }
|
|
434
|
+
.carried > summary { cursor: pointer; }
|
|
435
|
+
.carried pre { margin-top: 6px; max-height: 280px; overflow: auto; font-size: 12px; }
|
|
366
436
|
.live-echo { display: flex; justify-content: flex-end; margin-top: 12px; }
|
|
367
437
|
.msg-claude { display: grid; grid-template-columns: 26px 1fr; gap: 10px; align-items: start; }
|
|
368
438
|
.claude-mark { width: 26px; height: 26px; border-radius: 8px; background: var(--panel); border: 1px solid var(--border); display: grid; place-items: center; color: var(--accent-ink); }
|
|
@@ -495,7 +565,7 @@ dialog.new-dialog { max-width: 520px; }
|
|
|
495
565
|
.section-title { display: flex; align-items: baseline; gap: 8px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin: 26px 0 10px; }
|
|
496
566
|
#inboxPending > .section-title { margin-top: 0; }
|
|
497
567
|
.all-clear { display: flex; align-items: center; gap: 14px; padding: 18px 20px; border-radius: var(--radius); background: var(--panel); border: 1px solid var(--border); }
|
|
498
|
-
.all-clear .big { width: 38px; height: 38px; border-radius: 50%; background: var(--
|
|
568
|
+
.all-clear .big { width: 38px; height: 38px; border-radius: 50%; background: var(--accent-soft); color: var(--accent-ink); display: grid; place-items: center; flex: none; }
|
|
499
569
|
.all-clear b { display: block; }
|
|
500
570
|
.all-clear span { color: var(--muted); font-size: 13px; }
|
|
501
571
|
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 10px; }
|
|
@@ -538,23 +608,49 @@ dialog form { text-align: right; }
|
|
|
538
608
|
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
539
609
|
}
|
|
540
610
|
|
|
611
|
+
/* ---------- collapsed sidebar: a rail of icons and sessions ---------- */
|
|
612
|
+
@media (min-width: 801px) {
|
|
613
|
+
.app.nav-min .sidebar { width: 60px; padding: 14px 8px 10px; align-items: stretch; }
|
|
614
|
+
.app.nav-min .brand-row { flex-direction: column; padding: 0 0 8px; }
|
|
615
|
+
.app.nav-min .brand { margin: 0; }
|
|
616
|
+
.app.nav-min .brand-name, .app.nav-min .search, .app.nav-min .usage, .app.nav-min .sidebar-foot, .app.nav-min .group-label { display: none; }
|
|
617
|
+
.app.nav-min .nav-item { justify-content: center; padding: 8px 0; position: relative; }
|
|
618
|
+
.app.nav-min .nav-item > span:not(.count) { display: none; }
|
|
619
|
+
.app.nav-min .nav-item .count { position: absolute; top: 0; right: 0; min-width: 16px; padding: 0 4px; line-height: 15px; font-size: 10px; }
|
|
620
|
+
.app.nav-min .session-list { margin: 0; padding: 0; }
|
|
621
|
+
.app.nav-min .s-item { grid-template-columns: 32px; justify-content: center; padding: 6px 0; }
|
|
622
|
+
.app.nav-min .s-main, .app.nav-min .s-when { display: none; }
|
|
623
|
+
}
|
|
624
|
+
|
|
541
625
|
/* ---------- small screens ---------- */
|
|
542
626
|
@media (max-width: 800px) {
|
|
543
627
|
.sidebar { position: fixed; inset: 0 auto 0 0; z-index: 40; transform: translateX(-100%); transition: transform .2s; box-shadow: var(--shadow); width: min(300px, 86vw); }
|
|
544
628
|
.app.nav-open .sidebar { transform: none; }
|
|
545
629
|
.app.nav-open .scrim { display: block; position: fixed; inset: 0; background: rgba(0, 0, 0, .35); z-index: 30; }
|
|
546
630
|
.menu-btn { display: inline-flex; }
|
|
631
|
+
.search kbd { display: none; }
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/* Going by the space beside the sidebar, so the same room lays out the same way whether
|
|
635
|
+
the sidebar is open, collapsed or a drawer. */
|
|
636
|
+
@container main (max-width: 900px) {
|
|
637
|
+
.head-actions .label { display: none; }
|
|
638
|
+
}
|
|
639
|
+
@container main (max-width: 760px) {
|
|
547
640
|
.main-head { padding: 10px 16px; }
|
|
641
|
+
.update-bar { padding: 8px 16px; }
|
|
548
642
|
.content { padding: 16px; }
|
|
549
643
|
.dock { padding: 0 16px 14px; }
|
|
550
|
-
.head-actions .label { display: none; }
|
|
551
644
|
.msg-user { max-width: 92%; }
|
|
552
|
-
.btn kbd, .reply-row .hint
|
|
645
|
+
.btn kbd, .reply-row .hint { display: none; }
|
|
553
646
|
.tools { margin-left: 8px; }
|
|
554
647
|
/* No room side by side: the app goes on top, the conversation below. */
|
|
555
648
|
.split { flex-direction: column; }
|
|
556
649
|
.splitter { display: none; }
|
|
557
|
-
.preview { order: -1; width: auto; max-width: none; min-width: 0; height: 48vh; border-bottom: 1px solid var(--border); }
|
|
650
|
+
.preview { order: -1; width: auto; max-width: none; min-width: 0; height: 48vh; flex: none; border-bottom: 1px solid var(--border); }
|
|
651
|
+
}
|
|
652
|
+
@media (max-width: 640px) {
|
|
653
|
+
.mem-body { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 2fr) minmax(0, 5fr); }
|
|
558
654
|
}
|
|
559
655
|
|
|
560
656
|
.auth-lost { position: fixed; top: 12px; left: 50%; transform: translateX(-50%); z-index: 1000; max-width: calc(100vw - 32px); padding: 10px 14px; border-radius: 10px; background: #5c2a26; color: #fff; font-size: 13px; }
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# The Windows folder window behind New session. Prints the folder you chose, or nothing if
|
|
2
|
+
# you cancel.
|
|
3
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
4
|
+
|
|
5
|
+
# Windows won't let a background process take focus, so without this the window could open
|
|
6
|
+
# behind the dashboard and clicking New session would seem to do nothing. Joining the input
|
|
7
|
+
# of the window that has focus is what makes SetForegroundWindow allowed.
|
|
8
|
+
Add-Type @"
|
|
9
|
+
using System;
|
|
10
|
+
using System.Text;
|
|
11
|
+
using System.Runtime.InteropServices;
|
|
12
|
+
public static class PickFolderWindow {
|
|
13
|
+
[DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();
|
|
14
|
+
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, IntPtr p);
|
|
15
|
+
[DllImport("kernel32.dll")] static extern uint GetCurrentThreadId();
|
|
16
|
+
[DllImport("user32.dll")] static extern bool AttachThreadInput(uint a, uint b, bool attach);
|
|
17
|
+
[DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr h);
|
|
18
|
+
[DllImport("user32.dll")] static extern bool BringWindowToTop(IntPtr h);
|
|
19
|
+
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
|
|
20
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetClassName(IntPtr h, StringBuilder name, int size);
|
|
21
|
+
delegate bool WindowVisitor(IntPtr h, IntPtr data);
|
|
22
|
+
[DllImport("user32.dll")] static extern bool EnumThreadWindows(uint thread, WindowVisitor visit, IntPtr data);
|
|
23
|
+
[DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
|
|
24
|
+
// Dialogs are the #32770 class. Runs on the dialog's thread, so its windows are ours.
|
|
25
|
+
static IntPtr FindDialog(uint thread) {
|
|
26
|
+
IntPtr found = IntPtr.Zero;
|
|
27
|
+
EnumThreadWindows(thread, (h, data) => {
|
|
28
|
+
StringBuilder name = new StringBuilder(16);
|
|
29
|
+
GetClassName(h, name, 16);
|
|
30
|
+
if (name.ToString() != "#32770" || !IsWindowVisible(h)) return true;
|
|
31
|
+
found = h;
|
|
32
|
+
return false;
|
|
33
|
+
}, IntPtr.Zero);
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
// True once the dialog really has focus.
|
|
37
|
+
public static bool BringForward() {
|
|
38
|
+
uint self = GetCurrentThreadId();
|
|
39
|
+
IntPtr dialog = FindDialog(self);
|
|
40
|
+
if (dialog == IntPtr.Zero) return false; // not up yet
|
|
41
|
+
if (GetForegroundWindow() == dialog) return true;
|
|
42
|
+
SetWindowPos(dialog, new IntPtr(-1), 0, 0, 0, 0, 0x0043); // topmost, no move or size, show
|
|
43
|
+
uint focused = GetWindowThreadProcessId(GetForegroundWindow(), IntPtr.Zero);
|
|
44
|
+
bool joined = focused != 0 && focused != self && AttachThreadInput(self, focused, true);
|
|
45
|
+
BringWindowToTop(dialog);
|
|
46
|
+
SetForegroundWindow(dialog);
|
|
47
|
+
if (joined) AttachThreadInput(self, focused, false);
|
|
48
|
+
return GetForegroundWindow() == dialog;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
"@
|
|
52
|
+
|
|
53
|
+
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
|
|
54
|
+
|
|
55
|
+
$owner = New-Object System.Windows.Forms.Form
|
|
56
|
+
$owner.TopMost = $true
|
|
57
|
+
# Windows can still refuse focus, and this keeps the window findable when it does.
|
|
58
|
+
$owner.ShowInTaskbar = $true
|
|
59
|
+
$owner.Text = 'Agenter: choose a folder'
|
|
60
|
+
$owner.FormBorderStyle = 'None'
|
|
61
|
+
$owner.Opacity = 0
|
|
62
|
+
$owner.StartPosition = 'Manual'
|
|
63
|
+
$owner.Location = New-Object System.Drawing.Point(-32000, -32000)
|
|
64
|
+
$owner.Show()
|
|
65
|
+
|
|
66
|
+
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
67
|
+
$dialog.Description = 'Choose the project folder for this session'
|
|
68
|
+
$dialog.ShowNewFolderButton = $true
|
|
69
|
+
if ($env:AGENTER_PICK_START -and (Test-Path -LiteralPath $env:AGENTER_PICK_START)) { $dialog.SelectedPath = $env:AGENTER_PICK_START }
|
|
70
|
+
|
|
71
|
+
# Runs inside the dialog's own message loop. Keeps trying for a few seconds, then lets you be.
|
|
72
|
+
$raise = New-Object System.Windows.Forms.Timer
|
|
73
|
+
$raise.Interval = 50
|
|
74
|
+
$script:tries = 0
|
|
75
|
+
$raise.add_Tick({
|
|
76
|
+
$script:tries++
|
|
77
|
+
if ([PickFolderWindow]::BringForward() -or $script:tries -ge 60) { $raise.Stop() }
|
|
78
|
+
})
|
|
79
|
+
$raise.Start()
|
|
80
|
+
|
|
81
|
+
if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
82
|
+
[Console]::Out.Write($dialog.SelectedPath)
|
|
83
|
+
}
|
|
84
|
+
$raise.Dispose()
|
|
85
|
+
$owner.Dispose()
|
package/server.js
CHANGED
|
@@ -13,6 +13,10 @@ const { truncate, describeTool, readTail, readHead, parseCodexItem, readTranscri
|
|
|
13
13
|
const { normalizeLimits, codexWindowLabel } = require('./lib/limits');
|
|
14
14
|
const { buildDiffs } = require('./lib/diff');
|
|
15
15
|
const { getToken, sameToken } = require('./lib/token');
|
|
16
|
+
const { version: VERSION } = require('./package.json');
|
|
17
|
+
const updates = require('./lib/update');
|
|
18
|
+
const { createTerminals } = require('./lib/terminal');
|
|
19
|
+
const { readMemory, saveMemory, carriedContext } = require('./lib/memory');
|
|
16
20
|
|
|
17
21
|
const PORT = Number(process.env.AGENTER_PORT) || 4317;
|
|
18
22
|
const HOST = '127.0.0.1';
|
|
@@ -36,6 +40,9 @@ const DISCOVER_FULL_SCAN_MS = 5 * 60 * 1000; // when a file watcher catches chan
|
|
|
36
40
|
const CREDENTIALS_FILE = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
37
41
|
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
38
42
|
const USAGE_INTERVAL_MS = 2 * 60 * 1000;
|
|
43
|
+
// A window opening also checks, so a long-running server still tells you about updates.
|
|
44
|
+
const UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
45
|
+
const UPDATE_THROTTLE_MS = 60 * 1000;
|
|
39
46
|
|
|
40
47
|
const sessions = new Map(); // session_id -> session
|
|
41
48
|
const pending = new Map(); // pending id -> { item, res, payload }
|
|
@@ -463,6 +470,56 @@ function noteCodexLimits(rl, at) {
|
|
|
463
470
|
codexUsage = { updatedAt: at, plan: rl.plan_type || '', limits, credits: rl.credits || null };
|
|
464
471
|
}
|
|
465
472
|
|
|
473
|
+
// ---------- updates ----------
|
|
474
|
+
|
|
475
|
+
let latestVersion = updates.cachedLatest();
|
|
476
|
+
let updateCheckedAt = 0;
|
|
477
|
+
|
|
478
|
+
async function checkForUpdate() {
|
|
479
|
+
if (!updates.enabled()) return;
|
|
480
|
+
updateCheckedAt = Date.now();
|
|
481
|
+
latestVersion = (await updates.checkLatest({ timeoutMs: 10000 })) || latestVersion;
|
|
482
|
+
broadcast();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function updateState() {
|
|
486
|
+
const installed = updates.installedVersion();
|
|
487
|
+
return {
|
|
488
|
+
current: VERSION,
|
|
489
|
+
latest: latestVersion,
|
|
490
|
+
installed,
|
|
491
|
+
available: updates.isNewer(latestVersion, VERSION) && !updates.isNewer(installed, VERSION),
|
|
492
|
+
restart: updates.isNewer(installed, VERSION), // installed, but this server is still the old code
|
|
493
|
+
canSelfUpdate: updates.canSelfUpdate(),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ---------- terminal ----------
|
|
498
|
+
|
|
499
|
+
const TERMINAL_ENABLED = process.platform === 'win32' && process.env.AGENTER_TERMINAL !== '0';
|
|
500
|
+
const terminals = createTerminals((e) => sendAll('terminal', e));
|
|
501
|
+
const terminalId = (cwd) => path.resolve(cwd).toLowerCase();
|
|
502
|
+
|
|
503
|
+
// ---------- folder window ----------
|
|
504
|
+
// The dashboard can't browse this machine's folders itself, so the server opens Windows' own.
|
|
505
|
+
|
|
506
|
+
let pickingFolder = null;
|
|
507
|
+
|
|
508
|
+
function pickFolder(start) {
|
|
509
|
+
return new Promise((resolve) => {
|
|
510
|
+
if (pickingFolder) return resolve({ busy: true });
|
|
511
|
+
if (process.platform !== 'win32') return resolve({ unsupported: true });
|
|
512
|
+
const args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-STA', '-File', path.join(__dirname, 'scripts', 'pick-folder.ps1')];
|
|
513
|
+
pickingFolder = execFile('powershell.exe', args, { windowsHide: true, env: { ...process.env, AGENTER_PICK_START: String(start || '') } }, (err, out) => {
|
|
514
|
+
pickingFolder = null;
|
|
515
|
+
onAttentionChanged();
|
|
516
|
+
const folder = String(out || '').trim();
|
|
517
|
+
resolve(err ? { unsupported: true } : folder ? { path: folder } : { cancelled: true });
|
|
518
|
+
});
|
|
519
|
+
onAttentionChanged();
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
466
523
|
function openInVSCode(cwd) {
|
|
467
524
|
if (!cwd || /["&|<>^%]/.test(cwd) || !fs.existsSync(cwd)) return false;
|
|
468
525
|
const child =
|
|
@@ -660,7 +717,8 @@ function respond(id, action, body = {}) {
|
|
|
660
717
|
|
|
661
718
|
// Sessions are only held while a dashboard window has focus. If you're in VS Code,
|
|
662
719
|
// Claude finishes (or asks) there as usual instead of spinning on the dashboard.
|
|
663
|
-
|
|
720
|
+
// The folder window takes focus from the dashboard, but you're still here.
|
|
721
|
+
const inUse = () => !!pickingFolder || [...clients.values()].some((c) => c.focused);
|
|
664
722
|
|
|
665
723
|
let releaseTimer = null;
|
|
666
724
|
function onAttentionChanged() {
|
|
@@ -673,15 +731,24 @@ function onAttentionChanged() {
|
|
|
673
731
|
}
|
|
674
732
|
|
|
675
733
|
// Quit, from the tray. Open windows close themselves on the 'quit' event.
|
|
676
|
-
|
|
677
|
-
|
|
734
|
+
// A restart (to finish an update) leaves them open; they reconnect to the new server.
|
|
735
|
+
function stopServer({ restart = false } = {}) {
|
|
736
|
+
if (!restart) sendAll('quit', {});
|
|
678
737
|
for (const id of [...pending.keys()]) respond(id, 'pass');
|
|
679
738
|
for (const child of runners.values()) {
|
|
680
739
|
child.interrupted = true;
|
|
681
740
|
killRun(child);
|
|
682
741
|
}
|
|
683
|
-
|
|
684
|
-
|
|
742
|
+
terminals.killAll();
|
|
743
|
+
pickingFolder?.kill();
|
|
744
|
+
if (restart) {
|
|
745
|
+
// It waits for this process to let go of the port.
|
|
746
|
+
const env = { ...process.env, AGENTER_RESTART: '1' };
|
|
747
|
+
spawn(process.execPath, [path.join(__dirname, 'server.js'), '--no-open'], { detached: true, stdio: 'ignore', windowsHide: true, env }).unref();
|
|
748
|
+
} else {
|
|
749
|
+
fs.mkdirSync(SETTINGS_DIR, { recursive: true });
|
|
750
|
+
fs.writeFileSync(STOPPED_FILE, '');
|
|
751
|
+
}
|
|
685
752
|
// Long enough for the hooks to receive those answers.
|
|
686
753
|
setTimeout(() => process.exit(0), 300);
|
|
687
754
|
}
|
|
@@ -929,6 +996,15 @@ function startCodexRun(s, text, { fresh = false } = {}) {
|
|
|
929
996
|
});
|
|
930
997
|
}
|
|
931
998
|
|
|
999
|
+
// ---------- memory ----------
|
|
1000
|
+
|
|
1001
|
+
const memoryContext = (s) => ({ agent: s.agent, cwd: s.cwd, transcriptPath: s.transcriptPath, home: os.homedir(), codexHome: CODEX_HOME });
|
|
1002
|
+
|
|
1003
|
+
function carryFrom(s) {
|
|
1004
|
+
const t = s?.transcriptPath && readTranscript(s.transcriptPath);
|
|
1005
|
+
return t ? carriedContext({ agent: s.agent, cwd: s.cwd, title: s.title, messages: t.messages }) : '';
|
|
1006
|
+
}
|
|
1007
|
+
|
|
932
1008
|
// ---------- hook events ----------
|
|
933
1009
|
|
|
934
1010
|
function handleHook(p, res) {
|
|
@@ -1011,6 +1087,9 @@ function handleHook(p, res) {
|
|
|
1011
1087
|
|
|
1012
1088
|
function snapshot() {
|
|
1013
1089
|
return {
|
|
1090
|
+
version: VERSION,
|
|
1091
|
+
update: updateState(),
|
|
1092
|
+
terminal: TERMINAL_ENABLED,
|
|
1014
1093
|
settings,
|
|
1015
1094
|
usage,
|
|
1016
1095
|
codexUsage,
|
|
@@ -1020,14 +1099,18 @@ function snapshot() {
|
|
|
1020
1099
|
};
|
|
1021
1100
|
}
|
|
1022
1101
|
|
|
1102
|
+
const sendAll = (event, data) => {
|
|
1103
|
+
const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
1104
|
+
for (const c of clients.keys()) c.write(msg);
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1023
1107
|
let broadcastQueued = false;
|
|
1024
1108
|
function broadcast() {
|
|
1025
1109
|
if (broadcastQueued) return;
|
|
1026
1110
|
broadcastQueued = true;
|
|
1027
1111
|
setTimeout(() => {
|
|
1028
1112
|
broadcastQueued = false;
|
|
1029
|
-
|
|
1030
|
-
for (const c of clients.keys()) c.write(msg);
|
|
1113
|
+
sendAll('state', snapshot());
|
|
1031
1114
|
}, 30);
|
|
1032
1115
|
}
|
|
1033
1116
|
|
|
@@ -1115,6 +1198,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1115
1198
|
clients.set(res, { id: url.searchParams.get('client') || '', focused: url.searchParams.get('focused') === '1' });
|
|
1116
1199
|
onAttentionChanged();
|
|
1117
1200
|
if (!usage || Date.now() - usage.fetchedAt > 60000) fetchUsage();
|
|
1201
|
+
if (Date.now() - updateCheckedAt > UPDATE_THROTTLE_MS) checkForUpdate();
|
|
1118
1202
|
const ping = setInterval(() => res.write(': ping\n\n'), 20000);
|
|
1119
1203
|
req.on('close', () => {
|
|
1120
1204
|
clearInterval(ping);
|
|
@@ -1141,6 +1225,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1141
1225
|
return json(res, 200, { apps: await findApps(s.cwd), detects: process.platform === 'win32' });
|
|
1142
1226
|
}
|
|
1143
1227
|
|
|
1228
|
+
if (req.method === 'GET' && url.pathname === '/api/memory') {
|
|
1229
|
+
const s = sessions.get(url.searchParams.get('session'));
|
|
1230
|
+
if (!s) return json(res, 404, { error: 'unknown session' });
|
|
1231
|
+
return json(res, 200, { files: readMemory(memoryContext(s)) });
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1144
1234
|
if (req.method === 'POST' && url.pathname.startsWith('/api/')) {
|
|
1145
1235
|
const body = await readBody(req);
|
|
1146
1236
|
|
|
@@ -1148,6 +1238,39 @@ const server = http.createServer(async (req, res) => {
|
|
|
1148
1238
|
stopServer();
|
|
1149
1239
|
return json(res, 200, {});
|
|
1150
1240
|
}
|
|
1241
|
+
if (url.pathname === '/api/restart') {
|
|
1242
|
+
stopServer({ restart: true });
|
|
1243
|
+
return json(res, 200, {});
|
|
1244
|
+
}
|
|
1245
|
+
if (url.pathname === '/api/update') {
|
|
1246
|
+
if (!TERMINAL_ENABLED) return json(res, 400, { error: 'Run agenter update in a terminal.' });
|
|
1247
|
+
if (!updates.canSelfUpdate()) return json(res, 400, { error: 'This copy is a git clone. Update it with git pull.' });
|
|
1248
|
+
const id = terminalId(os.homedir());
|
|
1249
|
+
const term = terminals.open(id, os.homedir());
|
|
1250
|
+
if (term.running) return json(res, 409, { error: 'That terminal is busy. Wait for it to finish, or press Stop.' });
|
|
1251
|
+
const quote = (p) => `'${p.replace(/'/g, "''")}'`;
|
|
1252
|
+
terminals.run(id, `& ${quote(process.execPath)} ${quote(path.join(__dirname, 'cli.js'))} update`, checkForUpdate);
|
|
1253
|
+
return json(res, 200, { id });
|
|
1254
|
+
}
|
|
1255
|
+
if (url.pathname.startsWith('/api/terminal/')) {
|
|
1256
|
+
if (!TERMINAL_ENABLED) return json(res, 400, { error: 'The terminal is turned off or not available here.' });
|
|
1257
|
+
if (url.pathname === '/api/terminal/open') {
|
|
1258
|
+
const cwd = sessions.get(body.sessionId)?.cwd;
|
|
1259
|
+
const folder = cwd && fs.existsSync(cwd) ? cwd : os.homedir();
|
|
1260
|
+
return json(res, 200, terminals.open(terminalId(folder), folder));
|
|
1261
|
+
}
|
|
1262
|
+
const ok = url.pathname === '/api/terminal/run' ? terminals.run(body.id, String(body.input ?? '')) : url.pathname === '/api/terminal/stop' && terminals.stop(body.id);
|
|
1263
|
+
return json(res, ok ? 200 : 404, {});
|
|
1264
|
+
}
|
|
1265
|
+
if (url.pathname === '/api/pick-folder') {
|
|
1266
|
+
return json(res, 200, await pickFolder(body.start));
|
|
1267
|
+
}
|
|
1268
|
+
if (url.pathname === '/api/memory/save') {
|
|
1269
|
+
const s = sessions.get(body.sessionId);
|
|
1270
|
+
if (!s) return json(res, 404, { error: 'unknown session' });
|
|
1271
|
+
const { status, ...out } = saveMemory(memoryContext(s), body.path, body.text, body.mtime);
|
|
1272
|
+
return json(res, status, out);
|
|
1273
|
+
}
|
|
1151
1274
|
if (url.pathname === '/api/respond') {
|
|
1152
1275
|
const s = sessions.get(pending.get(body.id)?.item.sessionId);
|
|
1153
1276
|
const ok = respond(body.id, body.action, body);
|
|
@@ -1178,6 +1301,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
1178
1301
|
const cwd = String(body.cwd || '').trim().replace(/^"(.*)"$/, '$1');
|
|
1179
1302
|
const text = String(body.message || '').trim();
|
|
1180
1303
|
const codex = body.agent === 'codex';
|
|
1304
|
+
const carried = carryFrom(sessions.get(body.fromSessionId));
|
|
1305
|
+
const prompt = carried ? `${text}\n\n${carried}` : text;
|
|
1181
1306
|
if (!text) return json(res, 400, { error: `Type a first message for ${codex ? 'Codex' : 'Claude'}.` });
|
|
1182
1307
|
let isDir = false;
|
|
1183
1308
|
try {
|
|
@@ -1189,7 +1314,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1189
1314
|
// Codex names the session itself, so it's registered once Codex has started it.
|
|
1190
1315
|
const draft = Object.assign(newSession(''), { agent: 'codex', title: text.split('\n')[0].slice(0, 140), lastEventAt: Date.now() });
|
|
1191
1316
|
setCwd(draft, path.resolve(cwd));
|
|
1192
|
-
const started = await Promise.race([startCodexRun(draft,
|
|
1317
|
+
const started = await Promise.race([startCodexRun(draft, prompt, { fresh: true }), new Promise((r) => setTimeout(() => r({ error: 'Codex took too long to start.' }), 60000))]);
|
|
1193
1318
|
if (!started?.id) return json(res, 500, { error: `Codex couldn't start: ${started?.error || 'unknown error'}` });
|
|
1194
1319
|
settings.drive = { ...settings.drive, [started.id]: 'agenter' };
|
|
1195
1320
|
saveSettings();
|
|
@@ -1204,7 +1329,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1204
1329
|
s.lastEventAt = Date.now();
|
|
1205
1330
|
settings.drive = { ...settings.drive, [s.id]: 'agenter' };
|
|
1206
1331
|
saveSettings();
|
|
1207
|
-
if (!startRun(s,
|
|
1332
|
+
if (!startRun(s, prompt, { fresh: true })) {
|
|
1208
1333
|
sessions.delete(s.id);
|
|
1209
1334
|
return json(res, 500, { error: "Couldn't start Claude." });
|
|
1210
1335
|
}
|
|
@@ -1278,7 +1403,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1278
1403
|
server.requestTimeout = 0; // hooks can legitimately wait a long time for you
|
|
1279
1404
|
// Only listen when run directly; the tests load this file to drive the hook and steering logic.
|
|
1280
1405
|
if (require.main === module) {
|
|
1406
|
+
let portRetries = process.env.AGENTER_RESTART ? 50 : 0;
|
|
1281
1407
|
server.on('error', (err) => {
|
|
1408
|
+
if (err.code === 'EADDRINUSE' && portRetries-- > 0) return void setTimeout(() => server.listen(PORT, HOST), 200);
|
|
1282
1409
|
if (err.code === 'EADDRINUSE') console.error(`Port ${PORT} is busy. Is Agenter already running? (Set AGENTER_PORT to use another port.)`);
|
|
1283
1410
|
else console.error(err);
|
|
1284
1411
|
process.exit(1);
|
|
@@ -1286,12 +1413,18 @@ if (require.main === module) {
|
|
|
1286
1413
|
server.listen(PORT, HOST, () => {
|
|
1287
1414
|
const url = `http://localhost:${PORT}`;
|
|
1288
1415
|
console.log(`Agenter dashboard: ${url}`);
|
|
1416
|
+
delete process.env.AGENTER_RESTART;
|
|
1289
1417
|
fs.rmSync(STOPPED_FILE, { force: true });
|
|
1418
|
+
// Shortcuts start us inside the install folder, and on Windows a folder that's in use
|
|
1419
|
+
// can't be replaced by `npm i -g`. Everything here uses full paths.
|
|
1420
|
+
process.chdir(os.homedir());
|
|
1290
1421
|
startTray();
|
|
1291
1422
|
startDiscovery();
|
|
1292
1423
|
startCodexDiscovery();
|
|
1293
1424
|
fetchUsage();
|
|
1294
1425
|
setInterval(fetchUsage, USAGE_INTERVAL_MS);
|
|
1426
|
+
checkForUpdate();
|
|
1427
|
+
setInterval(checkForUpdate, UPDATE_INTERVAL_MS);
|
|
1295
1428
|
if (!process.argv.includes('--no-open')) openDashboard(url);
|
|
1296
1429
|
});
|
|
1297
1430
|
}
|