openclacky 1.3.2 → 1.3.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/Dockerfile +3 -0
- data/README.md +1 -1
- data/README_JA.md +237 -0
- data/lib/clacky/agent/session_serializer.rb +49 -5
- data/lib/clacky/agent/time_machine.rb +247 -26
- data/lib/clacky/agent.rb +12 -1
- data/lib/clacky/agent_config.rb +14 -2
- data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
- data/lib/clacky/default_agents/coding/profile.yml +3 -0
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
- data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
- data/lib/clacky/media/openai_compat.rb +64 -1
- data/lib/clacky/media/output_dir.rb +43 -0
- data/lib/clacky/message_history.rb +9 -0
- data/lib/clacky/server/channel/channel_manager.rb +26 -0
- data/lib/clacky/server/git_panel.rb +115 -0
- data/lib/clacky/server/http_server.rb +497 -12
- data/lib/clacky/server/server_master.rb +6 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +473 -60
- data/lib/clacky/web/app.js +30 -7
- data/lib/clacky/web/components/code-editor.js +197 -0
- data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
- data/lib/clacky/web/core/aside.js +112 -0
- data/lib/clacky/web/core/ext.js +387 -0
- data/lib/clacky/web/features/backup/store.js +92 -0
- data/lib/clacky/web/features/backup/view.js +94 -0
- data/lib/clacky/web/features/billing/store.js +163 -0
- data/lib/clacky/web/{billing.js → features/billing/view.js} +132 -240
- data/lib/clacky/web/features/brand/store.js +110 -0
- data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
- data/lib/clacky/web/features/channels/store.js +103 -0
- data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
- data/lib/clacky/web/features/creator/store.js +81 -0
- data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
- data/lib/clacky/web/features/mcp/store.js +158 -0
- data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
- data/lib/clacky/web/features/model-tester/store.js +77 -0
- data/lib/clacky/web/features/model-tester/view.js +7 -0
- data/lib/clacky/web/features/profile/store.js +170 -0
- data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
- data/lib/clacky/web/features/share/store.js +145 -0
- data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
- data/lib/clacky/web/features/skills/store.js +303 -0
- data/lib/clacky/web/features/skills/view.js +550 -0
- data/lib/clacky/web/features/tasks/store.js +135 -0
- data/lib/clacky/web/features/tasks/view.js +241 -0
- data/lib/clacky/web/features/trash/store.js +242 -0
- data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
- data/lib/clacky/web/features/version/store.js +165 -0
- data/lib/clacky/web/features/version/view.js +323 -0
- data/lib/clacky/web/features/workspace/store.js +99 -0
- data/lib/clacky/web/features/workspace/view.js +305 -0
- data/lib/clacky/web/i18n.js +56 -6
- data/lib/clacky/web/index.html +117 -58
- data/lib/clacky/web/sessions.js +221 -25
- data/lib/clacky/web/settings.js +118 -22
- data/lib/clacky/web/skills.js +3 -863
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
- data/lib/clacky.rb +1 -0
- metadata +45 -20
- data/lib/clacky/web/backup.js +0 -119
- data/lib/clacky/web/model-tester.js +0 -66
- data/lib/clacky/web/tasks.js +0 -373
- data/lib/clacky/web/version.js +0 -449
- data/lib/clacky/web/workspace.js +0 -316
- /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
- /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
- /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
- /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
- /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
data/lib/clacky/web/index.html
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
<!-- ── TOP HEADER ──────────────────────────────────────────────────────── -->
|
|
26
26
|
<header id="top-header">
|
|
27
27
|
<div id="header-left">
|
|
28
|
+
<span id="ext-slot-header-left" data-slot="header.left"></span>
|
|
28
29
|
<button id="btn-toggle-sidebar" class="sidebar-toggle-btn" title="Toggle sidebar">
|
|
29
30
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
30
31
|
<rect width="18" height="18" x="3" y="3" rx="2"/>
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
</button>
|
|
52
53
|
</div>
|
|
53
54
|
<div id="header-right">
|
|
55
|
+
<span id="ext-slot-header-right" data-slot="header.right"></span>
|
|
54
56
|
<button id="share-toggle-header" class="theme-toggle-btn" data-i18n-title="share.tooltip" title="Share">
|
|
55
57
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
|
|
56
58
|
<circle cx="18" cy="5" r="3"/>
|
|
@@ -269,10 +271,14 @@
|
|
|
269
271
|
</div>
|
|
270
272
|
</div>
|
|
271
273
|
</div>
|
|
274
|
+
|
|
275
|
+
<!-- Extension nav slot: extensions add custom menu items / entry points to whole new workspaces here. -->
|
|
276
|
+
<div id="ext-slot-sidebar-nav" data-slot="sidebar.nav"></div>
|
|
272
277
|
</div>
|
|
273
278
|
|
|
274
279
|
<!-- Bottom Settings -->
|
|
275
280
|
<div id="sidebar-footer">
|
|
281
|
+
<span id="ext-slot-sidebar-footer" data-slot="sidebar.footer"></span>
|
|
276
282
|
<div class="sidebar-nav-row">
|
|
277
283
|
<button id="btn-settings" class="sidebar-nav-btn" title="Settings">
|
|
278
284
|
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -297,6 +303,9 @@
|
|
|
297
303
|
<!-- ── MAIN ─────────────────────────────────────────────────────────── -->
|
|
298
304
|
<main id="main">
|
|
299
305
|
|
|
306
|
+
<!-- Extension workspace slot: extensions mount entirely new custom panels here. -->
|
|
307
|
+
<div id="ext-slot-main-workspace" data-slot="main.workspace"></div>
|
|
308
|
+
|
|
300
309
|
|
|
301
310
|
<!-- Onboard panel: kept as empty shell (soul_setup auto-launches /onboard session) -->
|
|
302
311
|
<div id="onboard-panel" style="display:none"></div>
|
|
@@ -354,6 +363,8 @@
|
|
|
354
363
|
|
|
355
364
|
<!-- Chat column (messages + info bar + input) -->
|
|
356
365
|
<div id="chat-main">
|
|
366
|
+
<!-- Banner slot: agent-scoped notices/toolbars above the message stream. -->
|
|
367
|
+
<div id="ext-slot-session-banner" class="session-banner" data-slot="session.banner"></div>
|
|
357
368
|
<div id="messages" class="chat-messages-scroll"></div>
|
|
358
369
|
<!-- New message notification banner -->
|
|
359
370
|
<div id="new-message-banner" class="new-message-banner" style="display:none">
|
|
@@ -403,6 +414,8 @@
|
|
|
403
414
|
</span>
|
|
404
415
|
</div>
|
|
405
416
|
|
|
417
|
+
<!-- Composer slot: agent-scoped controls just above the input bar. -->
|
|
418
|
+
<div id="ext-slot-session-composer" class="session-composer" data-slot="session.composer"></div>
|
|
406
419
|
<div id="input-area">
|
|
407
420
|
<div id="ws-disconnect-hint" style="display:none"></div>
|
|
408
421
|
<!-- Skill autocomplete dropdown (shown when user types /xxx) -->
|
|
@@ -441,33 +454,23 @@
|
|
|
441
454
|
</div>
|
|
442
455
|
</div><!-- /#chat-main -->
|
|
443
456
|
|
|
444
|
-
<!-- ──
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
<button id="btn-workspace-close" class="workspace-icon-btn" data-i18n-title="workspace.collapse" title="Collapse">
|
|
457
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
458
|
-
<path d="M9 18l6-6-6-6"/>
|
|
459
|
-
</svg>
|
|
460
|
-
</button>
|
|
461
|
-
</div>
|
|
462
|
-
</div>
|
|
463
|
-
<div id="workspace-tree" role="tree"></div>
|
|
464
|
-
</aside>
|
|
457
|
+
<!-- ── SESSION ASIDE (right) ────────────────────────────────────────
|
|
458
|
+
One resizable / collapsible right column. The tab bar and tab bodies
|
|
459
|
+
are rendered into #ext-slot-session-aside by Clacky.ext (tabbed slot);
|
|
460
|
+
the chrome here (resize handle, collapse button) is host-owned and
|
|
461
|
+
sits outside the slot container so re-renders never clobber it. -->
|
|
462
|
+
<div id="session-aside" class="session-aside">
|
|
463
|
+
<div id="session-aside-resize" class="session-aside-resize"></div>
|
|
464
|
+
<button id="btn-aside-collapse" class="session-aside-collapse" data-i18n-title="aside.collapse" title="Collapse">
|
|
465
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
|
|
466
|
+
</button>
|
|
467
|
+
<div id="ext-slot-session-aside" class="session-aside-slot" data-slot="session.aside"></div>
|
|
468
|
+
</div>
|
|
465
469
|
|
|
466
470
|
<!-- Collapsed-state opener tab -->
|
|
467
|
-
<
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
</svg>
|
|
471
|
+
<div id="workspace-overlay"></div>
|
|
472
|
+
<button id="btn-aside-open" class="session-aside-opener" data-i18n-title="aside.expand" title="Open panel" style="display:none">
|
|
473
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
|
|
471
474
|
</button>
|
|
472
475
|
</div>
|
|
473
476
|
|
|
@@ -741,7 +744,10 @@
|
|
|
741
744
|
<div id="profile-soul-body" class="profile-markdown"></div>
|
|
742
745
|
<div class="profile-pane-footer">
|
|
743
746
|
<p class="profile-pane-footer-hint" data-i18n="profile.soul.curateHint">Not quite right? Let the assistant curate this through a short conversation.</p>
|
|
744
|
-
<
|
|
747
|
+
<div class="profile-pane-footer-actions">
|
|
748
|
+
<button id="btn-profile-curate-soul" class="btn-profile-update" data-i18n="profile.soul.curateBtn">Have the assistant curate this</button>
|
|
749
|
+
<button id="btn-profile-edit-soul" class="btn-profile-edit" data-i18n="profile.soul.editBtn">Edit directly</button>
|
|
750
|
+
</div>
|
|
745
751
|
</div>
|
|
746
752
|
</section>
|
|
747
753
|
|
|
@@ -757,7 +763,10 @@
|
|
|
757
763
|
<div id="profile-user-body" class="profile-markdown"></div>
|
|
758
764
|
<div class="profile-pane-footer">
|
|
759
765
|
<p class="profile-pane-footer-hint" data-i18n="profile.user.curateHint">Changed jobs? Picked up new interests? Let the assistant update your profile.</p>
|
|
760
|
-
<
|
|
766
|
+
<div class="profile-pane-footer-actions">
|
|
767
|
+
<button id="btn-profile-curate-user" class="btn-profile-update" data-i18n="profile.user.curateBtn">Have the assistant update this</button>
|
|
768
|
+
<button id="btn-profile-edit-user" class="btn-profile-edit" data-i18n="profile.user.editBtn">Edit directly</button>
|
|
769
|
+
</div>
|
|
761
770
|
</div>
|
|
762
771
|
</section>
|
|
763
772
|
|
|
@@ -797,9 +806,14 @@
|
|
|
797
806
|
<button class="settings-tab" data-tab="ui" data-i18n="settings.tabs.ui">UI</button>
|
|
798
807
|
<button class="settings-tab" data-tab="general" data-i18n="settings.tabs.general">General</button>
|
|
799
808
|
<button class="settings-tab" data-tab="about" data-i18n="settings.tabs.about">About</button>
|
|
809
|
+
<!-- Extension tab slot: extensions add custom settings tabs here (button needs data-tab="<id>"). -->
|
|
810
|
+
<span id="ext-slot-settings-tabs" data-slot="settings.tabs"></span>
|
|
800
811
|
</div>
|
|
801
812
|
|
|
802
813
|
<div id="settings-body">
|
|
814
|
+
<!-- Extension tab-content slot: matching panels go here (root needs data-tab-content="<id>"). -->
|
|
815
|
+
<span id="ext-slot-settings-body" data-slot="settings.body"></span>
|
|
816
|
+
|
|
803
817
|
|
|
804
818
|
<!-- ══ Tab: Models ══ -->
|
|
805
819
|
<div class="settings-tab-content active" data-tab-content="models">
|
|
@@ -821,6 +835,19 @@
|
|
|
821
835
|
Optional. Image / video / audio / vision models.
|
|
822
836
|
</div>
|
|
823
837
|
<div id="media-rows"></div>
|
|
838
|
+
|
|
839
|
+
<!-- Output directory: scoped under media section -->
|
|
840
|
+
<div class="settings-subsection" id="media-output-dir-section">
|
|
841
|
+
<p class="settings-subsection-desc" data-i18n="settings.media.output_dir.desc">Where generated images, videos and audio are saved (optional)</p>
|
|
842
|
+
<div class="settings-network-url">
|
|
843
|
+
<div class="settings-network-url-row">
|
|
844
|
+
<input type="text" id="settings-media-output-dir" class="field-input" placeholder="" autocomplete="off" spellcheck="false" readonly>
|
|
845
|
+
<button type="button" id="btn-browse-media-output-dir" class="btn-settings-action" data-i18n="settings.media.output_dir.browse">Browse…</button>
|
|
846
|
+
<button type="button" id="btn-clear-media-output-dir" class="btn-settings-action" data-i18n="settings.media.output_dir.clear">Clear</button>
|
|
847
|
+
</div>
|
|
848
|
+
<div id="settings-media-output-dir-status" class="model-test-result"></div>
|
|
849
|
+
</div>
|
|
850
|
+
</div>
|
|
824
851
|
</section>
|
|
825
852
|
</div>
|
|
826
853
|
|
|
@@ -934,22 +961,24 @@
|
|
|
934
961
|
</div>
|
|
935
962
|
<p class="settings-section-desc" data-i18n="settings.backup.desc">Back up your ~/.clacky directory (config, skills, memories, tasks, sessions). Regenerable caches and logs are excluded.</p>
|
|
936
963
|
|
|
937
|
-
<div class="backup-auto-
|
|
938
|
-
<
|
|
939
|
-
<
|
|
940
|
-
|
|
964
|
+
<div class="backup-auto-card">
|
|
965
|
+
<div class="backup-auto-row">
|
|
966
|
+
<label class="toggle-switch">
|
|
967
|
+
<input type="checkbox" id="backup-auto-toggle">
|
|
968
|
+
<span class="toggle-slider"></span>
|
|
969
|
+
</label>
|
|
970
|
+
<span class="backup-auto-label" data-i18n="settings.backup.autoLabel">Automatic backup</span>
|
|
971
|
+
<span class="backup-auto-hint" data-i18n="settings.backup.autoHint">Daily at 03:00, keeps the latest 7</span>
|
|
972
|
+
</div>
|
|
973
|
+
|
|
974
|
+
<label class="backup-option backup-option-nested">
|
|
975
|
+
<input type="checkbox" id="backup-include-sessions">
|
|
976
|
+
<span data-i18n="settings.backup.includeSessions">Include session history (larger archive)</span>
|
|
941
977
|
</label>
|
|
942
|
-
<span class="backup-auto-label" data-i18n="settings.backup.autoLabel">Automatic backup</span>
|
|
943
|
-
<span class="backup-auto-hint" data-i18n="settings.backup.autoHint">Daily at 03:00, keeps the latest 7</span>
|
|
944
978
|
</div>
|
|
945
979
|
|
|
946
|
-
<label class="backup-option">
|
|
947
|
-
<input type="checkbox" id="backup-include-sessions">
|
|
948
|
-
<span data-i18n="settings.backup.includeSessions">Include session history (larger archive)</span>
|
|
949
|
-
</label>
|
|
950
|
-
|
|
951
980
|
<div class="backup-actions">
|
|
952
|
-
<button id="btn-backup-now" class="btn-settings-action" data-i18n="settings.backup.runNow"
|
|
981
|
+
<button id="btn-backup-now" class="btn-settings-action" data-i18n="settings.backup.runNow">Download backup (full snapshot)</button>
|
|
953
982
|
<span id="backup-status" class="model-test-result"></span>
|
|
954
983
|
</div>
|
|
955
984
|
</section>
|
|
@@ -1441,38 +1470,68 @@
|
|
|
1441
1470
|
|
|
1442
1471
|
<div id="tooltip" style="display:none"></div>
|
|
1443
1472
|
|
|
1444
|
-
<script src="/
|
|
1473
|
+
<script src="/vendor/codemirror/codemirror.min.js"></script>
|
|
1474
|
+
<script src="/components/code-editor.js"></script>
|
|
1475
|
+
<script src="/vendor/marked/marked.min.js"></script>
|
|
1445
1476
|
<script src="/vendor/hljs/highlight.min.js"></script>
|
|
1446
1477
|
<script src="/vendor/katex/katex.min.js"></script>
|
|
1447
1478
|
<script src="/vendor/katex/auto-render.min.js"></script>
|
|
1448
1479
|
<script src="/utils.js"></script>
|
|
1480
|
+
<script src="/core/ext.js"></script>
|
|
1481
|
+
<script src="/core/aside.js"></script>
|
|
1449
1482
|
<script src="/i18n.js"></script>
|
|
1450
1483
|
<script src="/auth.js"></script>
|
|
1451
1484
|
<script src="/theme.js"></script>
|
|
1452
|
-
<script src="/notify.js"></script>
|
|
1485
|
+
<script src="/components/notify.js"></script>
|
|
1453
1486
|
<script src="/ws.js"></script>
|
|
1454
1487
|
<script src="/ws-dispatcher.js"></script>
|
|
1455
1488
|
<script src="/sessions.js"></script>
|
|
1456
|
-
<script src="/workspace.js"></script>
|
|
1457
|
-
<script src="/
|
|
1458
|
-
<script src="/
|
|
1489
|
+
<script src="/features/workspace/store.js"></script>
|
|
1490
|
+
<script src="/features/workspace/view.js"></script>
|
|
1491
|
+
<script src="/components/datepicker.js"></script>
|
|
1492
|
+
<script src="/features/tasks/store.js"></script>
|
|
1493
|
+
<script src="/features/tasks/view.js"></script>
|
|
1494
|
+
<script src="/features/skills/store.js"></script>
|
|
1495
|
+
<script src="/features/skills/view.js"></script>
|
|
1496
|
+
<script src="/features/channels/store.js"></script>
|
|
1497
|
+
<script src="/features/channels/view.js"></script>
|
|
1459
1498
|
<script src="/skills.js"></script>
|
|
1460
|
-
<script src="/
|
|
1461
|
-
<script src="/mcp.js"></script>
|
|
1462
|
-
<script src="/backup.js"></script>
|
|
1463
|
-
<script src="/
|
|
1499
|
+
<script src="/features/mcp/store.js"></script>
|
|
1500
|
+
<script src="/features/mcp/view.js"></script>
|
|
1501
|
+
<script src="/features/backup/store.js"></script>
|
|
1502
|
+
<script src="/features/backup/view.js"></script>
|
|
1503
|
+
<script src="/features/model-tester/store.js"></script>
|
|
1504
|
+
<script src="/features/model-tester/view.js"></script>
|
|
1464
1505
|
<script src="/settings.js"></script>
|
|
1465
|
-
<script src="/billing.js"></script>
|
|
1466
|
-
<script src="/
|
|
1467
|
-
<script src="/
|
|
1468
|
-
<script src="/
|
|
1469
|
-
<script src="/
|
|
1470
|
-
<script src="/
|
|
1471
|
-
<script src="/
|
|
1472
|
-
<script src="/
|
|
1506
|
+
<script src="/features/billing/store.js"></script>
|
|
1507
|
+
<script src="/features/billing/view.js"></script>
|
|
1508
|
+
<script src="/components/onboard.js"></script>
|
|
1509
|
+
<script src="/features/brand/store.js"></script>
|
|
1510
|
+
<script src="/features/brand/view.js"></script>
|
|
1511
|
+
<script src="/features/creator/store.js"></script>
|
|
1512
|
+
<script src="/features/creator/view.js"></script>
|
|
1513
|
+
<script src="/features/trash/store.js"></script>
|
|
1514
|
+
<script src="/features/trash/view.js"></script>
|
|
1515
|
+
<script src="/features/profile/store.js"></script>
|
|
1516
|
+
<script src="/features/profile/view.js"></script>
|
|
1517
|
+
<script src="/features/version/store.js"></script>
|
|
1518
|
+
<script src="/features/version/view.js"></script>
|
|
1519
|
+
<script src="/components/sidebar.js"></script>
|
|
1473
1520
|
<script src="/vendor/qrcode/qrcode.min.js"></script>
|
|
1474
|
-
<script src="/share.js"></script>
|
|
1521
|
+
<script src="/features/share/store.js"></script>
|
|
1522
|
+
<script src="/features/share/view.js"></script>
|
|
1475
1523
|
<script src="/app.js"></script>
|
|
1476
|
-
|
|
1524
|
+
{{EXT_SCRIPTS}}
|
|
1525
|
+
<script>
|
|
1526
|
+
Tooltip.init();
|
|
1527
|
+
// Render every host-declared slot once extensions have registered.
|
|
1528
|
+
// Each [data-slot] element is a named injection point; renderSlot isolates
|
|
1529
|
+
// failing extensions so a crash degrades only that slot.
|
|
1530
|
+
(function () {
|
|
1531
|
+
document.querySelectorAll("[data-slot]").forEach(function (el) {
|
|
1532
|
+
Clacky.ext.renderSlot(el.getAttribute("data-slot"), el, {});
|
|
1533
|
+
});
|
|
1534
|
+
})();
|
|
1535
|
+
</script>
|
|
1477
1536
|
</body>
|
|
1478
1537
|
</html>
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -29,6 +29,12 @@ const Sessions = (() => {
|
|
|
29
29
|
// Search results live in their own list, rendered into the overlay's
|
|
30
30
|
// #session-search-results — they NEVER replace the sidebar session list.
|
|
31
31
|
let _searchResults = [];
|
|
32
|
+
// Sessions resolved by id but not in the paged sidebar list — e.g. landed
|
|
33
|
+
// here via search-result click, URL deep link, share link, browser
|
|
34
|
+
// back/forward, or external notification jump. Acts as a local cache for
|
|
35
|
+
// `findOrFetch`. Excluded from sidebar render and from the loadMore cursor
|
|
36
|
+
// so the pagination of `_sessions` stays correct.
|
|
37
|
+
const _extraSessions = [];
|
|
32
38
|
// Active search result split when _filter.q is non-empty:
|
|
33
39
|
// { nameIds: Set<id>, contentIds: Set<id>, contentLoaded: bool }
|
|
34
40
|
let _searchSplit = null;
|
|
@@ -1353,8 +1359,13 @@ const Sessions = (() => {
|
|
|
1353
1359
|
}
|
|
1354
1360
|
bubbleHtml += escapeHtml(ev.content || "");
|
|
1355
1361
|
el.innerHTML = bubbleHtml;
|
|
1362
|
+
if (ev.created_at) el.dataset.createdAt = ev.created_at;
|
|
1356
1363
|
_appendMsgTime(el, ev.created_at);
|
|
1357
|
-
|
|
1364
|
+
const wrap = document.createElement("div");
|
|
1365
|
+
wrap.className = "msg-user-wrap";
|
|
1366
|
+
wrap.appendChild(el);
|
|
1367
|
+
_appendUserActionBar(el, wrap);
|
|
1368
|
+
container.appendChild(wrap);
|
|
1358
1369
|
break;
|
|
1359
1370
|
}
|
|
1360
1371
|
|
|
@@ -1694,6 +1705,151 @@ const Sessions = (() => {
|
|
|
1694
1705
|
el.appendChild(span);
|
|
1695
1706
|
}
|
|
1696
1707
|
|
|
1708
|
+
// ── User message action bar (copy + edit) ───────────────────────────────
|
|
1709
|
+
|
|
1710
|
+
const COPY_SVG = `<svg class="msg-user-copy-icon" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">` +
|
|
1711
|
+
`<path fill="currentColor" d="M10 1H4a2 2 0 0 0-2 2v8h1.5V3a.5.5 0 0 1 .5-.5h6V1zm3 3H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm.5 10a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V6a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v8z"/>` +
|
|
1712
|
+
`</svg>` +
|
|
1713
|
+
`<svg class="msg-user-copy-icon-check" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">` +
|
|
1714
|
+
`<path fill="currentColor" d="M13.5 3.5 6 11 2.5 7.5 1 9l5 5 9-9z"/>` +
|
|
1715
|
+
`</svg>`;
|
|
1716
|
+
|
|
1717
|
+
const EDIT_SVG = `<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">` +
|
|
1718
|
+
`<path fill="currentColor" d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61zm1.414 1.06a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354l-1.086-1.086zM11.189 6.25 9.75 4.81l-6.286 6.287a.25.25 0 0 0-.064.108l-.558 1.953 1.953-.558a.249.249 0 0 0 .108-.064l6.286-6.286z"/>` +
|
|
1719
|
+
`</svg>`;
|
|
1720
|
+
|
|
1721
|
+
function _appendUserActionBar(el, wrap) {
|
|
1722
|
+
el.dataset.originalHtml = el.innerHTML;
|
|
1723
|
+
|
|
1724
|
+
const bar = document.createElement("div");
|
|
1725
|
+
bar.className = "msg-user-actions";
|
|
1726
|
+
|
|
1727
|
+
const copyBtn = document.createElement("button");
|
|
1728
|
+
copyBtn.type = "button";
|
|
1729
|
+
copyBtn.className = "msg-user-action-btn";
|
|
1730
|
+
copyBtn.setAttribute("aria-label", I18n.t("chat.copy"));
|
|
1731
|
+
copyBtn.title = I18n.t("chat.copy");
|
|
1732
|
+
copyBtn.innerHTML = COPY_SVG;
|
|
1733
|
+
copyBtn.addEventListener("click", (e) => {
|
|
1734
|
+
e.stopPropagation();
|
|
1735
|
+
const text = _extractUserBubbleText(el);
|
|
1736
|
+
_copyTextAndFlash(copyBtn, text);
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
const editBtn = document.createElement("button");
|
|
1740
|
+
editBtn.type = "button";
|
|
1741
|
+
editBtn.className = "msg-user-action-btn";
|
|
1742
|
+
editBtn.setAttribute("aria-label", I18n.t("chat.edit"));
|
|
1743
|
+
editBtn.title = I18n.t("chat.edit");
|
|
1744
|
+
editBtn.innerHTML = EDIT_SVG;
|
|
1745
|
+
editBtn.addEventListener("click", (e) => {
|
|
1746
|
+
e.stopPropagation();
|
|
1747
|
+
_enterEditMode(el);
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
bar.appendChild(copyBtn);
|
|
1751
|
+
bar.appendChild(editBtn);
|
|
1752
|
+
wrap.appendChild(bar);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
function _extractUserBubbleText(el) {
|
|
1756
|
+
const clone = el.cloneNode(true);
|
|
1757
|
+
clone.querySelectorAll(".msg-user-actions, .msg-time").forEach(n => n.remove());
|
|
1758
|
+
return (clone.textContent || "").trim();
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function _enterEditMode(el) {
|
|
1762
|
+
if (el.classList.contains("editing")) return;
|
|
1763
|
+
el.classList.add("editing");
|
|
1764
|
+
|
|
1765
|
+
const originalHtml = el.dataset.originalHtml || "";
|
|
1766
|
+
const originalText = (() => {
|
|
1767
|
+
const tmp = document.createElement("div");
|
|
1768
|
+
tmp.innerHTML = originalHtml;
|
|
1769
|
+
tmp.querySelectorAll(".msg-user-actions, .msg-time, .msg-pdf-badge, img").forEach(n => n.remove());
|
|
1770
|
+
return (tmp.textContent || "").trim();
|
|
1771
|
+
})();
|
|
1772
|
+
|
|
1773
|
+
el.innerHTML = "";
|
|
1774
|
+
|
|
1775
|
+
const wrap = document.createElement("div");
|
|
1776
|
+
wrap.className = "msg-user-edit-wrap";
|
|
1777
|
+
|
|
1778
|
+
const textarea = document.createElement("textarea");
|
|
1779
|
+
textarea.className = "msg-user-edit-textarea";
|
|
1780
|
+
textarea.value = originalText;
|
|
1781
|
+
textarea.rows = 1;
|
|
1782
|
+
|
|
1783
|
+
const actions = document.createElement("div");
|
|
1784
|
+
actions.className = "msg-user-edit-actions";
|
|
1785
|
+
|
|
1786
|
+
const cancelBtn = document.createElement("button");
|
|
1787
|
+
cancelBtn.type = "button";
|
|
1788
|
+
cancelBtn.className = "msg-user-edit-cancel";
|
|
1789
|
+
cancelBtn.textContent = I18n.t("chat.cancel");
|
|
1790
|
+
cancelBtn.addEventListener("click", () => _exitEditMode(el));
|
|
1791
|
+
|
|
1792
|
+
const sendBtn = document.createElement("button");
|
|
1793
|
+
sendBtn.type = "button";
|
|
1794
|
+
sendBtn.className = "msg-user-edit-send";
|
|
1795
|
+
sendBtn.textContent = I18n.t("chat.send");
|
|
1796
|
+
sendBtn.addEventListener("click", () => _submitEdit(el, textarea.value.trim()));
|
|
1797
|
+
|
|
1798
|
+
textarea.addEventListener("keydown", (e) => {
|
|
1799
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1800
|
+
e.preventDefault();
|
|
1801
|
+
_submitEdit(el, textarea.value.trim());
|
|
1802
|
+
}
|
|
1803
|
+
if (e.key === "Escape") _exitEditMode(el);
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
textarea.addEventListener("input", () => {
|
|
1807
|
+
textarea.style.height = "auto";
|
|
1808
|
+
textarea.style.height = textarea.scrollHeight + "px";
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
actions.appendChild(cancelBtn);
|
|
1812
|
+
actions.appendChild(sendBtn);
|
|
1813
|
+
wrap.appendChild(textarea);
|
|
1814
|
+
wrap.appendChild(actions);
|
|
1815
|
+
el.appendChild(wrap);
|
|
1816
|
+
|
|
1817
|
+
requestAnimationFrame(() => {
|
|
1818
|
+
textarea.style.height = textarea.scrollHeight + "px";
|
|
1819
|
+
textarea.focus();
|
|
1820
|
+
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function _exitEditMode(el) {
|
|
1825
|
+
el.classList.remove("editing");
|
|
1826
|
+
el.innerHTML = el.dataset.originalHtml || "";
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function _submitEdit(el, newContent) {
|
|
1830
|
+
if (!newContent) return;
|
|
1831
|
+
if (!Sessions.activeId) return;
|
|
1832
|
+
|
|
1833
|
+
const createdAt = el.dataset.createdAt || null;
|
|
1834
|
+
|
|
1835
|
+
const messages = el.closest("#messages, .messages");
|
|
1836
|
+
if (messages) {
|
|
1837
|
+
const wrap = el.parentElement;
|
|
1838
|
+
let sibling = wrap ? wrap.nextSibling : el.nextSibling;
|
|
1839
|
+
while (sibling) {
|
|
1840
|
+
const next = sibling.nextSibling;
|
|
1841
|
+
sibling.remove();
|
|
1842
|
+
sibling = next;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
_exitEditMode(el);
|
|
1847
|
+
|
|
1848
|
+
WS.send({ type: "edit_message", session_id: Sessions.activeId, content: newContent, created_at: createdAt });
|
|
1849
|
+
|
|
1850
|
+
if (messages) messages.scrollTop = messages.scrollHeight;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1697
1853
|
// ── Copy button for assistant messages ──────────────────────────────────
|
|
1698
1854
|
//
|
|
1699
1855
|
// Each assistant bubble gets a small copy button in its top-right corner.
|
|
@@ -2017,7 +2173,41 @@ const Sessions = (() => {
|
|
|
2017
2173
|
get all() { return _sessions; },
|
|
2018
2174
|
get activeId() { return _activeId; },
|
|
2019
2175
|
get searchOpen() { return _searchOpen; },
|
|
2020
|
-
find: id => _sessions.find(s => s.id === id)
|
|
2176
|
+
find: id => _sessions.find(s => s.id === id)
|
|
2177
|
+
|| _extraSessions.find(s => s.id === id),
|
|
2178
|
+
|
|
2179
|
+
// Async variant of `find`: when not found in memory, falls back to
|
|
2180
|
+
// GET /api/sessions/:id which returns the on-disk session merged with
|
|
2181
|
+
// any live in-memory state (see SessionRegistry#snapshot in
|
|
2182
|
+
// session_registry.rb). Resolved rows are cached in `_extraSessions`
|
|
2183
|
+
// so subsequent synchronous `find` calls hit too. Returns null on
|
|
2184
|
+
// 404 / network error.
|
|
2185
|
+
//
|
|
2186
|
+
// Use this in code paths where missing-id should NOT silently fail
|
|
2187
|
+
// (Router navigation: search clicks, URL deep links, share links,
|
|
2188
|
+
// browser back/forward, notification jumps). For tight synchronous
|
|
2189
|
+
// paths (WS dispatch, status updates) keep using `find`.
|
|
2190
|
+
async findOrFetch(id) {
|
|
2191
|
+
if (!id) return null;
|
|
2192
|
+
const local = _sessions.find(s => s.id === id)
|
|
2193
|
+
|| _extraSessions.find(s => s.id === id);
|
|
2194
|
+
if (local) return local;
|
|
2195
|
+
try {
|
|
2196
|
+
const resp = await fetch(`/api/sessions/${encodeURIComponent(id)}`);
|
|
2197
|
+
if (!resp.ok) return null;
|
|
2198
|
+
const data = await resp.json();
|
|
2199
|
+
if (!data || !data.session) return null;
|
|
2200
|
+
// Race guard: another caller may have hydrated meanwhile.
|
|
2201
|
+
if (!_sessions.find(s => s.id === id)
|
|
2202
|
+
&& !_extraSessions.find(s => s.id === id)) {
|
|
2203
|
+
_extraSessions.push(data.session);
|
|
2204
|
+
}
|
|
2205
|
+
return data.session;
|
|
2206
|
+
} catch (e) {
|
|
2207
|
+
console.error("Sessions.findOrFetch failed:", e);
|
|
2208
|
+
return null;
|
|
2209
|
+
}
|
|
2210
|
+
},
|
|
2021
2211
|
|
|
2022
2212
|
// Composer entry point — called by Skill autocomplete keydown handler
|
|
2023
2213
|
// (in app.js) when the user presses Enter without an active completion.
|
|
@@ -3265,26 +3455,31 @@ const Sessions = (() => {
|
|
|
3265
3455
|
}
|
|
3266
3456
|
if (type === "user" && time) _appendMsgTime(el, time);
|
|
3267
3457
|
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3458
|
+
if (type === "user") {
|
|
3459
|
+
const wrap = document.createElement("div");
|
|
3460
|
+
wrap.className = "msg-user-wrap";
|
|
3461
|
+
wrap.appendChild(el);
|
|
3462
|
+
_appendUserActionBar(el, wrap);
|
|
3463
|
+
messages.appendChild(wrap);
|
|
3464
|
+
} else {
|
|
3465
|
+
// For error messages, add a retry button
|
|
3466
|
+
if (type === "error") {
|
|
3467
|
+
const retryBtn = document.createElement("button");
|
|
3468
|
+
retryBtn.className = "retry-btn";
|
|
3469
|
+
retryBtn.textContent = I18n.t("chat.retry");
|
|
3470
|
+
retryBtn.onclick = () => {
|
|
3471
|
+
if (!_activeId) return;
|
|
3472
|
+
WS.send({
|
|
3473
|
+
type: "message",
|
|
3474
|
+
session_id: _activeId,
|
|
3475
|
+
content: I18n.t("chat.continue")
|
|
3476
|
+
});
|
|
3477
|
+
retryBtn.disabled = true;
|
|
3478
|
+
};
|
|
3479
|
+
el.appendChild(retryBtn);
|
|
3480
|
+
}
|
|
3481
|
+
messages.appendChild(el);
|
|
3285
3482
|
}
|
|
3286
|
-
|
|
3287
|
-
messages.appendChild(el);
|
|
3288
3483
|
// User messages: force scroll to bottom (user just sent a message)
|
|
3289
3484
|
// Assistant/info: conditional scroll (preserve position if user is viewing history)
|
|
3290
3485
|
if (type === "user") {
|
|
@@ -4418,7 +4613,7 @@ const Sessions = (() => {
|
|
|
4418
4613
|
// ── Tree-based directory picker ─────────────────────────────────────────
|
|
4419
4614
|
const ICON_FOLDER_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
|
4420
4615
|
const ICON_CARET_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>';
|
|
4421
|
-
function showDirectoryPicker(currentDir, sessionId) {
|
|
4616
|
+
function showDirectoryPicker(currentDir, sessionId, titleText) {
|
|
4422
4617
|
return new Promise((resolve) => {
|
|
4423
4618
|
const t = (key, fallback) => {
|
|
4424
4619
|
const s = I18n.t(key);
|
|
@@ -4570,9 +4765,10 @@ const Sessions = (() => {
|
|
|
4570
4765
|
// Title
|
|
4571
4766
|
const title = document.createElement("div");
|
|
4572
4767
|
title.className = "modal-title";
|
|
4573
|
-
title.textContent =
|
|
4574
|
-
|
|
4575
|
-
|
|
4768
|
+
title.textContent = titleText
|
|
4769
|
+
|| (sessionLess
|
|
4770
|
+
? t("sessions.modal.dirpicker.title", "选择工作目录")
|
|
4771
|
+
: t("sib.dir.changePrompt", "切换工作目录"));
|
|
4576
4772
|
modal.appendChild(title);
|
|
4577
4773
|
|
|
4578
4774
|
// Modal body
|