kward 0.69.1 → 0.71.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +1 -1
  3. data/CHANGELOG.md +68 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +90 -2
  6. data/README.md +30 -6
  7. data/Rakefile +96 -0
  8. data/doc/agent-tools.md +43 -0
  9. data/doc/api.md +92 -0
  10. data/doc/authentication.md +39 -25
  11. data/doc/configuration.md +2 -16
  12. data/doc/context-tools.md +70 -0
  13. data/doc/getting-started.md +3 -1
  14. data/doc/plugins.md +2 -2
  15. data/doc/releasing.md +14 -5
  16. data/doc/rpc.md +3 -11
  17. data/doc/session-management.md +220 -0
  18. data/doc/usage.md +13 -7
  19. data/doc/workspace-tools.md +105 -0
  20. data/lib/kward/cli/commands.rb +8 -0
  21. data/lib/kward/cli/openrouter_commands.rb +55 -0
  22. data/lib/kward/cli/prompt_interface.rb +85 -7
  23. data/lib/kward/cli/rendering.rb +11 -6
  24. data/lib/kward/cli/sessions.rb +454 -15
  25. data/lib/kward/cli/settings.rb +0 -30
  26. data/lib/kward/cli/slash_commands.rb +38 -11
  27. data/lib/kward/cli.rb +14 -0
  28. data/lib/kward/compactor.rb +4 -1
  29. data/lib/kward/config_files.rb +4 -6
  30. data/lib/kward/conversation.rb +49 -5
  31. data/lib/kward/model/client.rb +37 -50
  32. data/lib/kward/model/context_usage.rb +13 -6
  33. data/lib/kward/model/model_info.rb +92 -9
  34. data/lib/kward/model/payloads.rb +2 -0
  35. data/lib/kward/openrouter_model_cache.rb +120 -0
  36. data/lib/kward/plugin_registry.rb +47 -1
  37. data/lib/kward/prompt_interface/banner.rb +16 -51
  38. data/lib/kward/prompt_interface/composer_controller.rb +60 -87
  39. data/lib/kward/prompt_interface/composer_renderer.rb +7 -1
  40. data/lib/kward/prompt_interface/key_handler.rb +31 -10
  41. data/lib/kward/prompt_interface/layout.rb +2 -2
  42. data/lib/kward/prompt_interface/overlay_renderer.rb +24 -0
  43. data/lib/kward/prompt_interface/prompt_renderer.rb +23 -2
  44. data/lib/kward/prompt_interface/question_prompt.rb +34 -42
  45. data/lib/kward/prompt_interface/runtime_state.rb +6 -1
  46. data/lib/kward/prompt_interface/screen.rb +10 -4
  47. data/lib/kward/prompt_interface/selection_prompt.rb +518 -61
  48. data/lib/kward/prompt_interface/slash_overlay.rb +4 -4
  49. data/lib/kward/prompt_interface/transcript_buffer.rb +7 -16
  50. data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
  51. data/lib/kward/prompt_interface.rb +31 -32
  52. data/lib/kward/prompts/commands.rb +6 -3
  53. data/lib/kward/prompts.rb +2 -2
  54. data/lib/kward/rpc/server.rb +3 -8
  55. data/lib/kward/rpc/session_manager.rb +19 -8
  56. data/lib/kward/session_diff.rb +106 -9
  57. data/lib/kward/session_store.rb +23 -4
  58. data/lib/kward/session_tree_renderer.rb +2 -1
  59. data/lib/kward/telemetry/logger.rb +5 -3
  60. data/lib/kward/tool_output_compactor.rb +127 -0
  61. data/lib/kward/tools/base.rb +8 -2
  62. data/lib/kward/tools/registry.rb +37 -6
  63. data/lib/kward/tools/retrieve_tool_output.rb +71 -0
  64. data/lib/kward/tools/search/web.rb +2 -2
  65. data/lib/kward/tools/summarize_file_structure.rb +29 -0
  66. data/lib/kward/tools/tool_call.rb +2 -0
  67. data/lib/kward/version.rb +1 -1
  68. data/lib/kward/workspace.rb +58 -2
  69. data/templates/default/fulldoc/html/css/kward.css +570 -78
  70. data/templates/default/fulldoc/html/full_list.erb +107 -0
  71. data/templates/default/fulldoc/html/js/kward.js +259 -97
  72. data/templates/default/fulldoc/html/setup.rb +8 -0
  73. data/templates/default/kward_navigation.rb +91 -0
  74. data/templates/default/layout/html/layout.erb +59 -13
  75. data/templates/default/layout/html/setup.rb +34 -39
  76. metadata +13 -3
  77. data/lib/kward/resources/avatar_kward_logo.rb +0 -50
  78. data/lib/kward/resources/pixel_logo.rb +0 -232
@@ -0,0 +1,107 @@
1
+ <!DOCTYPE html>
2
+ <html <%= "lang=\"#{html_lang}\"" unless html_lang.nil? %>>
3
+ <head>
4
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
5
+ <meta charset="<%= charset %>">
6
+ <% stylesheets_full_list.each do |stylesheet| %>
7
+ <link rel="stylesheet" href="<%= mtime_url(stylesheet) %>" type="text/css" media="screen">
8
+ <% end %>
9
+
10
+ <% javascripts_full_list.each do |javascript| %>
11
+ <script type="text/javascript" charset="utf-8" src="<%= mtime_url(javascript) %>"></script>
12
+ <% end %>
13
+
14
+ <title><%= @list_title %></title>
15
+ <base id="base_target" target="_parent">
16
+ </head>
17
+ <body class="kward-docs kward-content-body kward-full-list-body">
18
+ <a href="#content" class="kward-skip-link">Skip to content</a>
19
+
20
+ <header class="kward-topbar">
21
+ <a class="kward-brand" href="index.html">
22
+ <img class="kward-brand-logo" src="images/kward_logo.png" alt="" aria-hidden="true">
23
+ <span>
24
+ <strong>Kward</strong>
25
+ </span>
26
+ </a>
27
+ <button class="kward-nav-toggle" type="button" aria-expanded="false" aria-controls="kward-primary-nav" aria-label="Toggle navigation menu" onclick="document.body.classList.toggle('kward-nav-open'); this.setAttribute('aria-expanded', document.body.classList.contains('kward-nav-open'))">
28
+ <span class="kward-nav-toggle-bar"></span>
29
+ <span class="kward-nav-toggle-bar"></span>
30
+ <span class="kward-nav-toggle-bar"></span>
31
+ <span class="kward-sr-only">Menu</span>
32
+ </button>
33
+ <nav id="kward-primary-nav" class="kward-topnav" aria-label="Primary navigation">
34
+ <a href="index.html">Home</a>
35
+ <div class="kward-nav-menu">
36
+ <a class="kward-nav-menu-link" href="file.README.html">Guides</a>
37
+ <button class="kward-nav-menu-button" type="button" aria-label="Expand guides" aria-expanded="false">⌄</button>
38
+ <div class="kward-nav-dropdown">
39
+ <% guide_groups.each do |title, items| %>
40
+ <section>
41
+ <h2><%= h title %></h2>
42
+ <% items.each do |label, link| %>
43
+ <a href="<%= url_for(link) %>"><%= h label %></a>
44
+ <% end %>
45
+ </section>
46
+ <% end %>
47
+ </div>
48
+ </div>
49
+ <div class="kward-nav-menu">
50
+ <a class="kward-nav-menu-link" href="file.extensibility.html">Advanced</a>
51
+ <button class="kward-nav-menu-button" type="button" aria-label="Expand advanced guides" aria-expanded="false">⌄</button>
52
+ <div class="kward-nav-dropdown kward-advanced-nav-dropdown">
53
+ <% extension_groups.each do |title, items| %>
54
+ <section>
55
+ <h2><%= h title %></h2>
56
+ <% items.each do |label, link| %>
57
+ <a href="<%= url_for(link) %>"><%= h label %></a>
58
+ <% end %>
59
+ </section>
60
+ <% end %>
61
+ </div>
62
+ </div>
63
+ <div class="kward-nav-menu active">
64
+ <a class="kward-nav-menu-link" href="file.api.html">API Docs</a>
65
+ <button class="kward-nav-menu-button" type="button" aria-label="Expand API docs" aria-expanded="false">⌄</button>
66
+ <div class="kward-nav-dropdown kward-api-nav-dropdown">
67
+ <% api_groups.each do |title, items| %>
68
+ <section>
69
+ <h2><%= h title %></h2>
70
+ <% items.each do |label, link| %>
71
+ <a href="<%= url_for(link) %>"><%= h label %></a>
72
+ <% end %>
73
+ </section>
74
+ <% end %>
75
+ </div>
76
+ </div>
77
+ </nav>
78
+ <a class="kward-api-index-link" href="class_list.html">Search API index</a>
79
+ </header>
80
+
81
+ <main class="kward-page">
82
+ <div id="content" class="kward-content-page kward-api-page">
83
+ <div class="fixed_header">
84
+ <h1 id="full_list_header"><%= @list_title %></h1>
85
+ <div id="full_list_nav">
86
+ <% menu_lists.each do |list| %>
87
+ <span><a target="_self" href="<%= url_for_list list[:type] %>">
88
+ <%= list[:title] %>
89
+ </a></span>
90
+ <% end %>
91
+ </div>
92
+
93
+ <div id="search">
94
+ <label for="search-class">Search:</label>
95
+ <input id="search-class" type="text">
96
+ </div>
97
+ </div>
98
+
99
+ <ul id="full_list" class="<%= @list_class || @list_type %>">
100
+ <%= erb "full_list_#{@list_type}" %>
101
+ </ul>
102
+ </div>
103
+ </main>
104
+
105
+ <script type="text/javascript" charset="utf-8" src="js/kward.js"></script>
106
+ </body>
107
+ </html>
@@ -1,47 +1,94 @@
1
- const resetScrollPosition = () => {
2
- if (window.location.hash) return
3
-
4
- document.documentElement.scrollTop = 0
5
- if (document.body) document.body.scrollTop = 0
6
- window.scrollTo(0, 0)
7
-
8
- const main = document.getElementById('main')
9
- if (main) {
10
- main.scrollTop = 0
11
- main.scrollLeft = 0
12
- }
13
- }
14
-
15
- if (!window.location.hash && 'scrollRestoration' in history) {
16
- history.scrollRestoration = 'manual'
17
- }
18
-
19
- resetScrollPosition()
20
- window.addEventListener('load', resetScrollPosition)
21
- window.addEventListener('pageshow', resetScrollPosition)
22
- window.addEventListener('DOMContentLoaded', resetScrollPosition)
23
- window.setTimeout(resetScrollPosition, 0)
24
- window.setTimeout(resetScrollPosition, 50)
25
- window.setTimeout(resetScrollPosition, 250)
26
- if (window.requestAnimationFrame) window.requestAnimationFrame(resetScrollPosition)
27
-
1
+ (() => {
28
2
  const guideLinks = {
29
3
  'doc/getting-started.md': 'file.getting-started.html',
30
4
  'doc/usage.md': 'file.usage.html',
31
5
  'doc/configuration.md': 'file.configuration.html',
32
6
  'doc/authentication.md': 'file.authentication.html',
33
7
  'doc/troubleshooting.md': 'file.troubleshooting.html',
8
+ 'doc/session-management.md': 'file.session-management.html',
34
9
  'doc/memory.md': 'file.memory.html',
35
10
  'doc/personas.md': 'file.personas.html',
36
11
  'doc/extensibility.md': 'file.extensibility.html',
37
12
  'doc/plugins.md': 'file.plugins.html',
13
+ 'doc/agent-tools.md': 'file.agent-tools.html',
14
+ 'doc/workspace-tools.md': 'file.workspace-tools.html',
38
15
  'doc/web-search.md': 'file.web-search.html',
39
16
  'doc/code-search.md': 'file.code-search.html',
17
+ 'doc/context-tools.md': 'file.context-tools.html',
40
18
  'doc/rpc.md': 'file.rpc.html',
41
19
  'doc/releasing.md': 'file.releasing.html'
42
20
  }
43
21
 
44
22
  let pageController = null
23
+ let navigating = false
24
+
25
+ const scrollToCurrentHash = () => {
26
+ if (!window.location.hash) {
27
+ window.scrollTo({ top: 0 })
28
+ return
29
+ }
30
+
31
+ const id = decodeURIComponent(window.location.hash.slice(1))
32
+ const target = document.getElementById(id) || document.getElementsByName(id)[0]
33
+ if (target) target.scrollIntoView()
34
+ }
35
+
36
+ const replacePage = async (url, pushState = true) => {
37
+ if (navigating) return
38
+ navigating = true
39
+ document.documentElement.classList.add('kward-navigating')
40
+
41
+ try {
42
+ const response = await fetch(url, { headers: { 'X-Requested-With': 'Kward-Docs' } })
43
+ if (!response.ok) throw new Error(`Navigation failed with ${response.status}`)
44
+
45
+ const html = await response.text()
46
+ const nextDocument = new DOMParser().parseFromString(html, 'text/html')
47
+ if (!nextDocument.body) throw new Error('Navigation response did not include a body')
48
+
49
+ document.title = nextDocument.title
50
+ document.body.className = nextDocument.body.className
51
+ document.body.innerHTML = nextDocument.body.innerHTML
52
+
53
+ if (pushState) window.history.pushState({ kwardDocs: true }, '', url)
54
+ initializePage()
55
+ scrollToCurrentHash()
56
+ } catch (_error) {
57
+ window.location.href = url
58
+ } finally {
59
+ navigating = false
60
+ document.documentElement.classList.remove('kward-navigating')
61
+ }
62
+ }
63
+
64
+ const isNavigableLink = (event, link) => {
65
+ if (event.defaultPrevented || event.button !== 0) return false
66
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false
67
+ if (link.target && link.target !== '_self') return false
68
+ if (link.download || link.dataset.noTurbo) return false
69
+
70
+ const url = new URL(link.href, window.location.href)
71
+ if (url.origin !== window.location.origin) return false
72
+ if (url.pathname === window.location.pathname && url.search === window.location.search) return false
73
+
74
+ return url.pathname === '/' || url.pathname.endsWith('/') || url.pathname.endsWith('.html')
75
+ }
76
+
77
+ const visitLink = (link) => replacePage(link.href)
78
+
79
+ const setupTurbolinks = (signal) => {
80
+ document.addEventListener('click', (event) => {
81
+ const link = event.target.closest('a[href]')
82
+ if (!link || !isNavigableLink(event, link)) return
83
+
84
+ event.preventDefault()
85
+ visitLink(link)
86
+ }, { signal })
87
+
88
+ window.addEventListener('popstate', () => {
89
+ replacePage(window.location.href, false)
90
+ }, { signal })
91
+ }
45
92
 
46
93
  const setupGuideSearch = (signal) => {
47
94
  const form = document.querySelector('.kward-guide-search')
@@ -58,9 +105,12 @@ const setupGuideSearch = (signal) => {
58
105
  return
59
106
  }
60
107
 
108
+ let selectedIndex = -1
109
+
61
110
  const closeResults = () => {
62
111
  results.classList.remove('open')
63
112
  results.innerHTML = ''
113
+ selectedIndex = -1
64
114
  }
65
115
 
66
116
  const excerpt = (text, query) => {
@@ -98,6 +148,18 @@ const setupGuideSearch = (signal) => {
98
148
  .map((result) => result.item)
99
149
  }
100
150
 
151
+ const updateActiveItem = () => {
152
+ const items = results.querySelectorAll('a')
153
+ items.forEach((item, i) => {
154
+ if (i === selectedIndex) {
155
+ item.classList.add('kward-search-active')
156
+ item.scrollIntoView({ block: 'nearest' })
157
+ } else {
158
+ item.classList.remove('kward-search-active')
159
+ }
160
+ })
161
+ }
162
+
101
163
  const renderResults = () => {
102
164
  const query = input.value.trim()
103
165
  if (query.length < 2) {
@@ -107,6 +169,7 @@ const setupGuideSearch = (signal) => {
107
169
 
108
170
  const matches = search(query)
109
171
  results.innerHTML = ''
172
+ selectedIndex = -1
110
173
 
111
174
  if (matches.length === 0) {
112
175
  const empty = document.createElement('div')
@@ -139,17 +202,37 @@ const setupGuideSearch = (signal) => {
139
202
  input.addEventListener('input', renderResults, { signal })
140
203
 
141
204
  input.addEventListener('keydown', (event) => {
205
+ const items = results.querySelectorAll('a')
206
+
142
207
  if (event.key === 'Escape') {
143
208
  input.value = ''
144
209
  closeResults()
145
210
  input.blur()
211
+ return
212
+ }
213
+
214
+ if (items.length === 0) return
215
+
216
+ if (event.key === 'ArrowDown') {
217
+ event.preventDefault()
218
+ selectedIndex = Math.min(selectedIndex + 1, items.length - 1)
219
+ updateActiveItem()
220
+ } else if (event.key === 'ArrowUp') {
221
+ event.preventDefault()
222
+ selectedIndex = Math.max(selectedIndex - 1, 0)
223
+ updateActiveItem()
224
+ } else if (event.key === 'Enter') {
225
+ if (selectedIndex >= 0 && items[selectedIndex]) {
226
+ event.preventDefault()
227
+ visitLink(items[selectedIndex])
228
+ }
146
229
  }
147
230
  }, { signal })
148
231
 
149
232
  form.addEventListener('submit', (event) => {
150
233
  event.preventDefault()
151
234
  const firstResult = results.querySelector('a')
152
- if (firstResult) visitPage(firstResult.href)
235
+ if (firstResult) visitLink(firstResult)
153
236
  }, { signal })
154
237
 
155
238
  document.addEventListener('click', (event) => {
@@ -159,26 +242,80 @@ const setupGuideSearch = (signal) => {
159
242
 
160
243
  const setupNavigation = (signal) => {
161
244
  const toggle = document.querySelector('.kward-nav-toggle')
245
+ const nav = document.getElementById('kward-primary-nav')
246
+
247
+ const closeMenu = () => {
248
+ document.body.classList.remove('kward-nav-open')
249
+ if (toggle) toggle.setAttribute('aria-expanded', 'false')
250
+ document.querySelectorAll('.kward-nav-menu.open').forEach((menu) => {
251
+ menu.classList.remove('open')
252
+ const btn = menu.querySelector('.kward-nav-menu-button')
253
+ if (btn) btn.setAttribute('aria-expanded', 'false')
254
+ })
255
+ }
162
256
 
163
257
  if (toggle) {
258
+ // The inline onclick handles the toggle. We only need to close
259
+ // sub-menus when the main menu closes.
164
260
  toggle.addEventListener('click', () => {
165
- const isOpen = document.body.classList.toggle('kward-nav-open')
166
- toggle.setAttribute('aria-expanded', String(isOpen))
261
+ if (!document.body.classList.contains('kward-nav-open')) {
262
+ document.querySelectorAll('.kward-nav-menu.open').forEach((menu) => {
263
+ menu.classList.remove('open')
264
+ const btn = menu.querySelector('.kward-nav-menu-button')
265
+ if (btn) btn.setAttribute('aria-expanded', 'false')
266
+ })
267
+ }
167
268
  }, { signal })
168
269
  }
169
270
 
170
271
  document.querySelectorAll('.kward-nav-menu-button').forEach((button) => {
171
272
  button.addEventListener('click', (event) => {
172
273
  event.stopPropagation()
173
- button.parentElement.classList.toggle('open')
274
+ const menu = button.parentElement
275
+ const wasOpen = menu.classList.contains('open')
276
+ document.querySelectorAll('.kward-nav-menu.open').forEach((m) => {
277
+ m.classList.remove('open')
278
+ const b = m.querySelector('.kward-nav-menu-button')
279
+ if (b) b.setAttribute('aria-expanded', 'false')
280
+ })
281
+ if (!wasOpen) {
282
+ menu.classList.add('open')
283
+ button.setAttribute('aria-expanded', 'true')
284
+ }
174
285
  }, { signal })
175
286
  })
176
287
 
288
+ // Close menu when a nav link is clicked (mobile navigation)
289
+ if (nav) {
290
+ nav.addEventListener('click', (event) => {
291
+ const link = event.target.closest('a[href]')
292
+ if (link) closeMenu()
293
+ }, { signal })
294
+ }
295
+
296
+ // Close on outside click
177
297
  document.addEventListener('click', (event) => {
298
+ if (document.body.classList.contains('kward-nav-open')) {
299
+ if (nav && !nav.contains(event.target) && toggle && !toggle.contains(event.target)) {
300
+ closeMenu()
301
+ }
302
+ }
178
303
  document.querySelectorAll('.kward-nav-menu.open').forEach((menu) => {
179
- if (!menu.contains(event.target)) menu.classList.remove('open')
304
+ if (!menu.contains(event.target)) {
305
+ menu.classList.remove('open')
306
+ const btn = menu.querySelector('.kward-nav-menu-button')
307
+ if (btn) btn.setAttribute('aria-expanded', 'false')
308
+ }
180
309
  })
181
310
  }, { signal })
311
+
312
+ // Close on Escape
313
+ document.addEventListener('keydown', (event) => {
314
+ if (event.key === 'Escape' && document.body.classList.contains('kward-nav-open')) {
315
+ closeMenu()
316
+ if (toggle) toggle.focus()
317
+ }
318
+ }, { signal })
182
319
  }
183
320
 
184
321
  const rewriteGuideLinks = () => {
@@ -188,6 +325,90 @@ const rewriteGuideLinks = () => {
188
325
  })
189
326
  }
190
327
 
328
+ const setupTableOfContents = () => {
329
+ const existingToc = document.getElementById('toc')
330
+ if (document.querySelector('.kward-no-toc')) {
331
+ if (existingToc) existingToc.remove()
332
+ return
333
+ }
334
+
335
+ const fileContents = document.getElementById('filecontents')
336
+ const content = document.getElementById('content')
337
+ if (!fileContents || !content || content.querySelector('#toc')) return
338
+
339
+ const headingTags = ['h2', 'h3', 'h4', 'h5', 'h6']
340
+ if (fileContents.querySelectorAll('h1').length > 1) headingTags.unshift('h1')
341
+
342
+ const headings = Array.from(fileContents.querySelectorAll(headingTags.join(', ')))
343
+ .filter((heading) => !heading.closest('.method_details .docstring') && heading.id !== 'filecontents')
344
+ if (headings.length === 0) return
345
+
346
+ const topLevel = document.createElement('ol')
347
+ topLevel.className = 'top'
348
+
349
+ let currentList = topLevel
350
+ let currentItem = null
351
+ let counter = 0
352
+ let lastLevel = parseInt(headingTags[0].slice(1), 10)
353
+
354
+ headings.forEach((heading) => {
355
+ const level = parseInt(heading.tagName.slice(1), 10)
356
+
357
+ if (!heading.id) {
358
+ let proposedId = heading.getAttribute('toc-id')
359
+ if (!proposedId) {
360
+ proposedId = heading.textContent.replace(/[^a-z0-9-]/gi, '_')
361
+ if (document.getElementById(proposedId)) {
362
+ proposedId += counter
363
+ counter += 1
364
+ }
365
+ }
366
+ heading.id = proposedId
367
+ }
368
+
369
+ if (level > lastLevel) {
370
+ while (level > lastLevel) {
371
+ if (!currentItem) {
372
+ currentItem = document.createElement('li')
373
+ currentList.appendChild(currentItem)
374
+ }
375
+ const nestedList = document.createElement('ol')
376
+ currentItem.appendChild(nestedList)
377
+ currentList = nestedList
378
+ currentItem = null
379
+ lastLevel += 1
380
+ }
381
+ } else if (level < lastLevel) {
382
+ while (level < lastLevel && currentList.parentElement) {
383
+ currentList = currentList.parentElement.parentElement
384
+ lastLevel -= 1
385
+ }
386
+ }
387
+
388
+ const title = heading.getAttribute('toc-title') || heading.textContent
389
+ const item = document.createElement('li')
390
+ const link = document.createElement('a')
391
+ link.href = `#${heading.id}`
392
+ link.textContent = title
393
+ item.appendChild(link)
394
+ currentList.appendChild(item)
395
+ currentItem = item
396
+ })
397
+
398
+ const toc = document.createElement('div')
399
+ toc.id = 'toc'
400
+ toc.innerHTML = '<p class="title hide_toc"><a href="#"><strong>Table of Contents</strong></a></p>'
401
+ toc.appendChild(topLevel)
402
+ content.insertBefore(toc, content.firstChild)
403
+
404
+ const hideLink = toc.querySelector('.hide_toc')
405
+ hideLink.addEventListener('click', (event) => {
406
+ event.preventDefault()
407
+ const hidden = toc.classList.toggle('hidden')
408
+ topLevel.style.display = hidden ? 'none' : ''
409
+ })
410
+ }
411
+
191
412
  const setupCodeCopy = () => {
192
413
  document.querySelectorAll('pre').forEach((block) => {
193
414
  if (block.closest('.code-copy-wrapper')) return
@@ -221,76 +442,17 @@ const initializePage = () => {
221
442
  if (pageController) pageController.abort()
222
443
  pageController = new AbortController()
223
444
 
224
- resetScrollPosition()
445
+ setupTurbolinks(pageController.signal)
225
446
  setupGuideSearch(pageController.signal)
226
447
  setupNavigation(pageController.signal)
227
448
  rewriteGuideLinks()
449
+ setupTableOfContents()
228
450
  setupCodeCopy()
229
451
  }
230
452
 
231
- const samePageUrl = (url) => {
232
- return url.origin === window.location.origin &&
233
- url.pathname === window.location.pathname &&
234
- url.search === window.location.search
235
- }
236
-
237
- const navigableUrl = (url) => {
238
- if (url.hash) return false
239
- if (url.origin !== window.location.origin) return false
240
- if (!url.pathname.endsWith('.html') && !url.pathname.endsWith('/')) return false
241
- return !samePageUrl(url)
242
- }
243
-
244
- const replacePage = (html, url) => {
245
- const nextDocument = new DOMParser().parseFromString(html, 'text/html')
246
- const nextBody = nextDocument.body
247
- if (!nextBody) throw new Error('Missing response body')
248
-
249
- document.title = nextDocument.title
250
- document.body.className = nextBody.className
251
- document.body.innerHTML = nextBody.innerHTML
252
- window.history.pushState({}, '', url.href)
453
+ if (document.readyState === 'loading') {
454
+ document.addEventListener('DOMContentLoaded', initializePage, { once: true })
455
+ } else {
253
456
  initializePage()
254
457
  }
255
-
256
- const visitPage = async (href) => {
257
- const url = new URL(href, window.location.href)
258
-
259
- if (!navigableUrl(url)) {
260
- window.location.href = url.href
261
- return
262
- }
263
-
264
- document.documentElement.classList.add('kward-page-loading')
265
-
266
- try {
267
- const response = await fetch(url.href)
268
- if (!response.ok) throw new Error(`Failed to load ${url.href}`)
269
-
270
- const html = await response.text()
271
- replacePage(html, url)
272
- } catch (_error) {
273
- window.location.href = url.href
274
- } finally {
275
- document.documentElement.classList.remove('kward-page-loading')
276
- }
277
- }
278
-
279
- document.addEventListener('click', (event) => {
280
- if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
281
-
282
- const link = event.target.closest('a[href]')
283
- if (!link || link.target || link.hasAttribute('download')) return
284
-
285
- const url = new URL(link.href, window.location.href)
286
- if (!navigableUrl(url)) return
287
-
288
- event.preventDefault()
289
- visitPage(url.href)
290
- })
291
-
292
- window.addEventListener('popstate', () => {
293
- window.location.reload()
294
- })
295
-
296
- document.addEventListener('DOMContentLoaded', initializePage)
458
+ })()
@@ -1,8 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../kward_navigation"
4
+
5
+ include KwardDocsNavigationData
6
+
3
7
  def generate_assets
4
8
  super
5
9
  asset('css/kward.css', file('css/kward.css', true))
6
10
  asset('js/kward.js', file('js/kward.js', true))
7
11
  asset('images/kward_logo.png', file('images/kward_logo.png', true))
8
12
  end
13
+
14
+ def stylesheets_full_list
15
+ super + %w(css/kward.css)
16
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KwardDocsNavigationData
4
+ GUIDE_GROUPS = [
5
+ [
6
+ "Start here",
7
+ [
8
+ ["Getting started", "file.getting-started.html"],
9
+ ["Usage", "file.usage.html"],
10
+ ["Configuration", "file.configuration.html"],
11
+ ["Authentication", "file.authentication.html"],
12
+ ["Troubleshooting", "file.troubleshooting.html"]
13
+ ]
14
+ ],
15
+ [
16
+ "Feature guides",
17
+ [
18
+ ["Sessions", "file.session-management.html"],
19
+ ["Memory", "file.memory.html"],
20
+ ["Personas", "file.personas.html"]
21
+ ]
22
+ ]
23
+ ].freeze
24
+
25
+ EXTENSION_GROUPS = [
26
+ [
27
+ "Customize",
28
+ [
29
+ ["Extensibility", "file.extensibility.html"],
30
+ ["Plugins", "file.plugins.html"]
31
+ ]
32
+ ],
33
+ [
34
+ "Integrate",
35
+ [
36
+ ["RPC protocol", "file.rpc.html"],
37
+ ["Releasing", "file.releasing.html"]
38
+ ]
39
+ ],
40
+ [
41
+ "Agent tools",
42
+ [
43
+ ["Overview", "file.agent-tools.html"],
44
+ ["Workspace tools", "file.workspace-tools.html"],
45
+ ["Web search", "file.web-search.html"],
46
+ ["Code search", "file.code-search.html"],
47
+ ["Context tools", "file.context-tools.html"]
48
+ ]
49
+ ]
50
+ ].freeze
51
+
52
+ API_OVERVIEW = "file.api.html"
53
+ API_GROUPS = [
54
+ [
55
+ "Reference",
56
+ [
57
+ ["Overview", API_OVERVIEW],
58
+ ["Classes & Modules", "class_list.html"],
59
+ ["Methods", "method_list.html"],
60
+ ["Files", "file_list.html"]
61
+ ]
62
+ ],
63
+ [
64
+ "Key namespaces",
65
+ [
66
+ ["Kward", "Kward.html"],
67
+ ["Tools", "Kward/Tools.html"],
68
+ ["RPC", "Kward/RPC.html"],
69
+ ["CLI", "Kward/CLI.html"],
70
+ ["Prompts", "Kward/Prompts.html"],
71
+ ["Skills", "Kward/Skills.html"]
72
+ ]
73
+ ]
74
+ ].freeze
75
+
76
+ def guide_groups
77
+ GUIDE_GROUPS
78
+ end
79
+
80
+ def extension_groups
81
+ EXTENSION_GROUPS
82
+ end
83
+
84
+ def api_groups
85
+ API_GROUPS
86
+ end
87
+
88
+ def api_overview
89
+ API_OVERVIEW
90
+ end
91
+ end