koneck 2.26.0 → 2.28.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.
Files changed (63) hide show
  1. package/dist/doctor.d.ts +27 -0
  2. package/dist/doctor.d.ts.map +1 -1
  3. package/dist/doctor.js +70 -9
  4. package/dist/doctor.js.map +1 -1
  5. package/dist/engine.d.ts.map +1 -1
  6. package/dist/engine.js +4 -1
  7. package/dist/engine.js.map +1 -1
  8. package/dist/ink-chat.d.ts +12 -3
  9. package/dist/ink-chat.d.ts.map +1 -1
  10. package/dist/ink-chat.js +246 -30
  11. package/dist/ink-chat.js.map +1 -1
  12. package/dist/quality-gate.d.ts +5 -1
  13. package/dist/quality-gate.d.ts.map +1 -1
  14. package/dist/quality-gate.js +8 -6
  15. package/dist/quality-gate.js.map +1 -1
  16. package/dist/session.d.ts +10 -1
  17. package/dist/session.d.ts.map +1 -1
  18. package/dist/session.js +12 -1
  19. package/dist/session.js.map +1 -1
  20. package/dist/web/api.d.ts.map +1 -1
  21. package/dist/web/api.js +122 -27
  22. package/dist/web/api.js.map +1 -1
  23. package/dist/web/changes.d.ts +55 -0
  24. package/dist/web/changes.d.ts.map +1 -0
  25. package/dist/web/changes.js +215 -0
  26. package/dist/web/changes.js.map +1 -0
  27. package/dist/web/files.d.ts +43 -0
  28. package/dist/web/files.d.ts.map +1 -0
  29. package/dist/web/files.js +131 -0
  30. package/dist/web/files.js.map +1 -0
  31. package/dist/web/history.d.ts +61 -0
  32. package/dist/web/history.d.ts.map +1 -0
  33. package/dist/web/history.js +197 -0
  34. package/dist/web/history.js.map +1 -0
  35. package/dist/web/mark.d.ts +15 -0
  36. package/dist/web/mark.d.ts.map +1 -0
  37. package/dist/web/mark.js +15 -0
  38. package/dist/web/mark.js.map +1 -0
  39. package/dist/web/search.d.ts +45 -0
  40. package/dist/web/search.d.ts.map +1 -0
  41. package/dist/web/search.js +147 -0
  42. package/dist/web/search.js.map +1 -0
  43. package/dist/web/sessions.d.ts +8 -0
  44. package/dist/web/sessions.d.ts.map +1 -1
  45. package/dist/web/sessions.js +9 -0
  46. package/dist/web/sessions.js.map +1 -1
  47. package/dist/web/ui-client.d.ts +14 -0
  48. package/dist/web/ui-client.d.ts.map +1 -0
  49. package/dist/web/ui-client.js +0 -0
  50. package/dist/web/ui-client.js.map +1 -0
  51. package/dist/web/ui-css.d.ts +15 -0
  52. package/dist/web/ui-css.d.ts.map +1 -0
  53. package/dist/web/ui-css.js +329 -0
  54. package/dist/web/ui-css.js.map +1 -0
  55. package/dist/web/ui.d.ts +6 -9
  56. package/dist/web/ui.d.ts.map +1 -1
  57. package/dist/web/ui.js +100 -541
  58. package/dist/web/ui.js.map +1 -1
  59. package/dist/workspace-trust.d.ts +8 -0
  60. package/dist/workspace-trust.d.ts.map +1 -1
  61. package/dist/workspace-trust.js +11 -0
  62. package/dist/workspace-trust.js.map +1 -1
  63. package/package.json +1 -1
package/dist/web/ui.js CHANGED
@@ -1,230 +1,138 @@
1
1
  /**
2
- * The single page, as a string.
2
+ * The page, assembled.
3
3
  *
4
- * No build step and no dependencies: one HTML document with its styles and script inline, served
5
- * from memory. A web UI that needs bundling is a web UI that rots and KONECK is installed with
6
- * `npm i -g koneck`, so anything requiring a second toolchain would be broken for most people on
7
- * the day they first tried it.
8
- *
9
- * Nothing is fetched from the network either. A page that pulls a font or a script from a CDN does
10
- * not work on a machine that is offline, which is the machine most likely to be running a local
11
- * model in the first place.
4
+ * One HTML document with its styles and script inline, served from memory. No build step and
5
+ * nothing fetched from the network: KONECK is installed with `npm i -g koneck`, so anything needing
6
+ * a second toolchain would be broken for most people on the day they first tried it — and a page
7
+ * that pulls a font from a CDN does not work on the machine most likely to be running a local
8
+ * model, which is one that is offline.
12
9
  */
