11<script lang =" ts" >
22import {
3+ type GitHubSourceResourceType ,
34 type SignetSourceEntry ,
45 addDiscordSource ,
6+ addGitHubSource ,
57 addObsidianSource ,
68 getSources ,
79 pickSourceDirectory ,
@@ -137,10 +139,32 @@ let discordIncludeEmbeds = $state(true);
137139let discordIncludePolls = $state (true );
138140// biome-ignore lint/style/useConst: Svelte bind:checked mutates this rune from markup.
139141let discordIncludeThreadMembers = $state (true );
142+ // biome-ignore lint/style/useConst: Svelte bind:value mutates this rune from markup.
143+ let githubName = $state (" GitHub" );
144+ let githubReposText = $state (" " );
145+ let githubTokenRef = $state (" " );
146+ // biome-ignore lint/style/useConst: Svelte bind:value mutates this rune from markup.
147+ let githubState = $state <" open" | " closed" | " all" >(" all" );
148+ // biome-ignore lint/style/useConst: Svelte bind:checked mutates this rune from markup.
149+ let githubIncludeIssues = $state (true );
150+ // biome-ignore lint/style/useConst: Svelte bind:checked mutates this rune from markup.
151+ let githubIncludePulls = $state (true );
152+ // biome-ignore lint/style/useConst: Svelte bind:checked mutates this rune from markup.
153+ let githubIncludeDiscussions = $state (false );
154+ // biome-ignore lint/style/useConst: Svelte bind:checked mutates this rune from markup.
155+ let githubIncludeDocs = $state (true );
156+ // biome-ignore lint/style/useConst: Svelte bind:checked mutates this rune from markup.
157+ let githubIncludeComments = $state (true );
158+ let githubLabelsText = $state (" " );
159+ // biome-ignore lint/style/useConst: Svelte bind:value mutates this rune from markup.
160+ let githubDocPathsText = $state (" README.md\n CHANGELOG.md" );
161+ // biome-ignore lint/style/useConst: Svelte bind:value mutates this rune from markup.
162+ let githubMaxItems = $state (500 );
140163let status = $state <string | null >(null );
141164let error = $state <string | null >(null );
142165let touchedPath = $state (false );
143166let touchedDiscord = $state (false );
167+ let touchedGithub = $state (false );
144168let expandedKind = $state <SourceKind | null >(null );
145169let sourceRefreshTimer: ReturnType <typeof setInterval > | null = null ;
146170
@@ -166,12 +190,13 @@ const connectors: SourceConnector[] = [
166190 kind: " github" ,
167191 name: " GitHub Repositories" ,
168192 detail: " Issues, PRs, code, docs" ,
169- description: " Bring repository context, issues, pull requests, and project docs into recall." ,
193+ description:
194+ " Connect public or private repositories so issues, pull requests, discussions, comments, and docs stay available as source-backed recall." ,
170195 icon: " github" ,
171196 category: " code" ,
172- tags: [" code" , " cloud" ],
173- status: " planned " ,
174- indexes: [" Repository files " , " Issues and PRs " , " Project documentation " ],
197+ tags: [" code" , " cloud" , " issues " , " pull-requests " , " docs " ],
198+ status: " available " ,
199+ indexes: [" Issues and pull requests " , " Discussion and issue comments " , " README and docs paths " ],
175200 never: [" Push changes without an explicit action" ],
176201 learnMore: [],
177202 },
@@ -475,26 +500,45 @@ const filters: Array<{ id: ActiveFilter; label: string }> = [
475500const selectedConnector = $derived (connectors .find ((connector ) => connector .kind === selectedKind ) ?? connectors [0 ]);
476501const obsidianSources = $derived (sources .filter ((source ) => source .kind === " obsidian" ));
477502const discordSources = $derived (sources .filter ((source ) => source .kind === " discord" ));
478- const connectedCount = $derived (obsidianSources .length + discordSources .length );
503+ const githubSources = $derived (sources .filter ((source ) => source .kind === " github" ));
504+ const connectedSourceList = $derived ([... obsidianSources , ... discordSources , ... githubSources ]);
505+ const connectedCount = $derived (connectedSourceList .length );
479506const indexedCount = $derived (
480- [... obsidianSources , ... discordSources ].filter ((source ) => source .lastIndexedAt || (source .stats ?.indexed ?? 0 ) > 0 )
481- .length ,
507+ connectedSourceList .filter ((source ) => source .lastIndexedAt || (source .stats ?.indexed ?? 0 ) > 0 ).length ,
482508);
483509const hasActiveIndexJob = $derived (
484- [... obsidianSources , ... discordSources ].some (
485- (source ) => source .indexJob ?.status === " queued" || source .indexJob ?.status === " running" ,
486- ),
510+ connectedSourceList .some ((source ) => source .indexJob ?.status === " queued" || source .indexJob ?.status === " running" ),
487511);
488512const pathIsMissing = $derived (vaultPath .trim ().length === 0 );
489513const discordGuildIds = $derived (parseListInput (discordGuildIdsText ));
490514const discordChannelFilter = $derived (parseListInput (discordChannelFilterText ));
491515const discordUsesDesktopCache = $derived (discordSyncMode === " desktop-cache" );
492516const discordTokenMissing = $derived (discordTokenRef .trim ().length === 0 );
493517const discordGuildsMissing = $derived (discordGuildIds .length === 0 );
518+ const githubRepos = $derived (parseListInput (githubReposText ));
519+ const githubLabels = $derived (parseListInput (githubLabelsText ));
520+ const githubDocPaths = $derived (parseListInput (githubDocPathsText ));
521+ const githubResourceTypes = $derived .by (() => {
522+ const types: GitHubSourceResourceType [] = [];
523+ if (githubIncludeIssues ) types .push (" issues" );
524+ if (githubIncludePulls ) types .push (" pulls" );
525+ if (githubIncludeDiscussions ) types .push (" discussions" );
526+ if (githubIncludeDocs ) types .push (" docs" );
527+ return types ;
528+ });
529+ const githubReposMissing = $derived (githubRepos .length === 0 );
530+ const githubResourceTypesMissing = $derived (githubResourceTypes .length === 0 );
531+ const githubTokenMissingForDiscussions = $derived (githubIncludeDiscussions && githubTokenRef .trim ().length === 0 );
532+ const githubMaxItemsInvalid = $derived (! Number .isInteger (Number (githubMaxItems )) || Number (githubMaxItems ) < 1 );
494533const canSubmit = $derived .by (() => {
495534 if (adding ) return false ;
496535 if (selectedKind === " obsidian" ) return ! pathIsMissing ;
497536 if (selectedKind === " discord" ) return discordUsesDesktopCache || (! discordGuildsMissing && ! discordTokenMissing );
537+ if (selectedKind === " github" ) {
538+ return (
539+ ! githubReposMissing && ! githubResourceTypesMissing && ! githubTokenMissingForDiscussions && ! githubMaxItemsInvalid
540+ );
541+ }
498542 return false ;
499543});
500544const filteredConnectors = $derived .by (() => {
@@ -540,6 +584,7 @@ function selectConnector(kind: SourceKind): void {
540584 error = null ;
541585 touchedPath = false ;
542586 touchedDiscord = false ;
587+ touchedGithub = false ;
543588}
544589
545590async function chooseFolder(): Promise <void > {
@@ -664,8 +709,43 @@ async function submitDiscordSource(): Promise<void> {
664709 }
665710}
666711
712+ async function submitGitHubSource(): Promise <void > {
713+ touchedGithub = true ;
714+ if (! canSubmit ) return ;
715+ adding = true ;
716+ status = null ;
717+ error = null ;
718+ try {
719+ const result = await addGitHubSource ({
720+ repos: githubRepos ,
721+ tokenRef: githubTokenRef .trim () || undefined ,
722+ name: githubName .trim () || undefined ,
723+ resourceTypes: githubResourceTypes ,
724+ state: githubState ,
725+ includeComments: githubIncludeComments ,
726+ labels: githubLabels .length > 0 ? githubLabels : undefined ,
727+ docPaths: githubDocPaths ,
728+ maxItemsPerRepo: Number (githubMaxItems ),
729+ });
730+ if (result .error ) {
731+ error = result .error ;
732+ return ;
733+ }
734+ status = ` ${result .created ? " Connected" : " Updated" } ${result .source .name }. GitHub indexing is running in the background. ` ;
735+ githubReposText = " " ;
736+ githubTokenRef = " " ;
737+ githubLabelsText = " " ;
738+ touchedGithub = false ;
739+ connectMode = false ;
740+ await refreshSources ();
741+ } finally {
742+ adding = false ;
743+ }
744+ }
745+
667746async function disconnectSource(source : SignetSourceEntry ): Promise <void > {
668- const originalLabel = source .kind === " discord" ? " Discord data" : " vault files" ;
747+ const originalLabel =
748+ source .kind === " discord" ? " Discord data" : source .kind === " github" ? " GitHub data" : " vault files" ;
669749 const confirmed = window .confirm (
670750 ` Remove ${source .name } from Signet?\n\n This purges Signet's indexed source rows and chunks, but leaves the original ${originalLabel } untouched. ` ,
671751 );
@@ -718,7 +798,7 @@ function sourceScanLabel(source: SignetSourceEntry): string {
718798 return scanned > 0 ? ` Indexing · ${scanned } scanned · ${indexed } changed ` : " Indexing in background" ;
719799 }
720800 if (source .indexJob ?.status === " error" ) return ` Index failed: ${source .indexJob .error ?? " unknown error" } ` ;
721- const itemLabel = source .kind === " obsidian" ? " notes" : " artifacts" ;
801+ const itemLabel = source .kind === " obsidian" ? " notes" : source . kind === " github " ? " items " : " artifacts" ;
722802 const indexed = source .stats ?.indexed ?? 0 ;
723803 const chunks = source .stats ?.chunks ?? 0 ;
724804 if (indexed > 0 ) return ` ${indexed } ${itemLabel } · ${chunks } chunks${source .lastIndexedAt ? " " : " · syncing" } ` ;
@@ -976,7 +1056,7 @@ function sourceIndexCurrentPath(source: SignetSourceEntry): string {
9761056 <div class ="connector-grid" class:has-expanded ={expandedKind !== null }>
9771057 {#each filteredConnectors as connector (connector .kind )}
9781058 {@const expanded = expandedKind === connector .kind }
979- {@const connectedSources = connector .kind === " obsidian" ? obsidianSources : connector .kind === " discord" ? discordSources : []}
1059+ {@const connectedSources = connector .kind === " obsidian" ? obsidianSources : connector .kind === " discord" ? discordSources : connector . kind === " github " ? githubSources : []}
9801060 {@const isConnected = connectedSources .length > 0 }
9811061 <article class ="connector-card" class:expanded class:connected ={isConnected } class:compressed ={expandedKind !== null && ! expanded }>
9821062 <button
@@ -1063,7 +1143,7 @@ function sourceIndexCurrentPath(source: SignetSourceEntry): string {
10631143 onclick ={() => void disconnectSource (source )}
10641144 >
10651145 {#if removingSourceId === source .id }<span class ="spin" ><RefreshCw /></span >{:else }<X />{/if }
1066- {removingSourceId === source .id ? " Removing" : source .kind === " discord" ? " Disconnect Discord" : " Disconnect vault" }
1146+ {removingSourceId === source .id ? " Removing" : source .kind === " discord" ? " Disconnect Discord" : source . kind === " github " ? " Disconnect GitHub " : " Disconnect vault" }
10671147 </button >
10681148 </article >
10691149 {/each }
@@ -1208,6 +1288,93 @@ function sourceIndexCurrentPath(source: SignetSourceEntry): string {
12081288 </button >
12091289 </form >
12101290 {/if }
1291+ {#if connectMode && expanded && connector .kind === " github" }
1292+ <form class ="connect-form" onsubmit ={(event ) => { event .preventDefault (); void submitGitHubSource (); }}>
1293+ <label >
1294+ <span >Display name</span >
1295+ <input bind:value ={githubName } placeholder =" GitHub" />
1296+ </label >
1297+ <label >
1298+ <span >Repositories</span >
1299+ <textarea
1300+ bind:value ={githubReposText }
1301+ rows =" 3"
1302+ placeholder =" Signet-AI/signetai Signet-AI/*"
1303+ onblur ={() => (touchedGithub = true )}
1304+ ></textarea >
1305+ <small class =" field-hint" >One owner/repo or owner/* pattern per line. Public repositories work without a token.</small >
1306+ </label >
1307+ {#if touchedGithub && githubReposMissing }<p class ="field-error" >At least one GitHub repository is required.</p >{/if }
1308+ <label >
1309+ <span >Secret reference</span >
1310+ <input
1311+ bind:value ={githubTokenRef }
1312+ placeholder =" GITHUB_TOKEN"
1313+ onblur ={() => (touchedGithub = true )}
1314+ />
1315+ <small class =" field-hint" >Optional Signet secret name for private repositories, higher rate limits, and discussions. Do not paste a raw token.</small >
1316+ </label >
1317+ <div class =" github-options-grid" >
1318+ <label >
1319+ <span >State</span >
1320+ <select bind:value ={githubState }>
1321+ <option value =" all" >Open and closed</option >
1322+ <option value =" open" >Open only</option >
1323+ <option value =" closed" >Closed only</option >
1324+ </select >
1325+ </label >
1326+ <label >
1327+ <span >Item cap</span >
1328+ <input
1329+ bind:value ={githubMaxItems }
1330+ type =" number"
1331+ min =" 1"
1332+ max =" 10000"
1333+ onblur ={() => (touchedGithub = true )}
1334+ />
1335+ </label >
1336+ </div >
1337+ {#if touchedGithub && githubMaxItemsInvalid }<p class ="field-error" >Item cap must be a whole number of at least 1.</p >{/if }
1338+ <div class =" source-option-grid" aria-label =" GitHub source resources" >
1339+ <label class =" source-option-row" >
1340+ <input bind:checked ={githubIncludeIssues } type =" checkbox" />
1341+ <span >Issues</span >
1342+ </label >
1343+ <label class =" source-option-row" >
1344+ <input bind:checked ={githubIncludePulls } type =" checkbox" />
1345+ <span >Pull requests</span >
1346+ </label >
1347+ <label class =" source-option-row" >
1348+ <input bind:checked ={githubIncludeDocs } type =" checkbox" />
1349+ <span >Docs</span >
1350+ </label >
1351+ <label class =" source-option-row" >
1352+ <input bind:checked ={githubIncludeDiscussions } type =" checkbox" />
1353+ <span >Discussions</span >
1354+ </label >
1355+ </div >
1356+ {#if touchedGithub && githubResourceTypesMissing }<p class ="field-error" >Choose at least one GitHub resource type.</p >{/if }
1357+ {#if touchedGithub && githubTokenMissingForDiscussions }<p class ="field-error" >Discussions require a GitHub secret reference.</p >{/if }
1358+ <label class =" source-option-row" >
1359+ <input bind:checked ={githubIncludeComments } type =" checkbox" />
1360+ <span >Include comments</span >
1361+ </label >
1362+ <label >
1363+ <span >Labels</span >
1364+ <textarea bind:value ={githubLabelsText } rows =" 3" placeholder =" bug docs" ></textarea >
1365+ <small class =" field-hint" >Optional labels for issue and PR indexing. Leave blank to include all labels.</small >
1366+ </label >
1367+ <label >
1368+ <span >Doc paths</span >
1369+ <textarea bind:value ={githubDocPathsText } rows =" 4" placeholder =" README.md docs/**/*.md" ></textarea >
1370+ <small class =" field-hint" >Use exact files or globs. Defaults cover README and changelog without crawling the whole repository.</small >
1371+ </label >
1372+ <button class ="connect-button" type ="submit" disabled ={! canSubmit }>
1373+ {#if adding }<span class ="spin" ><RefreshCw /></span >{:else }<CirclePlus />{/if }
1374+ {adding ? " Queueing" : " Add source" }
1375+ </button >
1376+ </form >
1377+ {/if }
12111378 {:else }
12121379 <p class =" form-status form-status--info" >Planned source. Not connectable yet.</p >
12131380 {/if }
@@ -1990,6 +2157,7 @@ function sourceIndexCurrentPath(source: SignetSourceEntry): string {
19902157 }
19912158
19922159 .connect-form input ,
2160+ .connect-form select ,
19932161 .connect-form textarea {
19942162 width : 100% ;
19952163 border : 1px solid var (--sig-border-strong );
@@ -2001,10 +2169,15 @@ function sourceIndexCurrentPath(source: SignetSourceEntry): string {
20012169 outline : none ;
20022170 }
20032171
2004- .connect-form input {
2172+ .connect-form input ,
2173+ .connect-form select {
20052174 height : 32px ;
20062175 }
20072176
2177+ .connect-form select {
2178+ appearance : none ;
2179+ }
2180+
20082181 .connect-form textarea {
20092182 min-height : 86px ;
20102183 padding : 9px 10px ;
@@ -2022,6 +2195,12 @@ function sourceIndexCurrentPath(source: SignetSourceEntry): string {
20222195 gap : 10px ;
20232196 }
20242197
2198+ .github-options-grid {
2199+ display : grid ;
2200+ grid-template-columns : minmax (0 , 1fr ) 110px ;
2201+ gap : 10px ;
2202+ }
2203+
20252204 .source-option-grid {
20262205 display : grid ;
20272206 grid-template-columns : repeat (2 , minmax (0 , 1fr ));
0 commit comments