openclacky 1.3.1 → 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 +44 -0
- data/Dockerfile +3 -0
- data/README.md +1 -1
- data/README_JA.md +237 -0
- data/lib/clacky/agent/session_serializer.rb +65 -11
- 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/brand_config.rb +1 -1
- 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 +521 -13
- data/lib/clacky/server/server_master.rb +6 -4
- data/lib/clacky/utils/environment_detector.rb +16 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +512 -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} +134 -242
- 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 +60 -6
- data/lib/clacky/web/index.html +117 -57
- data/lib/clacky/web/sessions.js +221 -25
- data/lib/clacky/web/settings.js +121 -25
- data/lib/clacky/web/skills.js +3 -821
- 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 -365
- data/lib/clacky/web/version.js +0 -449
- data/lib/clacky/web/workspace.js +0 -212
- /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,32 +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
|
-
<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">
|
|
457
|
-
<path d="M9 18l6-6-6-6"/>
|
|
458
|
-
</svg>
|
|
459
|
-
</button>
|
|
460
|
-
</div>
|
|
461
|
-
</div>
|
|
462
|
-
<div id="workspace-tree" role="tree"></div>
|
|
463
|
-
</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>
|
|
464
469
|
|
|
465
470
|
<!-- Collapsed-state opener tab -->
|
|
466
|
-
<
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
</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>
|
|
470
474
|
</button>
|
|
471
475
|
</div>
|
|
472
476
|
|
|
@@ -740,7 +744,10 @@
|
|
|
740
744
|
<div id="profile-soul-body" class="profile-markdown"></div>
|
|
741
745
|
<div class="profile-pane-footer">
|
|
742
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>
|
|
743
|
-
<
|
|
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>
|
|
744
751
|
</div>
|
|
745
752
|
</section>
|
|
746
753
|
|
|
@@ -756,7 +763,10 @@
|
|
|
756
763
|
<div id="profile-user-body" class="profile-markdown"></div>
|
|
757
764
|
<div class="profile-pane-footer">
|
|
758
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>
|
|
759
|
-
<
|
|
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>
|
|
760
770
|
</div>
|
|
761
771
|
</section>
|
|
762
772
|
|
|
@@ -796,9 +806,14 @@
|
|
|
796
806
|
<button class="settings-tab" data-tab="ui" data-i18n="settings.tabs.ui">UI</button>
|
|
797
807
|
<button class="settings-tab" data-tab="general" data-i18n="settings.tabs.general">General</button>
|
|
798
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>
|
|
799
811
|
</div>
|
|
800
812
|
|
|
801
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
|
+
|
|
802
817
|
|
|
803
818
|
<!-- ══ Tab: Models ══ -->
|
|
804
819
|
<div class="settings-tab-content active" data-tab-content="models">
|
|
@@ -820,6 +835,19 @@
|
|
|
820
835
|
Optional. Image / video / audio / vision models.
|
|
821
836
|
</div>
|
|
822
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>
|
|
823
851
|
</section>
|
|
824
852
|
</div>
|
|
825
853
|
|
|
@@ -933,22 +961,24 @@
|
|
|
933
961
|
</div>
|
|
934
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>
|
|
935
963
|
|
|
936
|
-
<div class="backup-auto-
|
|
937
|
-
<
|
|
938
|
-
<
|
|
939
|
-
|
|
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>
|
|
940
977
|
</label>
|
|
941
|
-
<span class="backup-auto-label" data-i18n="settings.backup.autoLabel">Automatic backup</span>
|
|
942
|
-
<span class="backup-auto-hint" data-i18n="settings.backup.autoHint">Daily at 03:00, keeps the latest 7</span>
|
|
943
978
|
</div>
|
|
944
979
|
|
|
945
|
-
<label class="backup-option">
|
|
946
|
-
<input type="checkbox" id="backup-include-sessions">
|
|
947
|
-
<span data-i18n="settings.backup.includeSessions">Include session history (larger archive)</span>
|
|
948
|
-
</label>
|
|
949
|
-
|
|
950
980
|
<div class="backup-actions">
|
|
951
|
-
<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>
|
|
952
982
|
<span id="backup-status" class="model-test-result"></span>
|
|
953
983
|
</div>
|
|
954
984
|
</section>
|
|
@@ -1440,38 +1470,68 @@
|
|
|
1440
1470
|
|
|
1441
1471
|
<div id="tooltip" style="display:none"></div>
|
|
1442
1472
|
|
|
1443
|
-
<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>
|
|
1444
1476
|
<script src="/vendor/hljs/highlight.min.js"></script>
|
|
1445
1477
|
<script src="/vendor/katex/katex.min.js"></script>
|
|
1446
1478
|
<script src="/vendor/katex/auto-render.min.js"></script>
|
|
1447
1479
|
<script src="/utils.js"></script>
|
|
1480
|
+
<script src="/core/ext.js"></script>
|
|
1481
|
+
<script src="/core/aside.js"></script>
|
|
1448
1482
|
<script src="/i18n.js"></script>
|
|
1449
1483
|
<script src="/auth.js"></script>
|
|
1450
1484
|
<script src="/theme.js"></script>
|
|
1451
|
-
<script src="/notify.js"></script>
|
|
1485
|
+
<script src="/components/notify.js"></script>
|
|
1452
1486
|
<script src="/ws.js"></script>
|
|
1453
1487
|
<script src="/ws-dispatcher.js"></script>
|
|
1454
1488
|
<script src="/sessions.js"></script>
|
|
1455
|
-
<script src="/workspace.js"></script>
|
|
1456
|
-
<script src="/
|
|
1457
|
-
<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>
|
|
1458
1498
|
<script src="/skills.js"></script>
|
|
1459
|
-
<script src="/
|
|
1460
|
-
<script src="/mcp.js"></script>
|
|
1461
|
-
<script src="/backup.js"></script>
|
|
1462
|
-
<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>
|
|
1463
1505
|
<script src="/settings.js"></script>
|
|
1464
|
-
<script src="/billing.js"></script>
|
|
1465
|
-
<script src="/
|
|
1466
|
-
<script src="/
|
|
1467
|
-
<script src="/
|
|
1468
|
-
<script src="/
|
|
1469
|
-
<script src="/
|
|
1470
|
-
<script src="/
|
|
1471
|
-
<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>
|
|
1472
1520
|
<script src="/vendor/qrcode/qrcode.min.js"></script>
|
|
1473
|
-
<script src="/share.js"></script>
|
|
1521
|
+
<script src="/features/share/store.js"></script>
|
|
1522
|
+
<script src="/features/share/view.js"></script>
|
|
1474
1523
|
<script src="/app.js"></script>
|
|
1475
|
-
|
|
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>
|
|
1476
1536
|
</body>
|
|
1477
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
|