13
- /** The mark, drawn as the terminal draws it: a K facing its own reflection. */
14
- const MARK = `<svg viewBox="0 0 34 12" class="mark" aria-hidden="true">
15
- <g fill="currentColor">
16
- <rect x="0" y="0" width="5" height="12"/><rect x="29" y="0" width="5" height="12"/>
17
- <path d="M5 6 L11 0 L14 0 L7 7 Z"/><path d="M5 6 L11 12 L14 12 L7 5 Z"/>
18
- <path d="M29 6 L23 0 L20 0 L27 7 Z"/><path d="M29 6 L23 12 L20 12 L27 5 Z"/>
19
- </g></svg>`;
20
- /** The mirrored K, as a character reference so no source file needs the glyph itself. */
21
- const KK = '<span class="kk">K&#42200;</span>';
10
+ import { MARK_MASK } from './mark.js';
11
+ import { css } from './ui-css.js';
12
+ import { clientScript } from './ui-client.js';
22
13
  export function page() {
23
14
  return `<!doctype html>
24
15
  <html lang="en"><head>
25
16
  <meta charset="utf-8">
26
17
  <meta name="viewport" content="width=device-width,initial-scale=1">
27
18
  <meta name="referrer" content="no-referrer">
19
+ <meta name="color-scheme" content="light dark">
28
20
  <title>KONECK</title>
29
- <style>
30
- :root{
31
- --bg:#0b1013; --panel:#111a1f; --panel-2:#16232a; --line:#1e3038;
32
- --ink:#d7e6ec; --muted:#7f9aa6; --dim:#4f6874;
33
- --cyan:#4dd6e8; --amber:#e8b04d; --green:#57d99a; --red:#ef6d72; --violet:#a78bfa;
34
- --radius:10px; --mono:ui-monospace,'SF Mono',Menlo,Consolas,'DejaVu Sans Mono',monospace;
35
- }
36
- *{box-sizing:border-box}
37
- html,body{height:100%}
38
- body{margin:0;background:var(--bg);color:var(--ink);
39
- font:14px/1.55 ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
40
- display:flex;overflow:hidden}
41
- button,input,textarea,select{font:inherit;color:inherit}
42
- ::-webkit-scrollbar{width:9px;height:9px}
43
- ::-webkit-scrollbar-thumb{background:#24373f;border-radius:9px}
44
- ::-webkit-scrollbar-track{background:transparent}
45
- .kk{color:var(--cyan);font-family:var(--mono)}
46
-
47
- aside{width:264px;flex:0 0 264px;background:var(--panel);border-right:1px solid var(--line);
48
- display:flex;flex-direction:column;min-height:0}
49
- .brand{display:flex;align-items:center;gap:9px;padding:16px 16px 13px;color:var(--cyan)}
50
- .mark{width:30px;height:11px;display:block}
51
- .brand b{font-size:13px;letter-spacing:.16em;font-weight:600;color:var(--ink)}
52
- .brand span{font-size:9px;letter-spacing:.14em;color:var(--dim);
53
- border:1px solid var(--line);border-radius:4px;padding:2px 5px}
54
- .newbtn{margin:0 12px 14px;padding:9px;border:1px solid var(--line);background:var(--panel-2);
55
- border-radius:var(--radius);color:var(--ink);cursor:pointer;display:flex;align-items:center;
56
- justify-content:center;gap:7px;transition:.14s}
57
- .newbtn:hover{border-color:var(--cyan);color:var(--cyan)}
58
- .shead{padding:6px 16px;font-size:10px;letter-spacing:.13em;color:var(--dim);text-transform:uppercase}
59
- .slist{overflow-y:auto;flex:1;min-height:0;padding:0 8px 10px}
60
- .srow{padding:8px 10px;border-radius:8px;cursor:pointer;margin-bottom:2px;border:1px solid transparent}
61
- .srow:hover{background:var(--panel-2)}
62
- .srow.on{background:var(--panel-2);border-color:var(--line)}
63
- .srow .t{font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
64
- .srow .m{font-size:10.5px;color:var(--dim);margin-top:2px;display:flex;gap:7px;align-items:center}
65
- .dot{width:6px;height:6px;border-radius:50%;background:var(--green);flex:0 0 6px}
66
- .dot.busy{background:var(--amber);animation:pulse 1.1s infinite}
67
- @keyframes pulse{50%{opacity:.28}}
68
- .foot{border-top:1px solid var(--line);padding:10px 14px;font-size:10.5px;color:var(--dim);
69
- display:flex;justify-content:space-between;align-items:center}
70
-
71
- main{flex:1;display:flex;flex-direction:column;min-width:0;min-height:0}
72
- header{height:46px;flex:0 0 46px;border-bottom:1px solid var(--line);display:flex;
73
- align-items:center;gap:10px;padding:0 16px;background:var(--panel);font-size:12px}
74
- .pill{border:1px solid var(--line);background:var(--panel-2);border-radius:999px;
75
- padding:4px 11px;color:var(--muted);display:flex;gap:6px;align-items:center;cursor:pointer}
76
- .pill:hover{border-color:var(--cyan);color:var(--ink)}
77
- .pill b{color:var(--ink);font-weight:500}
78
- .spacer{flex:1}
79
- .stat{color:var(--dim);font-variant-numeric:tabular-nums}
80
- .stat b{color:var(--muted);font-weight:500}
81
-
82
- #feed{flex:1;overflow-y:auto;padding:26px 0 8px;min-height:0}
83
- .wrap{max-width:820px;margin:0 auto;padding:0 24px}
84
- .hero{text-align:center;padding:13vh 0 0;color:var(--muted)}
85
- .hero .big{display:flex;align-items:center;justify-content:center;gap:12px;color:var(--cyan);
86
- margin-bottom:8px}
87
- .hero .big .mark{width:52px;height:19px}
88
- .hero h1{font-size:21px;margin:0;color:var(--ink);font-weight:600;letter-spacing:.01em}
89
- .hero p{font-size:13px;margin:7px 0 0}
90
-
91
- .msg{margin-bottom:20px}
92
- .you{background:var(--panel-2);border:1px solid var(--line);border-radius:var(--radius);
93
- padding:11px 14px;white-space:pre-wrap;word-break:break-word}
94
- .who{display:flex;align-items:center;gap:8px;font-size:10.5px;letter-spacing:.12em;
95
- color:var(--dim);text-transform:uppercase;margin-bottom:7px}
96
- .reply{white-space:pre-wrap;word-break:break-word}
97
- .reply code{background:var(--panel-2);border:1px solid var(--line);border-radius:4px;
98
- padding:1px 5px;font-family:var(--mono);font-size:12.5px}
99
- .reply pre{background:var(--panel);border:1px solid var(--line);border-radius:8px;
100
- padding:12px 14px;overflow-x:auto;margin:10px 0;white-space:pre}
101
- .reply pre code{background:none;border:0;padding:0;font-size:12.5px;line-height:1.5}
102
- .reply h3{font-size:14px;margin:16px 0 6px}
103
- .reply strong{color:#eaf4f8}
104
- .reply table{border-collapse:collapse;margin:10px 0;font-size:12.5px;display:block;overflow-x:auto}
105
- .reply th,.reply td{border:1px solid var(--line);padding:5px 9px;text-align:left}
106
- .reply th{background:var(--panel-2);color:var(--muted)}
107
-
108
- .tool{border:1px solid var(--line);border-left:2px solid var(--dim);border-radius:8px;
109
- background:var(--panel);margin:8px 0;overflow:hidden}
110
- .tool.ok{border-left-color:var(--green)} .tool.bad{border-left-color:var(--red)}
111
- .tool.run{border-left-color:var(--amber)}
112
- .tool .th{display:flex;align-items:center;gap:9px;padding:8px 12px;cursor:pointer;font-size:12.5px}
113
- .tool .nm{font-family:var(--mono);color:var(--cyan)}
114
- .tool .dt{color:var(--muted);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
115
- .tool .ms{color:var(--dim);font-variant-numeric:tabular-nums;font-size:11px}
116
- .tool .body{border-top:1px solid var(--line);padding:9px 12px;font-family:var(--mono);
117
- font-size:11.5px;color:var(--muted);white-space:pre-wrap;max-height:280px;overflow:auto}
118
- .think{border:1px dashed var(--line);border-radius:8px;padding:9px 12px;margin:8px 0;
119
- color:var(--violet);font-size:12.5px;white-space:pre-wrap;max-height:200px;overflow:auto}
120
- .notice{border:1px solid var(--line);border-left:2px solid var(--amber);border-radius:8px;
121
- padding:10px 13px;margin:9px 0;font-size:12.5px;color:var(--muted);white-space:pre-wrap}
122
- .notice.info{border-left-color:var(--cyan)}
123
- .err{border:1px solid #4a2226;border-left:2px solid var(--red);background:#180f11;
124
- border-radius:8px;padding:10px 13px;margin:9px 0;font-size:12.5px;color:#f4b8ba;white-space:pre-wrap}
125
-
126
- .plan{border:1px solid var(--line);border-radius:8px;background:var(--panel);margin:9px 0;
127
- padding:10px 13px;font-size:12.5px}
128
- .plan h4{margin:0 0 7px;font-size:10.5px;letter-spacing:.12em;color:var(--dim);
129
- text-transform:uppercase;display:flex;gap:8px}
130
- .step{display:flex;gap:9px;padding:2px 0;color:var(--muted)}
131
- .step .bx{width:13px;color:var(--dim);flex:0 0 13px}
132
- .step.done{color:var(--dim)} .step.done .bx{color:var(--green)}
133
- .step.doing{color:var(--ink)} .step.doing .bx{color:var(--amber)}
134
- .step.blocked .bx{color:var(--red)}
135
-
136
- .ask{border:1px solid var(--amber);border-radius:var(--radius);background:#1a1508;
137
- padding:12px 14px;margin:11px 0}
138
- .ask .q{font-size:13px;margin-bottom:4px}
139
- .ask .a{font-family:var(--mono);font-size:11.5px;color:var(--muted);margin-bottom:10px;
140
- white-space:pre-wrap;max-height:130px;overflow:auto}
141
- .ask .btns{display:flex;gap:8px}
142
- .ask button{padding:6px 14px;border-radius:7px;border:1px solid var(--line);cursor:pointer;
143
- background:var(--panel-2)}
144
- .ask .yes{border-color:var(--green);color:var(--green)}
145
- .ask .no{border-color:var(--red);color:var(--red)}
146
-
147
- .busy{display:flex;align-items:center;gap:10px;color:var(--amber);font-size:12.5px;margin:8px 0}
148
- .sp{width:11px;height:11px;border:2px solid var(--amber);border-right-color:transparent;
149
- border-radius:50%;animation:spin .8s linear infinite}
150
- @keyframes spin{to{transform:rotate(360deg)}}
151
-
152
- footer{flex:0 0 auto;border-top:1px solid var(--line);background:var(--panel);padding:12px 0 14px}
153
- .cbox{max-width:820px;margin:0 auto;padding:0 24px}
154
- .cin{display:flex;align-items:flex-end;gap:10px;border:1px solid var(--line);
155
- background:var(--panel-2);border-radius:var(--radius);padding:9px 11px;transition:.14s}
156
- .cin:focus-within{border-color:var(--cyan)}
157
- textarea{flex:1;background:none;border:0;outline:0;resize:none;max-height:190px;
158
- line-height:1.5;font-size:14px}
159
- textarea::placeholder{color:var(--dim)}
160
- .send{width:31px;height:31px;flex:0 0 31px;border-radius:50%;border:0;cursor:pointer;
161
- background:var(--cyan);color:#04181c;font-size:15px;display:grid;place-items:center}
162
- .send:disabled{background:var(--line);color:var(--dim);cursor:not-allowed}
163
- .chint{display:flex;gap:12px;margin-top:7px;font-size:10.5px;color:var(--dim)}
164
- .chint kbd{border:1px solid var(--line);border-radius:4px;padding:0 4px;font-family:var(--mono)}
165
-
166
- dialog{border:1px solid var(--line);background:var(--panel);color:var(--ink);
167
- border-radius:var(--radius);padding:0;max-width:440px;width:92%}
168
- dialog::backdrop{background:rgba(0,0,0,.66)}
169
- .dlg{padding:18px}
170
- .dlg h3{margin:0 0 4px;font-size:15px}
171
- .dlg p{margin:0 0 14px;color:var(--muted);font-size:12.5px}
172
- .dlg input,.dlg select{width:100%;padding:8px 10px;border-radius:8px;border:1px solid var(--line);
173
- background:var(--panel-2);margin-bottom:10px}
174
- .dlg .row{display:flex;gap:8px;justify-content:flex-end}
175
- .dlg button{padding:7px 15px;border-radius:8px;border:1px solid var(--line);cursor:pointer;
176
- background:var(--panel-2)}
177
- .dlg .go{border-color:var(--cyan);color:var(--cyan)}
178
- </style>
21
+ <style>${css(MARK_MASK)}</style>
179
22
  </head><body>
180
23
 
181
- <aside>
182
- <div class="brand">${MARK}<b>KONECK</b><span>web</span></div>
183
- <button class="newbtn" id="new">+ New session</button>
184
- <div class="shead">Sessions</div>
24
+ <aside id="side">
25
+ <div class="brand">
26
+ <div class="mark"></div>
27
+ <b>KONECK</b>
28
+ <span class="tag">web</span>
29
+ </div>
30
+ <button class="newbtn" id="new">New session</button>
185
31
  <div class="slist" id="slist"></div>
186
- <div class="foot"><span id="wsname">&mdash;</span><span id="conn">offline</span></div>
32
+ <div class="foot">
33
+ <span id="wsname" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">&mdash;</span>
34
+ <button class="themebtn" id="theme">system</button>
35
+ <span id="conn">offline</span>
36
+ </div>
187
37
  </aside>
188
38
 
189
39
  <main>
190
40
  <header>
191
- <div class="pill" id="pprov">provider <b>&mdash;</b></div>
192
- <div class="pill" id="pmodel">model <b>&mdash;</b></div>
41
+ <button class="iconbtn" id="sidebtn" title="Show or hide the sidebar">&#9776;</button>
42
+ <div class="tabs">
43
+ <button class="tab on" id="tab-chat">Chat</button>
44
+ <button class="tab" id="tab-changes">Changes</button>
45
+ <button class="tab" id="tab-timing">Timing</button>
46
+ <button class="tab" id="tab-files">Files</button>
47
+ <button class="tab" id="tab-search">Search</button>
48
+ </div>
193
49
  <div class="spacer"></div>
194
- <div class="stat" id="stok">&mdash;</div>
195
- <div class="pill" id="pabort" style="display:none;border-color:var(--red);color:var(--red)">stop</div>
50
+ <button class="pill" id="pws" title="Switch workspace">workspace <b>&mdash;</b></button>
51
+ <button class="pill" id="pprov">provider <b>&mdash;</b></button>
52
+ <button class="pill" id="pmodel">model <b>&mdash;</b></button>
53
+ <div class="stat" id="stat">&mdash;</div>
54
+ <button class="stopbtn" id="stop" hidden>stop</button>
196
55
  </header>
197
56
 
198
- <div id="feed"><div class="wrap" id="stream">
199
- <div class="hero">
200
- <div class="big">${MARK}<h1>Into the workspace</h1></div>
201
- <p>Describe what you want built. Everything the CLI does, from here.</p>
57
+ <div class="view" id="chatview">
58
+ <div class="wrap" id="stream">
59
+ <div class="hero">
60
+ <div class="mark"></div>
61
+ <h1>Into the workspace</h1>
62
+ <p>Everything the CLI does, from here. Ask for a change and watch it happen.</p>
63
+ <div class="ex">
64
+ <button>What is in this project?</button>
65
+ <button>Find the slowest test and explain why</button>
66
+ <button>Add a README with real usage examples</button>
67
+ </div>
68
+ </div>
202
69
  </div>
203
- </div></div>
70
+ </div>
71
+
72
+ <div class="view" id="changesview" hidden></div>
73
+ <div class="view" id="timingview" hidden></div>
204
74
 
205
- <footer><div class="cbox">
206
- <div class="cin">
207
- <textarea id="ta" rows="1" placeholder="Describe what you want to build..."></textarea>
208
- <button class="send" id="send" disabled>&uarr;</button>
75
+ <div class="view" id="filesview" hidden>
76
+ <div class="split">
77
+ <div class="tree" id="tree"></div>
78
+ <div class="fileview" id="fileview"></div>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="view" id="searchview" hidden>
83
+ <div class="wrap">
84
+ <div class="searchbar">
85
+ <input id="q" placeholder="Search everything KONECK has done here&hellip;" autocomplete="off">
86
+ <button id="qgo">Search</button>
87
+ </div>
88
+ <div id="qresults"></div>
209
89
  </div>
210
- <div class="chint">
211
- <span><kbd>Enter</kbd> send</span><span><kbd>Shift</kbd>+<kbd>Enter</kbd> newline</span>
212
- <span id="hintstate"></span>
90
+ </div>
91
+
92
+ <footer id="composer">
93
+ <div class="cbox">
94
+ <div class="cin">
95
+ <textarea id="ta" rows="1" placeholder="Describe what you want to build&hellip;"></textarea>
96
+ <button class="send" id="send" disabled title="Send">&uarr;</button>
97
+ </div>
98
+ <div class="chint">
99
+ <span><kbd>Enter</kbd> send</span>
100
+ <span><kbd>Shift</kbd>+<kbd>Enter</kbd> newline</span>
101
+ </div>
213
102
  </div>
214
- </div></footer>
103
+ </footer>
104
+ <footer id="readonlybar" hidden>
105
+ <div class="readonly">
106
+ <span>Finished session, read only &mdash;</span>
107
+ <b id="readonlywhat" style="font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></b>
108
+ </div>
109
+ </footer>
215
110
  </main>
216
111
 
217
112
  <dialog id="tokdlg"><form class="dlg" method="dialog">
218
113
  <h3>Token needed</h3>
219
114
  <p>Anything that starts a run needs the token KONECK printed when it started. It is in the
220
115
  terminal you launched it from.</p>
221
- <input id="tokin" placeholder="paste the token" autocomplete="off">
116
+ <label for="tokin">Token</label>
117
+ <input id="tokin" placeholder="paste it here" autocomplete="off">
222
118
  <div class="row"><button class="go" id="toksave">Use it</button></div>
223
119
  </form></dialog>
224
120
 
121
+ <dialog id="wsdlg"><form class="dlg" method="dialog">
122
+ <h3>Workspace</h3>
123
+ <p>Only workspaces you have already trusted are listed. Trust is granted at a terminal, after
124
+ being asked &mdash; so a browser cannot add to this list.</p>
125
+ <label for="wschoice">Open</label>
126
+ <select id="wschoice"></select>
127
+ <div class="row"><button>Cancel</button><button class="go" id="wssave">Switch</button></div>
128
+ </form></dialog>
129
+
225
130
  <dialog id="mdlg"><form class="dlg" method="dialog">
226
- <h3>Model</h3><p>Applies from your next message.</p>
131
+ <h3>Model</h3>
132
+ <p>Applies from your next message.</p>
133
+ <label for="mprov">Provider</label>
227
134
  <select id="mprov"></select>
135
+ <label for="mmodel">Model</label>
228
136
  <input id="mmodel" placeholder="model name" autocomplete="off">
229
137
  <div class="row"><button>Cancel</button><button class="go" id="msave">Switch</button></div>
230
138
  </form></dialog>
@@ -234,353 +142,4 @@ ${clientScript()}
234
142
  </script>
235
143
  </body></html>`;
236
144
  }
