mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
select workspace
This commit is contained in:
parent
36f700cc77
commit
6cd5abce48
7 changed files with 303 additions and 53 deletions
|
|
@ -15,7 +15,11 @@ import { bus } from '@x/core/dist/runs/bus.js';
|
||||||
import { serviceBus } from '@x/core/dist/services/service_bus.js';
|
import { serviceBus } from '@x/core/dist/services/service_bus.js';
|
||||||
import type { FSWatcher } from 'chokidar';
|
import type { FSWatcher } from 'chokidar';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
|
import { exec } from 'node:child_process';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
import { RunEvent } from '@x/shared/dist/runs.js';
|
import { RunEvent } from '@x/shared/dist/runs.js';
|
||||||
import { ServiceEvent } from '@x/shared/dist/service-events.js';
|
import { ServiceEvent } from '@x/shared/dist/service-events.js';
|
||||||
import container from '@x/core/dist/di/container.js';
|
import container from '@x/core/dist/di/container.js';
|
||||||
|
|
@ -397,13 +401,27 @@ export function setupIpcHandlers() {
|
||||||
'slack:getConfig': async () => {
|
'slack:getConfig': async () => {
|
||||||
const repo = container.resolve<ISlackConfigRepo>('slackConfigRepo');
|
const repo = container.resolve<ISlackConfigRepo>('slackConfigRepo');
|
||||||
const config = await repo.getConfig();
|
const config = await repo.getConfig();
|
||||||
return { enabled: config.enabled };
|
return { enabled: config.enabled, workspaces: config.workspaces };
|
||||||
},
|
},
|
||||||
'slack:setConfig': async (_event, args) => {
|
'slack:setConfig': async (_event, args) => {
|
||||||
const repo = container.resolve<ISlackConfigRepo>('slackConfigRepo');
|
const repo = container.resolve<ISlackConfigRepo>('slackConfigRepo');
|
||||||
await repo.setConfig({ enabled: args.enabled });
|
await repo.setConfig({ enabled: args.enabled, workspaces: args.workspaces });
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
'slack:listWorkspaces': async () => {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('agent-slack auth whoami', { timeout: 10000 });
|
||||||
|
const parsed = JSON.parse(stdout);
|
||||||
|
const workspaces = (parsed.workspaces || []).map((w: { workspace_url?: string; workspace_name?: string }) => ({
|
||||||
|
url: w.workspace_url || '',
|
||||||
|
name: w.workspace_name || '',
|
||||||
|
}));
|
||||||
|
return { workspaces };
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to list Slack workspaces';
|
||||||
|
return { workspaces: [], error: message };
|
||||||
|
}
|
||||||
|
},
|
||||||
'onboarding:getStatus': async () => {
|
'onboarding:getStatus': async () => {
|
||||||
// Show onboarding if it hasn't been completed yet
|
// Show onboarding if it hasn't been completed yet
|
||||||
const complete = isOnboardingComplete();
|
const complete = isOnboardingComplete();
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,12 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
|
||||||
// Slack state (agent-slack CLI)
|
// Slack state (agent-slack CLI)
|
||||||
const [slackEnabled, setSlackEnabled] = useState(false)
|
const [slackEnabled, setSlackEnabled] = useState(false)
|
||||||
const [slackLoading, setSlackLoading] = useState(true)
|
const [slackLoading, setSlackLoading] = useState(true)
|
||||||
|
const [slackWorkspaces, setSlackWorkspaces] = useState<Array<{ url: string; name: string }>>([])
|
||||||
|
const [slackAvailableWorkspaces, setSlackAvailableWorkspaces] = useState<Array<{ url: string; name: string }>>([])
|
||||||
|
const [slackSelectedUrls, setSlackSelectedUrls] = useState<Set<string>>(new Set())
|
||||||
|
const [slackPickerOpen, setSlackPickerOpen] = useState(false)
|
||||||
|
const [slackDiscovering, setSlackDiscovering] = useState(false)
|
||||||
|
const [slackDiscoverError, setSlackDiscoverError] = useState<string | null>(null)
|
||||||
|
|
||||||
// Load available providers on mount
|
// Load available providers on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -110,21 +116,67 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
|
||||||
setSlackLoading(true)
|
setSlackLoading(true)
|
||||||
const result = await window.ipc.invoke('slack:getConfig', null)
|
const result = await window.ipc.invoke('slack:getConfig', null)
|
||||||
setSlackEnabled(result.enabled)
|
setSlackEnabled(result.enabled)
|
||||||
|
setSlackWorkspaces(result.workspaces || [])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load Slack config:', error)
|
console.error('Failed to load Slack config:', error)
|
||||||
setSlackEnabled(false)
|
setSlackEnabled(false)
|
||||||
|
setSlackWorkspaces([])
|
||||||
} finally {
|
} finally {
|
||||||
setSlackLoading(false)
|
setSlackLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Update Slack config
|
// Enable Slack: discover workspaces
|
||||||
const handleSlackToggle = useCallback(async (enabled: boolean) => {
|
const handleSlackEnable = useCallback(async () => {
|
||||||
|
setSlackDiscovering(true)
|
||||||
|
setSlackDiscoverError(null)
|
||||||
|
try {
|
||||||
|
const result = await window.ipc.invoke('slack:listWorkspaces', null)
|
||||||
|
if (result.error || result.workspaces.length === 0) {
|
||||||
|
setSlackDiscoverError(result.error || 'No Slack workspaces found. Set up with: agent-slack auth import-desktop')
|
||||||
|
setSlackAvailableWorkspaces([])
|
||||||
|
setSlackPickerOpen(true)
|
||||||
|
} else {
|
||||||
|
setSlackAvailableWorkspaces(result.workspaces)
|
||||||
|
setSlackSelectedUrls(new Set(result.workspaces.map((w: { url: string }) => w.url)))
|
||||||
|
setSlackPickerOpen(true)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to discover Slack workspaces:', error)
|
||||||
|
setSlackDiscoverError('Failed to discover Slack workspaces')
|
||||||
|
setSlackPickerOpen(true)
|
||||||
|
} finally {
|
||||||
|
setSlackDiscovering(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Save selected Slack workspaces
|
||||||
|
const handleSlackSaveWorkspaces = useCallback(async () => {
|
||||||
|
const selected = slackAvailableWorkspaces.filter(w => slackSelectedUrls.has(w.url))
|
||||||
try {
|
try {
|
||||||
setSlackLoading(true)
|
setSlackLoading(true)
|
||||||
await window.ipc.invoke('slack:setConfig', { enabled })
|
await window.ipc.invoke('slack:setConfig', { enabled: true, workspaces: selected })
|
||||||
setSlackEnabled(enabled)
|
setSlackEnabled(true)
|
||||||
toast.success(enabled ? 'Slack enabled' : 'Slack disabled')
|
setSlackWorkspaces(selected)
|
||||||
|
setSlackPickerOpen(false)
|
||||||
|
toast.success('Slack enabled')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save Slack config:', error)
|
||||||
|
toast.error('Failed to save Slack settings')
|
||||||
|
} finally {
|
||||||
|
setSlackLoading(false)
|
||||||
|
}
|
||||||
|
}, [slackAvailableWorkspaces, slackSelectedUrls])
|
||||||
|
|
||||||
|
// Disable Slack
|
||||||
|
const handleSlackDisable = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setSlackLoading(true)
|
||||||
|
await window.ipc.invoke('slack:setConfig', { enabled: false, workspaces: [] })
|
||||||
|
setSlackEnabled(false)
|
||||||
|
setSlackWorkspaces([])
|
||||||
|
setSlackPickerOpen(false)
|
||||||
|
toast.success('Slack disabled')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update Slack config:', error)
|
console.error('Failed to update Slack config:', error)
|
||||||
toast.error('Failed to update Slack settings')
|
toast.error('Failed to update Slack settings')
|
||||||
|
|
@ -505,29 +557,85 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Slack */}
|
{/* Slack */}
|
||||||
<div className="flex items-center justify-between gap-3 rounded-md px-3 py-2 hover:bg-accent">
|
<div className="rounded-md px-3 py-2 hover:bg-accent">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
|
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
|
||||||
<MessageSquare className="size-4" />
|
<MessageSquare className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col min-w-0">
|
<div className="flex flex-col min-w-0">
|
||||||
<span className="text-sm font-medium truncate">Slack</span>
|
<span className="text-sm font-medium truncate">Slack</span>
|
||||||
|
{slackEnabled && slackWorkspaces.length > 0 ? (
|
||||||
|
<span className="text-xs text-muted-foreground truncate">
|
||||||
|
{slackWorkspaces.map(w => w.name).join(', ')}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<span className="text-xs text-muted-foreground truncate">
|
<span className="text-xs text-muted-foreground truncate">
|
||||||
Send messages and view channels
|
Send messages and view channels
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 flex items-center gap-2">
|
<div className="shrink-0 flex items-center gap-2">
|
||||||
{slackLoading && (
|
{(slackLoading || slackDiscovering) && (
|
||||||
<Loader2 className="size-3 animate-spin" />
|
<Loader2 className="size-3 animate-spin" />
|
||||||
)}
|
)}
|
||||||
|
{slackEnabled ? (
|
||||||
<Switch
|
<Switch
|
||||||
checked={slackEnabled}
|
checked={true}
|
||||||
onCheckedChange={handleSlackToggle}
|
onCheckedChange={() => handleSlackDisable()}
|
||||||
disabled={slackLoading}
|
disabled={slackLoading}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSlackEnable}
|
||||||
|
disabled={slackLoading || slackDiscovering}
|
||||||
|
className="h-7 px-2 text-xs"
|
||||||
|
>
|
||||||
|
Enable
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{slackPickerOpen && (
|
||||||
|
<div className="mt-2 ml-11 space-y-2">
|
||||||
|
{slackDiscoverError ? (
|
||||||
|
<p className="text-xs text-muted-foreground">{slackDiscoverError}</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{slackAvailableWorkspaces.map(w => (
|
||||||
|
<label key={w.url} className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={slackSelectedUrls.has(w.url)}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSlackSelectedUrls(prev => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (e.target.checked) next.add(w.url)
|
||||||
|
else next.delete(w.url)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className="rounded border-border"
|
||||||
|
/>
|
||||||
|
<span className="truncate">{w.name}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSlackSaveWorkspaces}
|
||||||
|
disabled={slackSelectedUrls.size === 0 || slackLoading}
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,12 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
||||||
// Slack state (agent-slack CLI)
|
// Slack state (agent-slack CLI)
|
||||||
const [slackEnabled, setSlackEnabled] = useState(false)
|
const [slackEnabled, setSlackEnabled] = useState(false)
|
||||||
const [slackLoading, setSlackLoading] = useState(true)
|
const [slackLoading, setSlackLoading] = useState(true)
|
||||||
|
const [slackWorkspaces, setSlackWorkspaces] = useState<Array<{ url: string; name: string }>>([])
|
||||||
|
const [slackAvailableWorkspaces, setSlackAvailableWorkspaces] = useState<Array<{ url: string; name: string }>>([])
|
||||||
|
const [slackSelectedUrls, setSlackSelectedUrls] = useState<Set<string>>(new Set())
|
||||||
|
const [slackPickerOpen, setSlackPickerOpen] = useState(false)
|
||||||
|
const [slackDiscovering, setSlackDiscovering] = useState(false)
|
||||||
|
const [slackDiscoverError, setSlackDiscoverError] = useState<string | null>(null)
|
||||||
|
|
||||||
const updateProviderConfig = useCallback(
|
const updateProviderConfig = useCallback(
|
||||||
(provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string }>) => {
|
(provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string }>) => {
|
||||||
|
|
@ -214,21 +220,67 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
||||||
setSlackLoading(true)
|
setSlackLoading(true)
|
||||||
const result = await window.ipc.invoke('slack:getConfig', null)
|
const result = await window.ipc.invoke('slack:getConfig', null)
|
||||||
setSlackEnabled(result.enabled)
|
setSlackEnabled(result.enabled)
|
||||||
|
setSlackWorkspaces(result.workspaces || [])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load Slack config:', error)
|
console.error('Failed to load Slack config:', error)
|
||||||
setSlackEnabled(false)
|
setSlackEnabled(false)
|
||||||
|
setSlackWorkspaces([])
|
||||||
} finally {
|
} finally {
|
||||||
setSlackLoading(false)
|
setSlackLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Update Slack config
|
// Enable Slack: discover workspaces
|
||||||
const handleSlackToggle = useCallback(async (enabled: boolean) => {
|
const handleSlackEnable = useCallback(async () => {
|
||||||
|
setSlackDiscovering(true)
|
||||||
|
setSlackDiscoverError(null)
|
||||||
|
try {
|
||||||
|
const result = await window.ipc.invoke('slack:listWorkspaces', null)
|
||||||
|
if (result.error || result.workspaces.length === 0) {
|
||||||
|
setSlackDiscoverError(result.error || 'No Slack workspaces found. Set up with: agent-slack auth import-desktop')
|
||||||
|
setSlackAvailableWorkspaces([])
|
||||||
|
setSlackPickerOpen(true)
|
||||||
|
} else {
|
||||||
|
setSlackAvailableWorkspaces(result.workspaces)
|
||||||
|
setSlackSelectedUrls(new Set(result.workspaces.map((w: { url: string }) => w.url)))
|
||||||
|
setSlackPickerOpen(true)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to discover Slack workspaces:', error)
|
||||||
|
setSlackDiscoverError('Failed to discover Slack workspaces')
|
||||||
|
setSlackPickerOpen(true)
|
||||||
|
} finally {
|
||||||
|
setSlackDiscovering(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Save selected Slack workspaces
|
||||||
|
const handleSlackSaveWorkspaces = useCallback(async () => {
|
||||||
|
const selected = slackAvailableWorkspaces.filter(w => slackSelectedUrls.has(w.url))
|
||||||
try {
|
try {
|
||||||
setSlackLoading(true)
|
setSlackLoading(true)
|
||||||
await window.ipc.invoke('slack:setConfig', { enabled })
|
await window.ipc.invoke('slack:setConfig', { enabled: true, workspaces: selected })
|
||||||
setSlackEnabled(enabled)
|
setSlackEnabled(true)
|
||||||
toast.success(enabled ? 'Slack enabled' : 'Slack disabled')
|
setSlackWorkspaces(selected)
|
||||||
|
setSlackPickerOpen(false)
|
||||||
|
toast.success('Slack enabled')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save Slack config:', error)
|
||||||
|
toast.error('Failed to save Slack settings')
|
||||||
|
} finally {
|
||||||
|
setSlackLoading(false)
|
||||||
|
}
|
||||||
|
}, [slackAvailableWorkspaces, slackSelectedUrls])
|
||||||
|
|
||||||
|
// Disable Slack
|
||||||
|
const handleSlackDisable = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setSlackLoading(true)
|
||||||
|
await window.ipc.invoke('slack:setConfig', { enabled: false, workspaces: [] })
|
||||||
|
setSlackEnabled(false)
|
||||||
|
setSlackWorkspaces([])
|
||||||
|
setSlackPickerOpen(false)
|
||||||
|
toast.success('Slack disabled')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update Slack config:', error)
|
console.error('Failed to update Slack config:', error)
|
||||||
toast.error('Failed to update Slack settings')
|
toast.error('Failed to update Slack settings')
|
||||||
|
|
@ -492,29 +544,83 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
||||||
|
|
||||||
// Render Slack row
|
// Render Slack row
|
||||||
const renderSlackRow = () => (
|
const renderSlackRow = () => (
|
||||||
<div className="flex items-center justify-between gap-3 rounded-md px-3 py-3 hover:bg-accent">
|
<div className="rounded-md px-3 py-3 hover:bg-accent">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<div className="flex size-10 items-center justify-center rounded-md bg-muted">
|
<div className="flex size-10 items-center justify-center rounded-md bg-muted">
|
||||||
<MessageSquare className="size-5" />
|
<MessageSquare className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col min-w-0">
|
<div className="flex flex-col min-w-0">
|
||||||
<span className="text-sm font-medium truncate">Slack</span>
|
<span className="text-sm font-medium truncate">Slack</span>
|
||||||
|
{slackEnabled && slackWorkspaces.length > 0 ? (
|
||||||
|
<span className="text-xs text-muted-foreground truncate">
|
||||||
|
{slackWorkspaces.map(w => w.name).join(', ')}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<span className="text-xs text-muted-foreground truncate">
|
<span className="text-xs text-muted-foreground truncate">
|
||||||
Send messages and view channels
|
Send messages and view channels
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 flex items-center gap-2">
|
<div className="shrink-0 flex items-center gap-2">
|
||||||
{slackLoading && (
|
{(slackLoading || slackDiscovering) && (
|
||||||
<Loader2 className="size-3 animate-spin" />
|
<Loader2 className="size-3 animate-spin" />
|
||||||
)}
|
)}
|
||||||
|
{slackEnabled ? (
|
||||||
<Switch
|
<Switch
|
||||||
checked={slackEnabled}
|
checked={true}
|
||||||
onCheckedChange={handleSlackToggle}
|
onCheckedChange={() => handleSlackDisable()}
|
||||||
disabled={slackLoading}
|
disabled={slackLoading}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSlackEnable}
|
||||||
|
disabled={slackLoading || slackDiscovering}
|
||||||
|
>
|
||||||
|
Enable
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{slackPickerOpen && (
|
||||||
|
<div className="mt-2 ml-13 space-y-2">
|
||||||
|
{slackDiscoverError ? (
|
||||||
|
<p className="text-xs text-muted-foreground">{slackDiscoverError}</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{slackAvailableWorkspaces.map(w => (
|
||||||
|
<label key={w.url} className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={slackSelectedUrls.has(w.url)}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSlackSelectedUrls(prev => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (e.target.checked) next.add(w.url)
|
||||||
|
else next.delete(w.url)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className="rounded border-border"
|
||||||
|
/>
|
||||||
|
<span className="truncate">{w.name}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSlackSaveWorkspaces}
|
||||||
|
disabled={slackSelectedUrls.size === 0 || slackLoading}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
// Step 0: LLM Setup
|
// Step 0: LLM Setup
|
||||||
|
|
|
||||||
|
|
@ -83,13 +83,15 @@ agent-slack canvas get F01234567 --workspace https://team.slack.com
|
||||||
|
|
||||||
## 3. Multi-Workspace
|
## 3. Multi-Workspace
|
||||||
|
|
||||||
If the user has multiple workspaces configured, use \`--workspace <url>\` to disambiguate:
|
**Important:** The user has chosen which workspaces to use. Before your first Slack operation, read \`~/.rowboat/config/slack.json\` to see the selected workspaces. Only interact with workspaces listed in that config — ignore any other authenticated workspaces.
|
||||||
|
|
||||||
|
If the selected workspace list contains multiple entries, use \`--workspace <url>\` to disambiguate:
|
||||||
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
agent-slack message list "#general" --workspace https://team.slack.com
|
agent-slack message list "#general" --workspace https://team.slack.com
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Use \`agent-slack auth whoami\` to see all configured workspaces.
|
If only one workspace is selected, always use \`--workspace\` with its URL to avoid ambiguity with other authenticated workspaces.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export interface ISlackConfigRepo {
|
||||||
|
|
||||||
export class FSSlackConfigRepo implements ISlackConfigRepo {
|
export class FSSlackConfigRepo implements ISlackConfigRepo {
|
||||||
private readonly configPath = path.join(WorkDir, 'config', 'slack.json');
|
private readonly configPath = path.join(WorkDir, 'config', 'slack.json');
|
||||||
private readonly defaultConfig: SlackConfig = { enabled: false };
|
private readonly defaultConfig: SlackConfig = { enabled: false, workspaces: [] };
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.ensureConfigFile();
|
this.ensureConfigFile();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
|
export const SlackWorkspace = z.object({
|
||||||
|
url: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
export type SlackWorkspace = z.infer<typeof SlackWorkspace>;
|
||||||
|
|
||||||
export const SlackConfig = z.object({
|
export const SlackConfig = z.object({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
|
workspaces: z.array(SlackWorkspace).default([]),
|
||||||
});
|
});
|
||||||
export type SlackConfig = z.infer<typeof SlackConfig>;
|
export type SlackConfig = z.infer<typeof SlackConfig>;
|
||||||
|
|
|
||||||
|
|
@ -274,16 +274,25 @@ const ipcSchemas = {
|
||||||
req: z.null(),
|
req: z.null(),
|
||||||
res: z.object({
|
res: z.object({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
|
workspaces: z.array(z.object({ url: z.string(), name: z.string() })),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
'slack:setConfig': {
|
'slack:setConfig': {
|
||||||
req: z.object({
|
req: z.object({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
|
workspaces: z.array(z.object({ url: z.string(), name: z.string() })),
|
||||||
}),
|
}),
|
||||||
res: z.object({
|
res: z.object({
|
||||||
success: z.literal(true),
|
success: z.literal(true),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
'slack:listWorkspaces': {
|
||||||
|
req: z.null(),
|
||||||
|
res: z.object({
|
||||||
|
workspaces: z.array(z.object({ url: z.string(), name: z.string() })),
|
||||||
|
error: z.string().optional(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
'onboarding:getStatus': {
|
'onboarding:getStatus': {
|
||||||
req: z.null(),
|
req: z.null(),
|
||||||
res: z.object({
|
res: z.object({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue