@vibe80/vibe80 0.1.1
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.
- package/LICENSE +201 -0
- package/README.md +52 -0
- package/bin/vibe80.js +176 -0
- package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
- package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
- package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
- package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
- package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
- package/client/dist/assets/browser-e3WgtMs-.js +8 -0
- package/client/dist/assets/index-CgqGyssr.css +32 -0
- package/client/dist/assets/index-DnwKjoj7.js +706 -0
- package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
- package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/favicon.png +0 -0
- package/client/dist/favicon.svg +35 -0
- package/client/dist/index.html +14 -0
- package/client/index.html +16 -0
- package/client/package.json +34 -0
- package/client/public/favicon.ico +0 -0
- package/client/public/favicon.png +0 -0
- package/client/public/favicon.svg +35 -0
- package/client/public/pwa-192x192.png +0 -0
- package/client/public/pwa-512x512.png +0 -0
- package/client/src/App.jsx +3131 -0
- package/client/src/assets/logo_small.png +0 -0
- package/client/src/assets/vibe80_dark.svg +51 -0
- package/client/src/assets/vibe80_light.svg +50 -0
- package/client/src/components/Chat/ChatComposer.jsx +228 -0
- package/client/src/components/Chat/ChatMessages.jsx +811 -0
- package/client/src/components/Chat/ChatToolbar.jsx +109 -0
- package/client/src/components/Chat/useChatComposer.js +462 -0
- package/client/src/components/Diff/DiffPanel.jsx +129 -0
- package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
- package/client/src/components/Logs/LogsPanel.jsx +80 -0
- package/client/src/components/SessionGate/SessionGate.jsx +874 -0
- package/client/src/components/Settings/SettingsPanel.jsx +212 -0
- package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
- package/client/src/components/Topbar/Topbar.jsx +101 -0
- package/client/src/components/WorktreeTabs.css +419 -0
- package/client/src/components/WorktreeTabs.jsx +604 -0
- package/client/src/hooks/useAttachments.jsx +125 -0
- package/client/src/hooks/useBacklog.js +254 -0
- package/client/src/hooks/useChatClear.js +90 -0
- package/client/src/hooks/useChatCollapse.js +42 -0
- package/client/src/hooks/useChatCommands.js +294 -0
- package/client/src/hooks/useChatExport.js +144 -0
- package/client/src/hooks/useChatMessagesState.js +69 -0
- package/client/src/hooks/useChatSend.js +158 -0
- package/client/src/hooks/useChatSocket.js +1239 -0
- package/client/src/hooks/useDiffNavigation.js +19 -0
- package/client/src/hooks/useExplorerActions.js +1184 -0
- package/client/src/hooks/useGitIdentity.js +114 -0
- package/client/src/hooks/useLayoutMode.js +31 -0
- package/client/src/hooks/useLocalPreferences.js +131 -0
- package/client/src/hooks/useMessageSync.js +30 -0
- package/client/src/hooks/useNotifications.js +132 -0
- package/client/src/hooks/usePaneNavigation.js +67 -0
- package/client/src/hooks/usePanelState.js +13 -0
- package/client/src/hooks/useProviderSelection.js +70 -0
- package/client/src/hooks/useRepoBranchesModels.js +218 -0
- package/client/src/hooks/useRepoStatus.js +350 -0
- package/client/src/hooks/useRpcLogActions.js +19 -0
- package/client/src/hooks/useRpcLogView.js +58 -0
- package/client/src/hooks/useSessionHandoff.js +97 -0
- package/client/src/hooks/useSessionLifecycle.js +287 -0
- package/client/src/hooks/useSessionReset.js +63 -0
- package/client/src/hooks/useSessionResync.js +77 -0
- package/client/src/hooks/useTerminalSession.js +328 -0
- package/client/src/hooks/useToolbarExport.js +27 -0
- package/client/src/hooks/useTurnInterrupt.js +43 -0
- package/client/src/hooks/useVibe80Forms.js +128 -0
- package/client/src/hooks/useWorkspaceAuth.js +932 -0
- package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
- package/client/src/hooks/useWorktrees.js +396 -0
- package/client/src/i18n.jsx +87 -0
- package/client/src/index.css +5147 -0
- package/client/src/locales/en.json +37 -0
- package/client/src/locales/fr.json +321 -0
- package/client/src/main.jsx +16 -0
- package/client/vite.config.js +62 -0
- package/docs/api/asyncapi.json +1511 -0
- package/docs/api/openapi.json +3242 -0
- package/git_hooks/prepare-commit-msg +35 -0
- package/package.json +36 -0
- package/server/package.json +29 -0
- package/server/scripts/rotate-workspace-secret.js +101 -0
- package/server/src/claudeClient.js +454 -0
- package/server/src/clientEvents.js +594 -0
- package/server/src/clientFactory.js +164 -0
- package/server/src/codexClient.js +468 -0
- package/server/src/config.js +27 -0
- package/server/src/helpers.js +138 -0
- package/server/src/index.js +1641 -0
- package/server/src/middleware/auth.js +93 -0
- package/server/src/middleware/debug.js +89 -0
- package/server/src/middleware/errorTypes.js +60 -0
- package/server/src/providerLogger.js +60 -0
- package/server/src/routes/files.js +114 -0
- package/server/src/routes/git.js +183 -0
- package/server/src/routes/health.js +13 -0
- package/server/src/routes/sessions.js +407 -0
- package/server/src/routes/workspaces.js +296 -0
- package/server/src/routes/worktrees.js +993 -0
- package/server/src/runAs.js +458 -0
- package/server/src/runtimeStore.js +32 -0
- package/server/src/services/auth.js +157 -0
- package/server/src/services/claudeThreadDirectory.js +33 -0
- package/server/src/services/session.js +918 -0
- package/server/src/services/workspace.js +858 -0
- package/server/src/storage/index.js +17 -0
- package/server/src/storage/redis.js +412 -0
- package/server/src/storage/sqlite.js +649 -0
- package/server/src/worktreeManager.js +717 -0
- package/server/tests/README.md +13 -0
- package/server/tests/factories/workspaceFactory.js +13 -0
- package/server/tests/fixtures/workspaceCredentials.json +4 -0
- package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
- package/server/tests/setup/env.js +9 -0
- package/server/tests/unit/helpers.test.js +95 -0
- package/server/tests/unit/services/auth.test.js +181 -0
- package/server/tests/unit/services/workspace.test.js +115 -0
- package/server/vitest.config.js +23 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
3
|
+
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons";
|
|
4
|
+
|
|
5
|
+
export default function SettingsPanel({
|
|
6
|
+
t,
|
|
7
|
+
activePane,
|
|
8
|
+
handleSettingsBack,
|
|
9
|
+
language,
|
|
10
|
+
setLanguage,
|
|
11
|
+
showChatCommands,
|
|
12
|
+
setShowChatCommands,
|
|
13
|
+
showToolResults,
|
|
14
|
+
setShowToolResults,
|
|
15
|
+
notificationsEnabled,
|
|
16
|
+
setNotificationsEnabled,
|
|
17
|
+
themeMode,
|
|
18
|
+
setThemeMode,
|
|
19
|
+
gitIdentityName,
|
|
20
|
+
setGitIdentityName,
|
|
21
|
+
gitIdentityEmail,
|
|
22
|
+
setGitIdentityEmail,
|
|
23
|
+
gitIdentityGlobal,
|
|
24
|
+
gitIdentityRepo,
|
|
25
|
+
gitIdentityLoading,
|
|
26
|
+
gitIdentitySaving,
|
|
27
|
+
gitIdentityError,
|
|
28
|
+
gitIdentityMessage,
|
|
29
|
+
handleSaveGitIdentity,
|
|
30
|
+
attachmentSession,
|
|
31
|
+
}) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={`settings-panel ${activePane === "settings" ? "" : "is-hidden"}`}>
|
|
34
|
+
<div className="settings-header">
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
className="settings-back icon-button"
|
|
38
|
+
onClick={handleSettingsBack}
|
|
39
|
+
aria-label={t("Back to previous view")}
|
|
40
|
+
title={t("Back")}
|
|
41
|
+
>
|
|
42
|
+
<span aria-hidden="true">
|
|
43
|
+
<FontAwesomeIcon icon={faArrowLeft} />
|
|
44
|
+
</span>
|
|
45
|
+
</button>
|
|
46
|
+
<div className="settings-heading">
|
|
47
|
+
<div className="settings-title">{t("User settings")}</div>
|
|
48
|
+
<div className="settings-subtitle">
|
|
49
|
+
{t("These settings are stored in your browser.")}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="settings-group">
|
|
54
|
+
<label className="settings-item">
|
|
55
|
+
<span className="settings-text">
|
|
56
|
+
<span className="settings-name">{t("Language")}</span>
|
|
57
|
+
<span className="settings-hint">{t("Select a language")}</span>
|
|
58
|
+
</span>
|
|
59
|
+
<select
|
|
60
|
+
className="settings-select"
|
|
61
|
+
value={language}
|
|
62
|
+
onChange={(event) => setLanguage(event.target.value)}
|
|
63
|
+
>
|
|
64
|
+
<option value="fr">{t("French")}</option>
|
|
65
|
+
<option value="en">{t("English")}</option>
|
|
66
|
+
</select>
|
|
67
|
+
</label>
|
|
68
|
+
<label className="settings-item">
|
|
69
|
+
<span className="settings-text">
|
|
70
|
+
<span className="settings-name">{t("Show commands in chat")}</span>
|
|
71
|
+
<span className="settings-hint">
|
|
72
|
+
{t("Show executed command blocks in the conversation.")}
|
|
73
|
+
</span>
|
|
74
|
+
</span>
|
|
75
|
+
<input
|
|
76
|
+
type="checkbox"
|
|
77
|
+
className="settings-toggle"
|
|
78
|
+
checked={showChatCommands}
|
|
79
|
+
onChange={(event) => setShowChatCommands(event.target.checked)}
|
|
80
|
+
/>
|
|
81
|
+
</label>
|
|
82
|
+
<label className="settings-item">
|
|
83
|
+
<span className="settings-text">
|
|
84
|
+
<span className="settings-name">{t("Show tool results in chat")}</span>
|
|
85
|
+
<span className="settings-hint">
|
|
86
|
+
{t("Show tool_result blocks in the conversation.")}
|
|
87
|
+
</span>
|
|
88
|
+
</span>
|
|
89
|
+
<input
|
|
90
|
+
type="checkbox"
|
|
91
|
+
className="settings-toggle"
|
|
92
|
+
checked={showToolResults}
|
|
93
|
+
onChange={(event) => setShowToolResults(event.target.checked)}
|
|
94
|
+
/>
|
|
95
|
+
</label>
|
|
96
|
+
<label className="settings-item">
|
|
97
|
+
<span className="settings-text">
|
|
98
|
+
<span className="settings-name">{t("Notifications")}</span>
|
|
99
|
+
<span className="settings-hint">
|
|
100
|
+
{t("Show a notification and sound when a new message arrives.")}
|
|
101
|
+
</span>
|
|
102
|
+
</span>
|
|
103
|
+
<input
|
|
104
|
+
type="checkbox"
|
|
105
|
+
className="settings-toggle"
|
|
106
|
+
checked={notificationsEnabled}
|
|
107
|
+
onChange={(event) => setNotificationsEnabled(event.target.checked)}
|
|
108
|
+
/>
|
|
109
|
+
</label>
|
|
110
|
+
<label className="settings-item">
|
|
111
|
+
<span className="settings-text">
|
|
112
|
+
<span className="settings-name">{t("Dark mode")}</span>
|
|
113
|
+
<span className="settings-hint">
|
|
114
|
+
{t("Enable the dark theme for the interface.")}
|
|
115
|
+
</span>
|
|
116
|
+
</span>
|
|
117
|
+
<input
|
|
118
|
+
type="checkbox"
|
|
119
|
+
className="settings-toggle"
|
|
120
|
+
checked={themeMode === "dark"}
|
|
121
|
+
onChange={(event) =>
|
|
122
|
+
setThemeMode(event.target.checked ? "dark" : "light")
|
|
123
|
+
}
|
|
124
|
+
/>
|
|
125
|
+
</label>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="settings-group">
|
|
128
|
+
<div className="settings-item settings-item--stacked">
|
|
129
|
+
<div className="settings-text">
|
|
130
|
+
<span className="settings-name">
|
|
131
|
+
{t("Git identity for this repository")}
|
|
132
|
+
</span>
|
|
133
|
+
<span className="settings-hint">
|
|
134
|
+
{t("Provide user.name and user.email for repository commits.")}
|
|
135
|
+
</span>
|
|
136
|
+
<span className="settings-hint">
|
|
137
|
+
{t("Global values: {{name}} / {{email}}.", {
|
|
138
|
+
name: gitIdentityGlobal.name || t("Not set"),
|
|
139
|
+
email: gitIdentityGlobal.email || t("Not set"),
|
|
140
|
+
})}
|
|
141
|
+
</span>
|
|
142
|
+
<span className="settings-hint">
|
|
143
|
+
{gitIdentityRepo.name || gitIdentityRepo.email
|
|
144
|
+
? t("Repository values: {{name}} / {{email}}.", {
|
|
145
|
+
name: gitIdentityRepo.name || t("Not set"),
|
|
146
|
+
email: gitIdentityRepo.email || t("Not set"),
|
|
147
|
+
})
|
|
148
|
+
: t("No repository-specific values.")}
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="settings-fields">
|
|
152
|
+
<label className="settings-field">
|
|
153
|
+
<span className="settings-field-label">{t("user.name")}</span>
|
|
154
|
+
<input
|
|
155
|
+
type="text"
|
|
156
|
+
className="settings-input"
|
|
157
|
+
value={gitIdentityName}
|
|
158
|
+
onChange={(event) => setGitIdentityName(event.target.value)}
|
|
159
|
+
placeholder={gitIdentityGlobal.name || t("Full name")}
|
|
160
|
+
disabled={
|
|
161
|
+
gitIdentityLoading ||
|
|
162
|
+
gitIdentitySaving ||
|
|
163
|
+
!attachmentSession?.sessionId
|
|
164
|
+
}
|
|
165
|
+
/>
|
|
166
|
+
</label>
|
|
167
|
+
<label className="settings-field">
|
|
168
|
+
<span className="settings-field-label">{t("user.email")}</span>
|
|
169
|
+
<input
|
|
170
|
+
type="email"
|
|
171
|
+
className="settings-input"
|
|
172
|
+
value={gitIdentityEmail}
|
|
173
|
+
onChange={(event) => setGitIdentityEmail(event.target.value)}
|
|
174
|
+
placeholder={
|
|
175
|
+
gitIdentityGlobal.email || t("your.email@example.com")
|
|
176
|
+
}
|
|
177
|
+
disabled={
|
|
178
|
+
gitIdentityLoading ||
|
|
179
|
+
gitIdentitySaving ||
|
|
180
|
+
!attachmentSession?.sessionId
|
|
181
|
+
}
|
|
182
|
+
/>
|
|
183
|
+
</label>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="settings-actions">
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
className="settings-button"
|
|
189
|
+
onClick={handleSaveGitIdentity}
|
|
190
|
+
disabled={
|
|
191
|
+
gitIdentityLoading ||
|
|
192
|
+
gitIdentitySaving ||
|
|
193
|
+
!attachmentSession?.sessionId
|
|
194
|
+
}
|
|
195
|
+
>
|
|
196
|
+
{gitIdentitySaving ? t("Saving...") : t("Save")}
|
|
197
|
+
</button>
|
|
198
|
+
{gitIdentityLoading ? (
|
|
199
|
+
<span className="settings-status">{t("Loading...")}</span>
|
|
200
|
+
) : null}
|
|
201
|
+
{gitIdentityError ? (
|
|
202
|
+
<span className="settings-status is-error">{gitIdentityError}</span>
|
|
203
|
+
) : null}
|
|
204
|
+
{gitIdentityMessage ? (
|
|
205
|
+
<span className="settings-status">{gitIdentityMessage}</span>
|
|
206
|
+
) : null}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function TerminalPanel({
|
|
4
|
+
t,
|
|
5
|
+
terminalEnabled,
|
|
6
|
+
activePane,
|
|
7
|
+
repoName,
|
|
8
|
+
activeWorktree,
|
|
9
|
+
isInWorktree,
|
|
10
|
+
terminalContainerRef,
|
|
11
|
+
attachmentSession,
|
|
12
|
+
}) {
|
|
13
|
+
if (!terminalEnabled) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={`terminal-panel ${activePane === "terminal" ? "" : "is-hidden"}`}
|
|
20
|
+
>
|
|
21
|
+
<div className="terminal-header">
|
|
22
|
+
<div className="terminal-title">{t("Terminal")}</div>
|
|
23
|
+
{(repoName || activeWorktree?.branchName || activeWorktree?.name) && (
|
|
24
|
+
<div className="terminal-meta">
|
|
25
|
+
{isInWorktree
|
|
26
|
+
? activeWorktree?.branchName || activeWorktree?.name
|
|
27
|
+
: repoName}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
<div className="terminal-body" ref={terminalContainerRef} />
|
|
32
|
+
{!attachmentSession?.sessionId && (
|
|
33
|
+
<div className="terminal-empty">
|
|
34
|
+
{t("Start a session to open the terminal.")}
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
3
|
+
import { faGear, faQrcode, faRightFromBracket } from "@fortawesome/free-solid-svg-icons";
|
|
4
|
+
import WorktreeTabs from "../WorktreeTabs.jsx";
|
|
5
|
+
|
|
6
|
+
export default function Topbar({
|
|
7
|
+
t,
|
|
8
|
+
brandLogo,
|
|
9
|
+
allTabs,
|
|
10
|
+
activeWorktreeId,
|
|
11
|
+
handleSelectWorktree,
|
|
12
|
+
createWorktree,
|
|
13
|
+
openCloseConfirm,
|
|
14
|
+
renameWorktreeHandler,
|
|
15
|
+
llmProvider,
|
|
16
|
+
availableProviders,
|
|
17
|
+
branches,
|
|
18
|
+
defaultBranch,
|
|
19
|
+
currentBranch,
|
|
20
|
+
branchLoading,
|
|
21
|
+
branchError,
|
|
22
|
+
defaultInternetAccess,
|
|
23
|
+
defaultDenyGitCredentialsAccess,
|
|
24
|
+
deploymentMode,
|
|
25
|
+
loadBranches,
|
|
26
|
+
providerModelState,
|
|
27
|
+
loadProviderModels,
|
|
28
|
+
connected,
|
|
29
|
+
isMobileLayout,
|
|
30
|
+
requestHandoffQr,
|
|
31
|
+
attachmentSession,
|
|
32
|
+
handoffLoading,
|
|
33
|
+
handleOpenSettings,
|
|
34
|
+
handleLeaveSession,
|
|
35
|
+
}) {
|
|
36
|
+
return (
|
|
37
|
+
<header className="header">
|
|
38
|
+
<div className="topbar-left">
|
|
39
|
+
<div className="topbar-spacer" />
|
|
40
|
+
<div className="topbar-brand">
|
|
41
|
+
<img className="brand-logo" src={brandLogo} alt="vibe80" />
|
|
42
|
+
</div>
|
|
43
|
+
<div className="topbar-tabs">
|
|
44
|
+
<WorktreeTabs
|
|
45
|
+
worktrees={allTabs}
|
|
46
|
+
activeWorktreeId={activeWorktreeId}
|
|
47
|
+
onSelect={handleSelectWorktree}
|
|
48
|
+
onCreate={createWorktree}
|
|
49
|
+
onClose={openCloseConfirm}
|
|
50
|
+
onRename={renameWorktreeHandler}
|
|
51
|
+
provider={llmProvider}
|
|
52
|
+
providers={
|
|
53
|
+
availableProviders.length ? availableProviders : [llmProvider]
|
|
54
|
+
}
|
|
55
|
+
branches={branches}
|
|
56
|
+
defaultBranch={defaultBranch || currentBranch}
|
|
57
|
+
branchLoading={branchLoading}
|
|
58
|
+
branchError={branchError}
|
|
59
|
+
defaultInternetAccess={defaultInternetAccess}
|
|
60
|
+
defaultDenyGitCredentialsAccess={defaultDenyGitCredentialsAccess}
|
|
61
|
+
deploymentMode={deploymentMode}
|
|
62
|
+
onRefreshBranches={loadBranches}
|
|
63
|
+
providerModelState={providerModelState}
|
|
64
|
+
onRequestProviderModels={loadProviderModels}
|
|
65
|
+
disabled={!connected}
|
|
66
|
+
isMobile={isMobileLayout}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className="topbar-right">
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
className="icon-button"
|
|
75
|
+
aria-label={t("Resume on mobile")}
|
|
76
|
+
title={t("Resume on mobile")}
|
|
77
|
+
onClick={requestHandoffQr}
|
|
78
|
+
disabled={!attachmentSession?.sessionId || handoffLoading}
|
|
79
|
+
>
|
|
80
|
+
<FontAwesomeIcon icon={faQrcode} />
|
|
81
|
+
</button>
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
className="icon-button"
|
|
85
|
+
aria-label={t("Open settings")}
|
|
86
|
+
onClick={handleOpenSettings}
|
|
87
|
+
>
|
|
88
|
+
<FontAwesomeIcon icon={faGear} />
|
|
89
|
+
</button>
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
className="icon-button"
|
|
93
|
+
aria-label={t("Leave session")}
|
|
94
|
+
onClick={handleLeaveSession}
|
|
95
|
+
>
|
|
96
|
+
<FontAwesomeIcon icon={faRightFromBracket} />
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
</header>
|
|
100
|
+
);
|
|
101
|
+
}
|