openclacky 1.3.7 → 1.3.8
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 +24 -0
- data/lib/clacky/agent/hook_manager.rb +10 -1
- data/lib/clacky/agent/system_prompt_builder.rb +1 -1
- data/lib/clacky/agent.rb +9 -2
- data/lib/clacky/agent_profile.rb +7 -4
- data/lib/clacky/billing/billing_store.rb +3 -2
- data/lib/clacky/default_extensions/coding/agents/coding/avatar.png +0 -0
- data/lib/clacky/default_extensions/coding/ext.yml +4 -3
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/avatar.png +0 -0
- data/lib/clacky/default_extensions/ext-studio/ext.yml +1 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +24 -5
- data/lib/clacky/default_extensions/general/agents/general/avatar.png +0 -0
- data/lib/clacky/default_extensions/general/ext.yml +4 -3
- data/lib/clacky/default_extensions/git/panels/git/view.js +3 -3
- data/lib/clacky/default_extensions/meeting/panels/meeting/view.js +1 -2
- data/lib/clacky/extension/loader.rb +6 -0
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/server/http_server.rb +129 -5
- data/lib/clacky/shell_hook_loader.rb +266 -22
- data/lib/clacky/ui2/components/welcome_banner.rb +1 -1
- data/lib/clacky/utils/workspace_rules.rb +2 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +230 -46
- data/lib/clacky/web/core/ext.js +130 -24
- data/lib/clacky/web/features/extensions/view.js +4 -3
- data/lib/clacky/web/features/new-session/store.js +13 -0
- data/lib/clacky/web/features/new-session/view.js +314 -14
- data/lib/clacky/web/features/trash/view.js +4 -5
- data/lib/clacky/web/i18n.js +34 -0
- data/lib/clacky/web/index.html +43 -11
- data/lib/clacky/web/sessions.js +3 -3
- data/lib/clacky/web/skills.js +86 -48
- data/lib/clacky/web/theme.js +3 -0
- data/lib/clacky/web/ws-dispatcher.js +11 -2
- metadata +7 -7
data/lib/clacky/web/app.css
CHANGED
|
@@ -221,6 +221,7 @@ html { font-size: 16px; }
|
|
|
221
221
|
100% { background-position: -200% 0; }
|
|
222
222
|
}
|
|
223
223
|
.skel {
|
|
224
|
+
display: inline-block;
|
|
224
225
|
border-radius: 6px;
|
|
225
226
|
background: linear-gradient(90deg,
|
|
226
227
|
#e8e8e8 25%,
|
|
@@ -1855,42 +1856,87 @@ body {
|
|
|
1855
1856
|
/* ── New Session landing page (#new) ─────────────────────────────────────── */
|
|
1856
1857
|
.new-session-page {
|
|
1857
1858
|
justify-content: flex-start;
|
|
1858
|
-
padding:
|
|
1859
|
-
overflow
|
|
1859
|
+
padding: 0;
|
|
1860
|
+
overflow: hidden;
|
|
1860
1861
|
gap: 0;
|
|
1861
1862
|
}
|
|
1862
1863
|
.new-session-inner {
|
|
1863
1864
|
width: 100%;
|
|
1864
|
-
|
|
1865
|
+
height: 100%;
|
|
1865
1866
|
display: flex;
|
|
1866
1867
|
flex-direction: column;
|
|
1867
|
-
gap:
|
|
1868
|
+
gap: 0;
|
|
1869
|
+
}
|
|
1870
|
+
.new-session-body {
|
|
1871
|
+
flex: 1 1 auto;
|
|
1872
|
+
overflow-y: auto;
|
|
1873
|
+
display: flex;
|
|
1874
|
+
flex-direction: column;
|
|
1875
|
+
gap: 1.25rem;
|
|
1876
|
+
width: 100%;
|
|
1877
|
+
max-width: 60rem;
|
|
1878
|
+
margin: 0 auto;
|
|
1879
|
+
padding: 5.5rem 1.5rem 1rem;
|
|
1880
|
+
}
|
|
1881
|
+
.new-session-composer {
|
|
1882
|
+
position: relative;
|
|
1883
|
+
flex: none;
|
|
1884
|
+
padding: 0.75rem 1.5rem 1.25rem;
|
|
1868
1885
|
}
|
|
1869
1886
|
.new-session-title {
|
|
1870
1887
|
font-size: 1.5rem;
|
|
1871
1888
|
font-weight: 680;
|
|
1872
1889
|
color: var(--color-text-primary);
|
|
1873
1890
|
letter-spacing: -0.01em;
|
|
1874
|
-
text-align:
|
|
1891
|
+
text-align: left;
|
|
1875
1892
|
margin: 0;
|
|
1876
1893
|
}
|
|
1877
1894
|
.new-session-subtitle {
|
|
1878
1895
|
color: var(--color-text-tertiary);
|
|
1879
1896
|
font-size: 0.875rem;
|
|
1880
|
-
text-align:
|
|
1897
|
+
text-align: left;
|
|
1881
1898
|
margin: 0;
|
|
1882
1899
|
}
|
|
1883
1900
|
.new-session-agents {
|
|
1901
|
+
display: grid;
|
|
1902
|
+
grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
|
|
1903
|
+
grid-auto-rows: 1fr;
|
|
1904
|
+
gap: 0.75rem;
|
|
1905
|
+
}
|
|
1906
|
+
.new-session-agent-panels {
|
|
1884
1907
|
display: flex;
|
|
1885
1908
|
flex-wrap: wrap;
|
|
1886
|
-
|
|
1887
|
-
|
|
1909
|
+
align-items: center;
|
|
1910
|
+
gap: 0.375rem;
|
|
1911
|
+
margin-top: -0.625rem;
|
|
1912
|
+
padding-left: 0.125rem;
|
|
1913
|
+
}
|
|
1914
|
+
.ns-panels-label {
|
|
1915
|
+
width: 100%;
|
|
1916
|
+
font-size: 0.75rem;
|
|
1917
|
+
color: var(--color-text-tertiary);
|
|
1918
|
+
margin-bottom: 0.0625rem;
|
|
1919
|
+
}
|
|
1920
|
+
.ns-panel-chip {
|
|
1921
|
+
display: inline-flex;
|
|
1922
|
+
align-items: center;
|
|
1923
|
+
padding: 0.1875rem 0.5rem;
|
|
1924
|
+
border-radius: 0.375rem;
|
|
1925
|
+
font-size: 0.78125rem;
|
|
1926
|
+
font-weight: 500;
|
|
1927
|
+
line-height: 1.3;
|
|
1928
|
+
}
|
|
1929
|
+
.ns-panel-chip--panel {
|
|
1930
|
+
background: var(--color-accent-soft);
|
|
1931
|
+
color: var(--color-accent-primary);
|
|
1932
|
+
}
|
|
1933
|
+
.ns-panel-chip--skill {
|
|
1934
|
+
background: var(--color-bg-secondary);
|
|
1935
|
+
color: var(--color-text-secondary);
|
|
1936
|
+
border: 1px solid var(--color-border-primary);
|
|
1888
1937
|
}
|
|
1889
1938
|
.agent-card {
|
|
1890
1939
|
position: relative;
|
|
1891
|
-
flex: 1 1 12rem;
|
|
1892
|
-
min-width: 11rem;
|
|
1893
|
-
max-width: 15rem;
|
|
1894
1940
|
padding: 0.9375rem 1rem;
|
|
1895
1941
|
border: 1px solid var(--color-border-primary);
|
|
1896
1942
|
border-radius: 0.75rem;
|
|
@@ -1918,6 +1964,35 @@ body {
|
|
|
1918
1964
|
align-items: center;
|
|
1919
1965
|
gap: 0.375rem;
|
|
1920
1966
|
}
|
|
1967
|
+
.agent-card-header {
|
|
1968
|
+
display: flex;
|
|
1969
|
+
align-items: center;
|
|
1970
|
+
gap: 0.625rem;
|
|
1971
|
+
margin-bottom: 0.125rem;
|
|
1972
|
+
}
|
|
1973
|
+
.agent-card-avatar {
|
|
1974
|
+
flex: none;
|
|
1975
|
+
width: 2.25rem;
|
|
1976
|
+
height: 2.25rem;
|
|
1977
|
+
border-radius: 50%;
|
|
1978
|
+
overflow: hidden;
|
|
1979
|
+
background: var(--color-bg-hover);
|
|
1980
|
+
display: flex;
|
|
1981
|
+
align-items: center;
|
|
1982
|
+
justify-content: center;
|
|
1983
|
+
}
|
|
1984
|
+
.agent-card-avatar img {
|
|
1985
|
+
width: 100%;
|
|
1986
|
+
height: 100%;
|
|
1987
|
+
object-fit: cover;
|
|
1988
|
+
display: block;
|
|
1989
|
+
}
|
|
1990
|
+
.agent-card-avatar--fallback {
|
|
1991
|
+
font-size: 1rem;
|
|
1992
|
+
font-weight: 700;
|
|
1993
|
+
color: var(--color-accent-primary);
|
|
1994
|
+
background: var(--color-accent-soft);
|
|
1995
|
+
}
|
|
1921
1996
|
.agent-card-new {
|
|
1922
1997
|
font-size: 0.6rem;
|
|
1923
1998
|
font-weight: 700;
|
|
@@ -1951,45 +2026,77 @@ body {
|
|
|
1951
2026
|
color: var(--color-accent-primary);
|
|
1952
2027
|
}
|
|
1953
2028
|
.agent-card-author {
|
|
2029
|
+
margin-top: 0.25rem;
|
|
1954
2030
|
font-size: 0.72rem;
|
|
1955
2031
|
color: var(--color-text-tertiary);
|
|
1956
2032
|
opacity: 0.8;
|
|
1957
2033
|
}
|
|
1958
2034
|
|
|
1959
|
-
.new-session-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
gap: 0.5rem;
|
|
2035
|
+
.new-session-input-bar {
|
|
2036
|
+
margin: 0;
|
|
2037
|
+
padding: 10px 12px;
|
|
1963
2038
|
border: 1px solid var(--color-border-primary);
|
|
1964
|
-
border-radius:
|
|
1965
|
-
padding: 0.75rem;
|
|
2039
|
+
border-radius: 13px;
|
|
1966
2040
|
background: var(--color-bg-primary);
|
|
2041
|
+
display: flex;
|
|
2042
|
+
align-items: center;
|
|
2043
|
+
gap: 8px;
|
|
2044
|
+
box-shadow: var(--shadow-card);
|
|
2045
|
+
transition: border-color .14s;
|
|
2046
|
+
}
|
|
2047
|
+
.new-session-input-bar:focus-within {
|
|
2048
|
+
border-color: color-mix(in srgb, var(--color-accent-primary) 50%, var(--color-border-primary));
|
|
2049
|
+
}
|
|
2050
|
+
.new-session-composer-btn {
|
|
2051
|
+
flex: none;
|
|
2052
|
+
display: grid;
|
|
2053
|
+
place-items: center;
|
|
2054
|
+
width: 32px;
|
|
2055
|
+
height: 32px;
|
|
2056
|
+
padding: 0;
|
|
2057
|
+
border: none;
|
|
2058
|
+
border-radius: 8px;
|
|
2059
|
+
background: transparent;
|
|
2060
|
+
color: var(--color-text-tertiary);
|
|
2061
|
+
cursor: pointer;
|
|
2062
|
+
transition: background .12s, color .12s;
|
|
2063
|
+
}
|
|
2064
|
+
.new-session-composer-btn:hover {
|
|
2065
|
+
color: var(--color-text-primary);
|
|
2066
|
+
background: var(--color-bg-hover);
|
|
2067
|
+
}
|
|
2068
|
+
.new-session-composer-btn.active {
|
|
2069
|
+
color: var(--color-accent-primary);
|
|
2070
|
+
background: var(--color-accent-soft);
|
|
2071
|
+
}
|
|
2072
|
+
.new-session-composer-btn > span {
|
|
2073
|
+
font-size: 0.875rem;
|
|
2074
|
+
font-weight: 400;
|
|
2075
|
+
line-height: 1;
|
|
1967
2076
|
}
|
|
1968
2077
|
.new-session-textarea {
|
|
1969
|
-
|
|
2078
|
+
flex: 1;
|
|
1970
2079
|
border: none;
|
|
1971
2080
|
outline: none;
|
|
1972
|
-
resize:
|
|
2081
|
+
resize: none;
|
|
1973
2082
|
background: transparent;
|
|
1974
2083
|
color: var(--color-text-primary);
|
|
1975
|
-
font:
|
|
1976
|
-
font-size: 0.
|
|
1977
|
-
line-height: 1.
|
|
1978
|
-
padding:
|
|
1979
|
-
|
|
1980
|
-
max-height:
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
display: flex;
|
|
1984
|
-
align-items: center;
|
|
1985
|
-
justify-content: flex-end;
|
|
1986
|
-
gap: 0.5rem;
|
|
2084
|
+
font-family: var(--font-sans);
|
|
2085
|
+
font-size: 0.875rem;
|
|
2086
|
+
line-height: 1.55;
|
|
2087
|
+
padding: 4px 6px;
|
|
2088
|
+
margin: 0;
|
|
2089
|
+
max-height: 12.5rem;
|
|
2090
|
+
overflow-y: auto;
|
|
2091
|
+
scrollbar-width: none;
|
|
1987
2092
|
}
|
|
2093
|
+
.new-session-textarea::-webkit-scrollbar { display: none; }
|
|
1988
2094
|
.new-session-advanced-wrap {
|
|
1989
2095
|
display: flex;
|
|
1990
2096
|
flex-direction: column;
|
|
1991
2097
|
gap: 0.5rem;
|
|
1992
2098
|
align-items: flex-start;
|
|
2099
|
+
margin-top: -0.75rem;
|
|
1993
2100
|
}
|
|
1994
2101
|
.new-session-advanced-toggle {
|
|
1995
2102
|
display: inline-flex;
|
|
@@ -2006,7 +2113,6 @@ body {
|
|
|
2006
2113
|
}
|
|
2007
2114
|
.new-session-advanced-toggle:hover {
|
|
2008
2115
|
color: var(--color-text-secondary);
|
|
2009
|
-
background: var(--color-bg-hover);
|
|
2010
2116
|
}
|
|
2011
2117
|
.new-session-advanced-caret {
|
|
2012
2118
|
transition: transform 0.15s;
|
|
@@ -2015,22 +2121,30 @@ body {
|
|
|
2015
2121
|
transform: rotate(180deg);
|
|
2016
2122
|
}
|
|
2017
2123
|
.new-session-send {
|
|
2018
|
-
|
|
2124
|
+
flex: none;
|
|
2125
|
+
display: grid;
|
|
2126
|
+
place-items: center;
|
|
2127
|
+
width: 34px;
|
|
2128
|
+
height: 34px;
|
|
2019
2129
|
border: none;
|
|
2020
|
-
border-radius:
|
|
2130
|
+
border-radius: 9px;
|
|
2021
2131
|
background: var(--color-accent-primary);
|
|
2022
|
-
color:
|
|
2023
|
-
font-size: 0.875rem;
|
|
2024
|
-
font-weight: 550;
|
|
2132
|
+
color: #fff;
|
|
2025
2133
|
cursor: pointer;
|
|
2026
|
-
transition: opacity
|
|
2134
|
+
transition: background .12s, opacity .12s;
|
|
2135
|
+
}
|
|
2136
|
+
.new-session-send svg {
|
|
2137
|
+
width: 15px;
|
|
2138
|
+
height: 15px;
|
|
2027
2139
|
}
|
|
2028
2140
|
.new-session-send:disabled {
|
|
2029
|
-
|
|
2141
|
+
background: var(--color-border-primary);
|
|
2142
|
+
color: var(--color-text-tertiary);
|
|
2030
2143
|
cursor: not-allowed;
|
|
2144
|
+
opacity: 0.6;
|
|
2031
2145
|
}
|
|
2032
2146
|
.new-session-send:not(:disabled):hover {
|
|
2033
|
-
|
|
2147
|
+
background: color-mix(in srgb, var(--color-accent-primary) 85%, #fff);
|
|
2034
2148
|
}
|
|
2035
2149
|
|
|
2036
2150
|
.new-session-advanced {
|
|
@@ -2255,7 +2369,15 @@ body {
|
|
|
2255
2369
|
.session-banner:empty,
|
|
2256
2370
|
.session-composer:empty { display: none; }
|
|
2257
2371
|
.session-banner { flex-shrink: 0; }
|
|
2258
|
-
.session-composer {
|
|
2372
|
+
.session-composer {
|
|
2373
|
+
flex-shrink: 0;
|
|
2374
|
+
margin-bottom: -9px;
|
|
2375
|
+
display: flex;
|
|
2376
|
+
padding-inline: 12px;
|
|
2377
|
+
}
|
|
2378
|
+
.session-composer > * {
|
|
2379
|
+
justify-content: flex-start !important;
|
|
2380
|
+
}
|
|
2259
2381
|
|
|
2260
2382
|
/* ── File tree (used inside the Files tab) ──────────────────────────────── */
|
|
2261
2383
|
.wt-panel { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
@@ -4038,6 +4160,12 @@ body {
|
|
|
4038
4160
|
box-shadow: var(--shadow-card);
|
|
4039
4161
|
margin-bottom: -1px;
|
|
4040
4162
|
}
|
|
4163
|
+
#ns-image-preview-strip {
|
|
4164
|
+
display: flex;
|
|
4165
|
+
flex-wrap: wrap;
|
|
4166
|
+
gap: 0.5rem;
|
|
4167
|
+
padding: 0.25rem 0 0.5rem;
|
|
4168
|
+
}
|
|
4041
4169
|
/* Square off the top corners of the input-bar when the preview strip sits above it */
|
|
4042
4170
|
#input-area:has(#image-preview-strip:not([style*="display:none"])):not(:has(#image-preview-strip[style*="display: none"])) #input-bar {
|
|
4043
4171
|
border-top-left-radius: 0;
|
|
@@ -4136,7 +4264,8 @@ body {
|
|
|
4136
4264
|
.pdf-preview-remove:hover { background: var(--color-error); }
|
|
4137
4265
|
|
|
4138
4266
|
/* ── Skill autocomplete dropdown ─────────────────────────────────────────── */
|
|
4139
|
-
#skill-autocomplete
|
|
4267
|
+
#skill-autocomplete,
|
|
4268
|
+
.skill-autocomplete {
|
|
4140
4269
|
position: absolute;
|
|
4141
4270
|
bottom: 100%;
|
|
4142
4271
|
left: 0;
|
|
@@ -4154,7 +4283,8 @@ body {
|
|
|
4154
4283
|
z-index: 1000;
|
|
4155
4284
|
}
|
|
4156
4285
|
|
|
4157
|
-
#skill-autocomplete-list
|
|
4286
|
+
#skill-autocomplete-list,
|
|
4287
|
+
#ns-skill-autocomplete-list {
|
|
4158
4288
|
padding: 0.25rem 0;
|
|
4159
4289
|
}
|
|
4160
4290
|
|
|
@@ -6968,13 +7098,22 @@ body {
|
|
|
6968
7098
|
border-color: var(--color-accent, #6366f1);
|
|
6969
7099
|
}
|
|
6970
7100
|
.extensions-sort {
|
|
7101
|
+
flex-shrink: 0;
|
|
6971
7102
|
font-size: 0.8125rem;
|
|
6972
|
-
padding: 0.4375rem 0.
|
|
7103
|
+
padding: 0.4375rem 2rem 0.4375rem 0.75rem;
|
|
6973
7104
|
border-radius: 6px;
|
|
6974
7105
|
border: 1px solid var(--color-border-primary);
|
|
6975
7106
|
background: var(--color-bg-secondary);
|
|
6976
7107
|
color: var(--color-text-primary);
|
|
6977
7108
|
cursor: pointer;
|
|
7109
|
+
appearance: none;
|
|
7110
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
7111
|
+
background-repeat: no-repeat;
|
|
7112
|
+
background-position: right 0.625rem center;
|
|
7113
|
+
}
|
|
7114
|
+
.extensions-sort:focus {
|
|
7115
|
+
outline: none;
|
|
7116
|
+
border-color: var(--color-accent, #6366f1);
|
|
6978
7117
|
}
|
|
6979
7118
|
#extensions-list {
|
|
6980
7119
|
display: flex;
|
|
@@ -7065,14 +7204,23 @@ body {
|
|
|
7065
7204
|
}
|
|
7066
7205
|
.extension-detail-head { margin-bottom: 1rem; }
|
|
7067
7206
|
.extension-detail-back {
|
|
7207
|
+
display: inline-flex;
|
|
7208
|
+
align-items: center;
|
|
7209
|
+
gap: 0.25rem;
|
|
7068
7210
|
background: transparent;
|
|
7069
7211
|
border: none;
|
|
7070
7212
|
color: var(--color-text-secondary);
|
|
7071
7213
|
font-size: 0.8125rem;
|
|
7072
7214
|
cursor: pointer;
|
|
7073
|
-
padding: 0.
|
|
7215
|
+
padding: 0.3rem 0.6rem;
|
|
7216
|
+
border-radius: 6px;
|
|
7217
|
+
margin-left: -0.6rem;
|
|
7218
|
+
transition: background 0.15s, color 0.15s;
|
|
7219
|
+
}
|
|
7220
|
+
.extension-detail-back:hover {
|
|
7221
|
+
color: var(--color-text-primary);
|
|
7222
|
+
background: var(--color-bg-hover);
|
|
7074
7223
|
}
|
|
7075
|
-
.extension-detail-back:hover { color: var(--color-text-primary); }
|
|
7076
7224
|
.extension-detail-hero {
|
|
7077
7225
|
display: flex;
|
|
7078
7226
|
align-items: flex-start;
|
|
@@ -12472,5 +12620,41 @@ body.setup-mode[data-theme="dark"] {
|
|
|
12472
12620
|
#workspace-overlay.active {
|
|
12473
12621
|
display: block;
|
|
12474
12622
|
}
|
|
12623
|
+
|
|
12624
|
+
/* ── New-session page: mobile ── */
|
|
12625
|
+
#welcome.new-session-page {
|
|
12626
|
+
position: fixed;
|
|
12627
|
+
top: 3rem;
|
|
12628
|
+
left: 0;
|
|
12629
|
+
right: 0;
|
|
12630
|
+
bottom: 0;
|
|
12631
|
+
overflow: hidden;
|
|
12632
|
+
padding: 0;
|
|
12633
|
+
justify-content: flex-start;
|
|
12634
|
+
height: auto;
|
|
12635
|
+
}
|
|
12636
|
+
.new-session-inner {
|
|
12637
|
+
height: 100%;
|
|
12638
|
+
display: flex;
|
|
12639
|
+
flex-direction: column;
|
|
12640
|
+
}
|
|
12641
|
+
.new-session-spacer {
|
|
12642
|
+
display: none;
|
|
12643
|
+
}
|
|
12644
|
+
.new-session-body {
|
|
12645
|
+
flex: 1 1 auto;
|
|
12646
|
+
min-height: 0;
|
|
12647
|
+
overflow-y: auto;
|
|
12648
|
+
-webkit-overflow-scrolling: touch;
|
|
12649
|
+
padding: 1.25rem 1rem 1rem;
|
|
12650
|
+
max-width: 100%;
|
|
12651
|
+
}
|
|
12652
|
+
.new-session-composer {
|
|
12653
|
+
flex: none;
|
|
12654
|
+
position: static;
|
|
12655
|
+
padding: 0.75rem 1rem 1rem;
|
|
12656
|
+
border-top: 1px solid var(--color-border-subtle);
|
|
12657
|
+
background: var(--color-bg-primary);
|
|
12658
|
+
}
|
|
12475
12659
|
}
|
|
12476
12660
|
|
data/lib/clacky/web/core/ext.js
CHANGED
|
@@ -8,10 +8,16 @@
|
|
|
8
8
|
// Clacky.ext.subscribe(event, handler) — listen to store events (read-only)
|
|
9
9
|
// Clacky.ext.ui.mount(slot, spec, opts) — inject UI into a named slot
|
|
10
10
|
//
|
|
11
|
-
// `spec` is either a render function or { create?, render }.
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// `spec` is either a render function or { create?, render }. The render
|
|
12
|
+
// signature is always `render(container, ctx, runtime)`:
|
|
13
|
+
// - `container` is a host-owned DOM element — append your UI into it,
|
|
14
|
+
// or return a DOM node / HTML string and the host appends it for you.
|
|
15
|
+
// - returning a function registers it as a teardown callback.
|
|
16
|
+
// - returning null is treated as an error (almost always a wrong
|
|
17
|
+
// signature, e.g. `(ctx) => ...`), surfaced as a crashed placeholder.
|
|
18
|
+
// On per-session slots (session.aside/banner/composer), providing
|
|
19
|
+
// `create(ctx)` gives you one runtime per sessionId, isolated across
|
|
20
|
+
// sessions and disposed only when the session leaves the sidebar.
|
|
15
21
|
//
|
|
16
22
|
// Safety guarantees enforced here (the "constitution"):
|
|
17
23
|
// - Every extension callback is wrapped in try/catch. A throwing extension is
|
|
@@ -49,8 +55,6 @@ Clacky.ext = (() => {
|
|
|
49
55
|
// renderer becomes one tab (chrome drawn by the host), and only the active
|
|
50
56
|
// tab's body is shown. Renderers in these slots must carry opts.tab.
|
|
51
57
|
const TABBED_SLOTS = { "session.aside": true };
|
|
52
|
-
// slot => active tab id (remembered across re-renders within a page load).
|
|
53
|
-
const _activeTab = {};
|
|
54
58
|
|
|
55
59
|
// Slots that follow the current session's agent scope. A mount into any
|
|
56
60
|
// of these, made from inside a panel file, is confined to that panel's
|
|
@@ -92,6 +96,7 @@ Clacky.ext = (() => {
|
|
|
92
96
|
// Resolved by the host before the extension scripts run (see loader markers).
|
|
93
97
|
const _extAgents = {};
|
|
94
98
|
const _panelAgents = {};
|
|
99
|
+
const _agentContributions = {}; // agent id => { panels:[{id,title,title_zh}], skills:[...] }
|
|
95
100
|
let _currentPanel = null; // set while an official panel file is loading
|
|
96
101
|
|
|
97
102
|
// Current session context, kept in sync by the host on every session switch.
|
|
@@ -213,6 +218,11 @@ Clacky.ext = (() => {
|
|
|
213
218
|
Object.assign(_panelAgents, map);
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
function registerAgentContributions(map) {
|
|
222
|
+
if (PURE || !map) return;
|
|
223
|
+
Object.assign(_agentContributions, map);
|
|
224
|
+
}
|
|
225
|
+
|
|
216
226
|
const api = {
|
|
217
227
|
// Register a named data source. Host/extensions can later resolve it.
|
|
218
228
|
register(name, fn) {
|
|
@@ -376,26 +386,119 @@ Clacky.ext = (() => {
|
|
|
376
386
|
// as the view teardown, run before the next render or on session exit.
|
|
377
387
|
function _invokeRender(slot, entry, container, ctx) {
|
|
378
388
|
const runtime = _getRuntime(slot, entry, ctx);
|
|
379
|
-
let node, teardown, crashed = false;
|
|
389
|
+
let node, teardown, crashed = false, reason = null;
|
|
380
390
|
try {
|
|
381
391
|
const out = entry.render(container, ctx, runtime);
|
|
382
|
-
if (out === undefined) {
|
|
383
|
-
|
|
392
|
+
if (out === undefined || out === null) {
|
|
393
|
+
// undefined → container mutated in-place; null → renderer opted out of
|
|
394
|
+
// this context (e.g. `if (!ctx.sessionId) return null` on the new-session
|
|
395
|
+
// page). Both render nothing; neither is an error.
|
|
384
396
|
} else if (typeof out === "function") {
|
|
385
397
|
teardown = out;
|
|
386
398
|
} else {
|
|
387
399
|
node = out;
|
|
388
400
|
}
|
|
389
|
-
} catch (
|
|
401
|
+
} catch (err) {
|
|
402
|
+
reason = String((err && err.stack) || err);
|
|
403
|
+
console.error(`[Clacky.ext] extension "${entry.extId || "?"}" threw in render for slot ${JSON.stringify(slot)}:`, err);
|
|
390
404
|
crashed = true;
|
|
391
405
|
}
|
|
392
|
-
if (crashed) return { crashed: true };
|
|
406
|
+
if (crashed) return { crashed: true, reason, extId: entry.extId };
|
|
393
407
|
if (ctx && ctx.sessionId && teardown) {
|
|
394
408
|
_rememberViewTeardown(ctx.sessionId, _mountKey(slot, entry), teardown);
|
|
395
409
|
}
|
|
396
410
|
return { node };
|
|
397
411
|
}
|
|
398
412
|
|
|
413
|
+
// Build a styled, self-contained crashed-panel placeholder: a short headline
|
|
414
|
+
// plus a collapsible <details> exposing the actual reason and a nudge to the
|
|
415
|
+
// console. Never throws; the reason is rendered as plain text.
|
|
416
|
+
function _crashedPlaceholder(extId, reason) {
|
|
417
|
+
_injectCrashStyle();
|
|
418
|
+
const box = document.createElement("div");
|
|
419
|
+
box.className = "ext-slot-crashed";
|
|
420
|
+
box.setAttribute("data-ext-status", "crashed");
|
|
421
|
+
|
|
422
|
+
const head = document.createElement("div");
|
|
423
|
+
head.className = "ext-crashed-head";
|
|
424
|
+
const icon = document.createElement("span");
|
|
425
|
+
icon.className = "ext-crashed-icon";
|
|
426
|
+
icon.textContent = "!";
|
|
427
|
+
const title = document.createElement("span");
|
|
428
|
+
title.className = "ext-crashed-title";
|
|
429
|
+
title.textContent = extId ? `扩展「${extId}」渲染失败` : "扩展渲染失败";
|
|
430
|
+
head.appendChild(icon);
|
|
431
|
+
head.appendChild(title);
|
|
432
|
+
box.appendChild(head);
|
|
433
|
+
|
|
434
|
+
const hint = document.createElement("div");
|
|
435
|
+
hint.className = "ext-crashed-hint";
|
|
436
|
+
hint.textContent = "面板未能加载。展开下方详情,或打开浏览器控制台 (console) 查看完整错误。";
|
|
437
|
+
box.appendChild(hint);
|
|
438
|
+
|
|
439
|
+
if (reason) {
|
|
440
|
+
const details = document.createElement("details");
|
|
441
|
+
details.className = "ext-crashed-details";
|
|
442
|
+
const summary = document.createElement("summary");
|
|
443
|
+
summary.textContent = "错误详情";
|
|
444
|
+
const pre = document.createElement("pre");
|
|
445
|
+
pre.className = "ext-crashed-reason";
|
|
446
|
+
pre.textContent = reason;
|
|
447
|
+
details.appendChild(summary);
|
|
448
|
+
details.appendChild(pre);
|
|
449
|
+
box.appendChild(details);
|
|
450
|
+
}
|
|
451
|
+
return box;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let _crashStyleInjected = false;
|
|
455
|
+
function _injectCrashStyle() {
|
|
456
|
+
if (_crashStyleInjected) return;
|
|
457
|
+
_crashStyleInjected = true;
|
|
458
|
+
const style = document.createElement("style");
|
|
459
|
+
style.id = "ext-crashed-style";
|
|
460
|
+
style.textContent = `
|
|
461
|
+
.ext-slot-crashed {
|
|
462
|
+
margin: 12px;
|
|
463
|
+
padding: 14px 16px;
|
|
464
|
+
border: 1px solid var(--color-error-border, #fecaca);
|
|
465
|
+
background: var(--color-error-bg, #fef2f2);
|
|
466
|
+
border-radius: var(--radius-md, 8px);
|
|
467
|
+
color: var(--color-text-secondary, #56585e);
|
|
468
|
+
font-size: 13px;
|
|
469
|
+
line-height: 1.5;
|
|
470
|
+
}
|
|
471
|
+
.ext-crashed-head { display: flex; align-items: center; gap: 8px; }
|
|
472
|
+
.ext-crashed-icon {
|
|
473
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
474
|
+
width: 18px; height: 18px; flex: none;
|
|
475
|
+
border-radius: 50%;
|
|
476
|
+
background: var(--color-error, #ef4444);
|
|
477
|
+
color: #fff; font-size: 12px; font-weight: 700; line-height: 1;
|
|
478
|
+
}
|
|
479
|
+
.ext-crashed-title { font-weight: 600; color: var(--color-text-primary, #1a1b1e); }
|
|
480
|
+
.ext-crashed-hint { margin-top: 6px; color: var(--color-text-tertiary, #8a8d94); }
|
|
481
|
+
.ext-crashed-details { margin-top: 10px; }
|
|
482
|
+
.ext-crashed-details > summary {
|
|
483
|
+
cursor: pointer; user-select: none;
|
|
484
|
+
color: var(--color-error, #ef4444); font-weight: 500;
|
|
485
|
+
}
|
|
486
|
+
.ext-crashed-reason {
|
|
487
|
+
margin: 8px 0 0;
|
|
488
|
+
padding: 10px;
|
|
489
|
+
max-height: 220px; overflow: auto;
|
|
490
|
+
background: var(--color-bg-primary, #fbfbfa);
|
|
491
|
+
border: 1px solid var(--color-border-primary, #e8e8e4);
|
|
492
|
+
border-radius: var(--radius-sm, 6px);
|
|
493
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
494
|
+
font-size: 12px; line-height: 1.45;
|
|
495
|
+
white-space: pre-wrap; word-break: break-word;
|
|
496
|
+
color: var(--color-text-secondary, #56585e);
|
|
497
|
+
}
|
|
498
|
+
`;
|
|
499
|
+
(document.head || document.documentElement).appendChild(style);
|
|
500
|
+
}
|
|
501
|
+
|
|
399
502
|
|
|
400
503
|
// null agents AND null panel => global (always visible). Otherwise the
|
|
401
504
|
// current profile must be in the renderer's agent list, or in the set of
|
|
@@ -407,6 +510,13 @@ Clacky.ext = (() => {
|
|
|
407
510
|
return false;
|
|
408
511
|
}
|
|
409
512
|
|
|
513
|
+
// What a third-party agent's extension contributes (panels + skills), from
|
|
514
|
+
// ext.yml — so the new-session page can advertise it before a session exists.
|
|
515
|
+
// Returns { panels:[{id,title,title_zh}], skills:[...] } or null for built-ins.
|
|
516
|
+
function contributionsForAgent(agentId) {
|
|
517
|
+
return (agentId && _agentContributions[agentId]) || null;
|
|
518
|
+
}
|
|
519
|
+
|
|
410
520
|
// Render every extension registered for `slot` into `container`, scoped to the
|
|
411
521
|
// current agent profile. Called by the host's view layer wherever it exposes a
|
|
412
522
|
// slot, and re-called on session switch (the container is cleared first so a
|
|
@@ -436,11 +546,9 @@ Clacky.ext = (() => {
|
|
|
436
546
|
// Nav mounts that open a workspace declare it via opts.workspace; the
|
|
437
547
|
// host stamps it so the Router alone owns the active highlight.
|
|
438
548
|
if (entry.workspace) wrap.setAttribute("data-ext-workspace", entry.workspace);
|
|
439
|
-
const { node, crashed } = _invokeRender(slot, entry, wrap, renderCtx);
|
|
549
|
+
const { node, crashed, reason, extId } = _invokeRender(slot, entry, wrap, renderCtx);
|
|
440
550
|
if (crashed) {
|
|
441
|
-
wrap.
|
|
442
|
-
wrap.className = "ext-slot-crashed";
|
|
443
|
-
wrap.textContent = "Extension failed to render.";
|
|
551
|
+
wrap.appendChild(_crashedPlaceholder(extId || entry.extId, reason));
|
|
444
552
|
container.appendChild(wrap);
|
|
445
553
|
return;
|
|
446
554
|
}
|
|
@@ -478,17 +586,16 @@ Clacky.ext = (() => {
|
|
|
478
586
|
const bodies = document.createElement("div");
|
|
479
587
|
bodies.className = "aside-bodies";
|
|
480
588
|
|
|
481
|
-
//
|
|
589
|
+
// Always default to the first (highest-priority) tab. Tabs may register in
|
|
590
|
+
// several async passes; whichever pass runs last simply lands on tab #1.
|
|
482
591
|
const ids = renderers.map((e) => e.tab.id);
|
|
483
|
-
|
|
484
|
-
if (!active || !ids.includes(active)) active = ids[0];
|
|
592
|
+
const active = ids[0];
|
|
485
593
|
|
|
486
594
|
const tabBtns = {};
|
|
487
595
|
const bodyEls = {};
|
|
488
596
|
const rendered = {};
|
|
489
597
|
|
|
490
598
|
function activate(id) {
|
|
491
|
-
_activeTab[slot] = id;
|
|
492
599
|
Object.keys(tabBtns).forEach((k) => tabBtns[k].classList.toggle("active", k === id));
|
|
493
600
|
Object.keys(bodyEls).forEach((k) => bodyEls[k].classList.toggle("active", k === id));
|
|
494
601
|
if (!rendered[id]) {
|
|
@@ -498,12 +605,9 @@ Clacky.ext = (() => {
|
|
|
498
605
|
const localCtx = Object.assign({}, renderCtx, {
|
|
499
606
|
setBadge: (n) => _setTabBadge(tabBtns[id], n),
|
|
500
607
|
});
|
|
501
|
-
const { node, crashed } = _invokeRender(slot, entry, body, localCtx);
|
|
608
|
+
const { node, crashed, reason, extId } = _invokeRender(slot, entry, body, localCtx);
|
|
502
609
|
if (crashed) {
|
|
503
|
-
|
|
504
|
-
ph.className = "ext-slot-crashed";
|
|
505
|
-
ph.textContent = "Extension failed to render.";
|
|
506
|
-
body.appendChild(ph);
|
|
610
|
+
body.appendChild(_crashedPlaceholder(extId || entry.extId, reason));
|
|
507
611
|
} else if (node != null) {
|
|
508
612
|
if (typeof node === "string") body.innerHTML = node;
|
|
509
613
|
else body.appendChild(node);
|
|
@@ -608,6 +712,8 @@ Clacky.ext = (() => {
|
|
|
608
712
|
notifySessionRemoved,
|
|
609
713
|
refreshSlots,
|
|
610
714
|
registerPanelAgents,
|
|
715
|
+
registerAgentContributions,
|
|
716
|
+
contributionsForAgent,
|
|
611
717
|
api,
|
|
612
718
|
ui,
|
|
613
719
|
subscribe,
|
|
@@ -21,8 +21,8 @@ const ExtensionsView = (() => {
|
|
|
21
21
|
container.innerHTML = Array.from({ length: 4 }).map(() => `
|
|
22
22
|
<div class="extension-card">
|
|
23
23
|
<div class="extension-card-main">
|
|
24
|
-
<span class="skel" style="height:2rem;width:2rem;border-radius:8px;"></span>
|
|
25
|
-
<div class="extension-info">
|
|
24
|
+
<span class="skel" style="height:2rem;width:2rem;border-radius:8px;flex-shrink:0"></span>
|
|
25
|
+
<div class="extension-info" style="display:flex;flex-direction:column;gap:0.375rem">
|
|
26
26
|
<span class="skel skel-title"></span>
|
|
27
27
|
<span class="skel skel-subtitle"></span>
|
|
28
28
|
</div>
|
|
@@ -161,7 +161,8 @@ const ExtensionsView = (() => {
|
|
|
161
161
|
return `
|
|
162
162
|
<div class="extension-detail-head">
|
|
163
163
|
<button type="button" class="extension-detail-back" id="extension-detail-back">
|
|
164
|
-
|
|
164
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 2L4 7L9 12" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
165
|
+
${escapeHtml(I18n.t("extensions.detail.back"))}
|
|
165
166
|
</button>
|
|
166
167
|
</div>
|
|
167
168
|
${inner}`;
|