237
- /**
238
- * The client.
239
- *
240
- * Plain DOM. A framework would be more code than the page it renders, and this has one job: turn a
241
- * stream of typed events into a transcript that reads well.
242
- */
243
- function clientScript() {
244
- return String.raw `
245
- const $ = (id) => document.getElementById(id);
246
- const state = {
247
- token: new URLSearchParams(location.search).get('token') || localStorage.getItem('koneck.token') || '',
248
- session: null, provider: '-', model: '-', busy: false, es: null,
249
- // The element currently being appended to, so streamed text lands in one bubble rather than
250
- // producing a new one per chunk.
251
- replyEl: null, thinkEl: null, toolEls: new Map(), planEl: null, lastTool: null,
252
- };
253
- if (state.token) localStorage.setItem('koneck.token', state.token);
254
- // The token does not belong in the address bar, where it survives in history and in a screenshot.
255
- if (location.search.indexOf('token=') !== -1) history.replaceState({}, '', location.pathname);
256
-
257
- const api = async (path, opts) => {
258
- opts = opts || {};
259
- const sep = path.indexOf('?') === -1 ? '?' : '&';
260
- const res = await fetch(path + sep + 'token=' + encodeURIComponent(state.token), {
261
- method: opts.method || 'GET',
262
- body: opts.body,
263
- headers: { 'content-type': 'application/json' },
264
- });
265
- if (res.status === 401) { $('tokdlg').showModal(); throw new Error('token required'); }
266
- if (!res.ok) {
267
- let msg = res.statusText;
268
- try { msg = (await res.json()).error || msg; } catch (e) {}
269
- throw new Error(msg);
270
- }
271
- return res.status === 204 ? null : res.json();
272
- };
273
-
274
- const stream = () => $('stream');
275
- const atBottom = () => {
276
- const f = $('feed');
277
- return f.scrollHeight - f.scrollTop - f.clientHeight < 90;
278
- };
279
- const stick = (was) => { if (was) $('feed').scrollTop = $('feed').scrollHeight; };
280
- const el = (tag, cls, text) => {
281
- const n = document.createElement(tag);
282
- if (cls) n.className = cls;
283
- if (text !== undefined) n.textContent = text;
284
- return n;
285
- };
286
- const add = (node) => { const was = atBottom(); stream().appendChild(node); stick(was); return node; };
287
- const clearHero = () => { const h = stream().querySelector('.hero'); if (h) h.remove(); };
288
-
289
- /**
290
- * Enough markdown to read a reply: fences, inline code, bold, headings, tables.
291
- *
292
- * Everything is escaped before any tag is inserted, and fenced blocks are lifted out first so
293
- * their contents can never be interpreted. Model output is untrusted text — it routinely contains
294
- * HTML, because it is often writing HTML.
295
- */
296
- function md(src) {
297
- const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
298
- const blocks = [];
299
- let text = src.replace(/` + '```' + String.raw `([a-zA-Z0-9_+-]*)\n([\s\S]*?)` + '```' + String.raw `/g, function (m, lang, code) {
300
- blocks.push('<pre><code>' + esc(code.replace(/\n$/, '')) + '</code></pre>');
301
- return '' + (blocks.length - 1) + '';
302
- });
303
- text = esc(text);
304
- text = text.replace(/(^\|.+\|\n\|[ :|-]+\|\n(?:\|.*\|\n?)*)/gm, function (t) {
305
- const rows = t.trim().split('\n').filter(function (r) { return !/^\|[ :|-]+\|$/.test(r.trim()); });
306
- const cells = (r, tag) => '<tr>' + r.split('|').slice(1, -1)
307
- .map((c) => '<' + tag + '>' + c.trim() + '</' + tag + '>').join('') + '</tr>';
308
- return '<table>' + cells(rows[0], 'th') +
309
- rows.slice(1).map((r) => cells(r, 'td')).join('') + '</table>';
310
- });
311
- text = text
312
- .replace(/^#{2,3} (.*)$/gm, '<h3>$1</h3>')
313
- .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
314
- .replace(/` + '`' + String.raw `([^` + '`' + String.raw `\n]+)` + '`' + String.raw `/g, '<code>$1</code>');
315
- return text.replace(/(\d+)/g, (m, i) => blocks[Number(i)]);
316
- }
317
-
318
- const ms = (n) => n >= 1000 ? (n / 1000).toFixed(n >= 10000 ? 0 : 1) + 's' : Math.round(n) + 'ms';
319
- const num = (n) => n.toLocaleString();
320
-
321
- function renderTool(e) {
322
- const box = el('div', 'tool run');
323
- const th = el('div', 'th');
324
- th.appendChild(el('span', 'nm', e.name));
325
- th.appendChild(el('span', 'dt', e.detail || ''));
326
- th.appendChild(el('span', 'ms', '...'));
327
- const body = el('div', 'body');
328
- body.style.display = 'none';
329
- th.onclick = () => { body.style.display = body.style.display === 'none' ? 'block' : 'none'; };
330
- box.appendChild(th); box.appendChild(body);
331
- const entry = { box: box, th: th, body: body };
332
- state.toolEls.set(e.callId, entry);
333
- state.lastTool = entry;
334
- return add(box);
335
- }
336
-
337
- function finishTool(e) {
338
- const found = state.toolEls.get(e.callId) || state.lastTool;
339
- if (!found) return;
340
- found.box.className = 'tool ' + (e.ok ? 'ok' : 'bad');
341
- found.th.children[2].textContent = ms(e.ms);
342
- if (e.detail) found.th.children[1].textContent = e.detail;
343
- }
344
-
345
- function renderPlan(steps) {
346
- const done = steps.filter((s) => s.state === 'done').length;
347
- const box = el('div', 'plan');
348
- const h = el('h4');
349
- h.appendChild(el('span', null, 'Plan'));
350
- h.appendChild(el('span', null, done + '/' + steps.length));
351
- box.appendChild(h);
352
- for (const s of steps) {
353
- const row = el('div', 'step ' + s.state);
354
- const mark = s.state === 'done' ? 'v' : s.state === 'doing' ? '>' : s.state === 'blocked' ? 'x' : '.';
355
- row.appendChild(el('span', 'bx', mark));
356
- row.appendChild(el('span', null, s.text));
357
- box.appendChild(row);
358
- }
359
- if (state.planEl && state.planEl.parentNode) {
360
- state.planEl.replaceWith(box); state.planEl = box; return box;
361
- }
362
- state.planEl = box;
363
- return add(box);
364
- }
365
-
366
- function renderAsk(e) {
367
- const box = el('div', 'ask');
368
- box.appendChild(el('div', 'q', 'Allow ' + e.tool + '?'));
369
- box.appendChild(el('div', 'a', e.args));
370
- const btns = el('div', 'btns');
371
- const answer = async (allow) => {
372
- const all = btns.querySelectorAll('button');
373
- for (let i = 0; i < all.length; i++) all[i].disabled = true;
374
- await api('/api/answer', { method: 'POST',
375
- body: JSON.stringify({ session: state.session, ask: e.askId, allow: allow }) });
376
- box.appendChild(el('div', 'a', allow ? 'allowed' : 'refused'));
377
- };
378
- const yes = el('button', 'yes', 'Allow'); yes.onclick = () => answer(true);
379
- const no = el('button', 'no', 'Refuse'); no.onclick = () => answer(false);
380
- btns.appendChild(yes); btns.appendChild(no);
381
- box.appendChild(btns);
382
- return add(box);
383
- }
384
-
385
- let spinner = null;
386
- function setBusy(on) {
387
- state.busy = on;
388
- $('send').disabled = on || !$('ta').value.trim();
389
- $('pabort').style.display = on ? 'flex' : 'none';
390
- $('hintstate').textContent = on ? 'working...' : '';
391
- if (on && !spinner) {
392
- spinner = el('div', 'busy');
393
- spinner.appendChild(el('div', 'sp'));
394
- spinner.appendChild(el('span', null, 'working'));
395
- add(spinner);
396
- } else if (!on && spinner) { spinner.remove(); spinner = null; }
397
- }
398
-
399
- function onEvent(e) {
400
- switch (e.t) {
401
- case 'session':
402
- state.provider = e.provider; state.model = e.model;
403
- setPills(e.provider, e.model);
404
- $('wsname').textContent = e.cwd.split('/').pop() || e.cwd;
405
- break;
406
- case 'user': {
407
- clearHero();
408
- const box = el('div', 'msg');
409
- box.appendChild(el('div', 'who', 'you'));
410
- box.appendChild(el('div', 'you', e.text));
411
- add(box); state.replyEl = null; state.thinkEl = null;
412
- setBusy(true);
413
- break;
414
- }
415
- case 'chunk': {
416
- if (!state.replyEl) {
417
- const box = el('div', 'msg');
418
- const who = el('div', 'who');
419
- who.appendChild(el('span', 'kk', 'koneck'));
420
- const body = el('div', 'reply');
421
- box.appendChild(who); box.appendChild(body); add(box);
422
- state.replyEl = body; state.replyEl.dataset.raw = '';
423
- }
424
- state.replyEl.dataset.raw += e.text;
425
- const was = atBottom();
426
- state.replyEl.innerHTML = md(state.replyEl.dataset.raw);
427
- stick(was);
428
- break;
429
- }
430
- case 'thinking':
431
- if (!state.thinkEl) state.thinkEl = add(el('div', 'think', ''));
432
- state.thinkEl.textContent = (state.thinkEl.textContent + e.text).slice(-1400);
433
- break;
434
- case 'tool': clearHero(); renderTool(e); break;
435
- case 'tool-output': {
436
- const any = state.lastTool;
437
- if (any) any.body.textContent = (any.body.textContent + e.line + '\n').slice(-4000);
438
- break;
439
- }
440
- case 'tool-done': finishTool(e); break;
441
- case 'plan': renderPlan(e.steps); break;
442
- case 'tokens':
443
- $('stok').innerHTML = '<b>' + num(e.total) + '</b> tokens' +
444
- (e.cost > 0 ? ' &middot; $' + e.cost.toFixed(4) : '');
445
- break;
446
- case 'ask': renderAsk(e); break;
447
- case 'notice': add(el('div', 'notice ' + (e.level === 'info' ? 'info' : ''), e.text)); break;
448
- case 'error': add(el('div', 'err', e.text)); setBusy(false); break;
449
- case 'turn-end':
450
- setBusy(false);
451
- state.replyEl = null; state.thinkEl = null;
452
- break;
453
- }
454
- }
455
-
456
- function setPills(provider, model) {
457
- $('pprov').innerHTML = 'provider <b>' + escHtml(provider) + '</b>';
458
- $('pmodel').innerHTML = 'model <b>' + escHtml(model) + '</b>';
459
- }
460
- function escHtml(s) {
461
- return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
462
- }
463
-
464
- function listen() {
465
- if (state.es) state.es.close();
466
- const es = new EventSource('/api/events?session=' + encodeURIComponent(state.session) +
467
- '&token=' + encodeURIComponent(state.token));
468
- state.es = es;
469
- es.onopen = () => { $('conn').textContent = 'live'; $('conn').style.color = 'var(--green)'; };
470
- es.onerror = () => { $('conn').textContent = 'reconnecting'; $('conn').style.color = 'var(--amber)'; };
471
- es.onmessage = (m) => { try { onEvent(JSON.parse(m.data)); } catch (err) {} };
472
- }
473
-
474
- async function refreshSessions() {
475
- let data = null;
476
- try { data = await api('/api/sessions'); } catch (e) { return; }
477
- const list = $('slist');
478
- list.innerHTML = '';
479
- for (const s of data.live) {
480
- const row = el('div', 'srow' + (s.id === state.session ? ' on' : ''));
481
- row.appendChild(el('div', 't', s.title));
482
- const meta = el('div', 'm');
483
- meta.appendChild(el('span', 'dot' + (s.busy ? ' busy' : '')));
484
- meta.appendChild(el('span', null, s.model));
485
- meta.appendChild(el('span', null, s.turns + ' turns'));
486
- row.appendChild(meta);
487
- row.onclick = () => { openSession(s.id); };
488
- list.appendChild(row);
489
- }
490
- if (!data.live.length) list.appendChild(el('div', 'shead', 'none yet'));
491
- }
492
-
493
- function resetView() {
494
- stream().innerHTML = '';
495
- state.planEl = null; state.replyEl = null; state.thinkEl = null; state.lastTool = null;
496
- state.toolEls = new Map();
497
- }
498
-
499
- function openSession(id) {
500
- state.session = id;
501
- resetView();
502
- listen();
503
- refreshSessions();
504
- }
505
-
506
- async function newSession() {
507
- const s = await api('/api/sessions', { method: 'POST', body: JSON.stringify({}) });
508
- openSession(s.id);
509
- }
510
-
511
- async function send() {
512
- const ta = $('ta');
513
- const text = ta.value.trim();
514
- if (!text || state.busy) return;
515
- if (!state.session) await newSession();
516
- ta.value = ''; ta.style.height = 'auto';
517
- $('send').disabled = true;
518
- try {
519
- await api('/api/send', { method: 'POST',
520
- body: JSON.stringify({ session: state.session, text: text }) });
521
- } catch (e) { add(el('div', 'err', e.message)); setBusy(false); }
522
- }
523
-
524
- $('ta').addEventListener('input', (ev) => {
525
- const ta = ev.target;
526
- ta.style.height = 'auto';
527
- ta.style.height = Math.min(ta.scrollHeight, 190) + 'px';
528
- $('send').disabled = state.busy || !ta.value.trim();
529
- });
530
- $('ta').addEventListener('keydown', (ev) => {
531
- if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); send(); }
532
- });
533
- $('send').onclick = send;
534
- $('new').onclick = newSession;
535
- $('pabort').onclick = () => {
536
- api('/api/abort', { method: 'POST', body: JSON.stringify({ session: state.session }) })
537
- .catch(() => {});
538
- };
539
- $('toksave').onclick = () => {
540
- const v = $('tokin').value.trim();
541
- if (v) { localStorage.setItem('koneck.token', v); location.reload(); }
542
- };
543
- $('pmodel').onclick = async () => {
544
- let info = null;
545
- try { info = await api('/api/workspaces'); } catch (e) { return; }
546
- const sel = $('mprov');
547
- sel.innerHTML = '';
548
- for (const p of info.providers) {
549
- const o = document.createElement('option');
550
- o.value = p.name;
551
- o.textContent = p.label + (p.keyless ? ' - local' : '');
552
- if (p.name === state.provider) o.selected = true;
553
- sel.appendChild(o);
554
- }
555
- $('mmodel').value = state.model === '-' ? info.model : state.model;
556
- $('mdlg').showModal();
557
- };
558
- $('msave').onclick = async () => {
559
- const provider = $('mprov').value, model = $('mmodel').value.trim();
560
- if (!state.session) await newSession();
561
- let info = null;
562
- try {
563
- info = await api('/api/model', { method: 'POST',
564
- body: JSON.stringify({ session: state.session, provider: provider, model: model }) });
565
- } catch (e) { return; }
566
- state.provider = info.provider; state.model = info.model;
567
- setPills(info.provider, info.model);
568
- };
569
-
570
- (async function start() {
571
- let hello = null;
572
- try { hello = await fetch('/api/hello').then((r) => r.json()); } catch (e) {}
573
- if (hello && hello.needsToken && !state.token) $('tokdlg').showModal();
574
- let info = null;
575
- try { info = await api('/api/workspaces'); } catch (e) {}
576
- if (info) {
577
- state.provider = info.provider; state.model = info.model;
578
- setPills(info.provider, info.model);
579
- $('wsname').textContent = info.cwd.split('/').pop() || info.cwd;
580
- }
581
- await refreshSessions();
582
- setInterval(refreshSessions, 4000);
583
- })();
584
- `;
585
- }
586
145
  //# sourceMappingURL=ui.js.map