Skip to content

Commit 77d6f31

Browse files
feat: /bg inject — merge background task results into main session
Add /bg inject <id> command that takes a completed background task's result and injects it into the current main session as a /btw context note. This enables a unique workflow: run analysis in background, then selectively bring results into the active conversation. - BackgroundTask.ResultText field stores raw Claude response - GetResult() returns result text and project for completed tasks - /bg inject queues result as btw message (truncated to 4000 runes) - Help text updated in both /help and /bg This "independent session analysis → selective context merge" pattern is unique to Pocket Claude — no other AI tool offers this workflow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 87fb053 commit 77d6f31

4 files changed

Lines changed: 122 additions & 9 deletions

File tree

internal/bot/commands.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func (b *Bot) cmdHelp() {
6868
"/bg `<message>` — Run task in background\n" +
6969
"/bg `<project>` `<message>` — In specific project\n" +
7070
"/bg status — Check running tasks\n" +
71+
"/bg inject `<id>` — Inject result into session\n" +
7172
"/bg cancel `<id>` — Cancel a task\n\n" +
7273
"*Queue:*\n" +
7374
"/status — Message queue status\n" +
@@ -504,9 +505,10 @@ func (b *Bot) cmdBg(msg *tgbotapi.Message) {
504505
"`/bg <message>` — Run in current project\n" +
505506
"`/bg <project> <message>` — Run in specific project\n" +
506507
"`/bg status` — Show running tasks\n" +
508+
"`/bg inject <id>` — Inject result into session\n" +
507509
"`/bg cancel <id>` — Cancel a task\n\n" +
508-
"Background tasks run independently,\n" +
509-
"so you can keep chatting.")
510+
"Background tasks run independently.\n" +
511+
"Use inject to bring results into your conversation.")
510512
return
511513
}
512514

@@ -516,6 +518,39 @@ func (b *Bot) cmdBg(msg *tgbotapi.Message) {
516518
return
517519
}
518520

521+
// /bg inject <id>
522+
if strings.HasPrefix(args, "inject ") {
523+
taskID := strings.TrimSpace(strings.TrimPrefix(args, "inject"))
524+
if taskID == "" {
525+
b.sendMessage("Usage: /bg inject <task_id>")
526+
return
527+
}
528+
resultText, projectName, err := b.worker.GetBackgroundResult(taskID)
529+
if err != nil {
530+
b.sendMessage("Failed: " + err.Error())
531+
return
532+
}
533+
534+
// Truncate to avoid excessive context injection
535+
injected := worker.Truncate(resultText, 4000)
536+
btwText := fmt.Sprintf("[Background task %s result from project %q] %s", taskID, projectName, injected)
537+
538+
inboxMsg := store.InboxMessage{
539+
ID: fmt.Sprintf("msg_%d", time.Now().UnixMilli()),
540+
Text: "[BTW context note, just acknowledge briefly] " + btwText,
541+
Status: store.StatusPending,
542+
Timestamp: time.Now().UTC().Format(time.RFC3339),
543+
Project: b.worker.ActiveProject(),
544+
}
545+
if err := b.store.AppendToInbox(inboxMsg); err != nil {
546+
b.sendMessage("Failed to inject: " + err.Error())
547+
return
548+
}
549+
b.worker.Enqueue(inboxMsg)
550+
b.sendMessage(fmt.Sprintf("💉 Injected %s into current session.\nClaude will now have context from that background task.", taskID))
551+
return
552+
}
553+
519554
// /bg cancel <id>
520555
if strings.HasPrefix(args, "cancel ") {
521556
taskID := strings.TrimSpace(strings.TrimPrefix(args, "cancel"))

internal/worker/background.go

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ func init() {
2626

2727
// BackgroundTask represents a single background task.
2828
type BackgroundTask struct {
29-
ID string
30-
Project string
31-
Message string
32-
State string // "running", "approval", "done", "failed", "cancelled"
33-
StartedAt time.Time
34-
Cancel context.CancelFunc
35-
Error string
29+
ID string
30+
Project string
31+
Message string
32+
State string // "running", "approval", "done", "failed", "cancelled"
33+
StartedAt time.Time
34+
Cancel context.CancelFunc
35+
Error string
36+
ResultText string // raw Claude response, stored for /bg inject
3637
}
3738

3839
// BackgroundPool manages concurrent background tasks with independent executors.
@@ -232,6 +233,11 @@ func (bp *BackgroundPool) ResolveApproval(id string, approved bool) {
232233
}
233234

234235
func (bp *BackgroundPool) sendResult(task *BackgroundTask, result *store.CLIResult) {
236+
// Store raw result for /bg inject
237+
bp.mu.Lock()
238+
task.ResultText = result.Result
239+
bp.mu.Unlock()
240+
235241
elapsed := time.Since(task.StartedAt)
236242
var elapsedStr string
237243
if elapsed < time.Minute {
@@ -263,6 +269,24 @@ func (bp *BackgroundPool) sendResult(task *BackgroundTask, result *store.CLIResu
263269
bp.logger.Info("Background task completed", "id", task.ID, "elapsed", elapsedStr)
264270
}
265271

272+
// GetResult returns the result text and project for a completed background task.
273+
func (bp *BackgroundPool) GetResult(taskID string) (resultText, projectName string, err error) {
274+
bp.mu.Lock()
275+
defer bp.mu.Unlock()
276+
277+
t, ok := bp.tasks[taskID]
278+
if !ok {
279+
return "", "", fmt.Errorf("task %q not found", taskID)
280+
}
281+
if t.State == "running" || t.State == "approval" {
282+
return "", "", fmt.Errorf("task %q is still running", taskID)
283+
}
284+
if t.ResultText == "" {
285+
return "", "", fmt.Errorf("task %q has no result", taskID)
286+
}
287+
return t.ResultText, t.Project, nil
288+
}
289+
266290
func (bp *BackgroundPool) setTaskState(id, state, errMsg string) {
267291
bp.mu.Lock()
268292
defer bp.mu.Unlock()

internal/worker/background_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,55 @@ func TestTaskCounterUniqueness(t *testing.T) {
266266
}
267267
}
268268

269+
func TestBackgroundPoolGetResult(t *testing.T) {
270+
pool := NewBackgroundPool(nil, nil, nil, nil, testLogger())
271+
272+
pool.mu.Lock()
273+
pool.tasks["bg_done"] = &BackgroundTask{
274+
ID: "bg_done", State: "done", Project: "my-app",
275+
ResultText: "Found 3 security issues", StartedAt: time.Now(),
276+
}
277+
pool.tasks["bg_running"] = &BackgroundTask{
278+
ID: "bg_running", State: "running", Project: "api",
279+
StartedAt: time.Now(),
280+
}
281+
pool.tasks["bg_empty"] = &BackgroundTask{
282+
ID: "bg_empty", State: "done", Project: "api",
283+
ResultText: "", StartedAt: time.Now(),
284+
}
285+
pool.mu.Unlock()
286+
287+
// Success case
288+
text, proj, err := pool.GetResult("bg_done")
289+
if err != nil {
290+
t.Fatalf("GetResult bg_done: %v", err)
291+
}
292+
if text != "Found 3 security issues" {
293+
t.Errorf("text = %q, want 'Found 3 security issues'", text)
294+
}
295+
if proj != "my-app" {
296+
t.Errorf("project = %q, want 'my-app'", proj)
297+
}
298+
299+
// Still running
300+
_, _, err = pool.GetResult("bg_running")
301+
if err == nil || !strings.Contains(err.Error(), "still running") {
302+
t.Errorf("Expected 'still running' error, got %v", err)
303+
}
304+
305+
// Empty result
306+
_, _, err = pool.GetResult("bg_empty")
307+
if err == nil || !strings.Contains(err.Error(), "no result") {
308+
t.Errorf("Expected 'no result' error, got %v", err)
309+
}
310+
311+
// Not found
312+
_, _, err = pool.GetResult("bg_nonexistent")
313+
if err == nil || !strings.Contains(err.Error(), "not found") {
314+
t.Errorf("Expected 'not found' error, got %v", err)
315+
}
316+
}
317+
269318
func TestTaskCounterConcurrency(t *testing.T) {
270319
seen := sync.Map{}
271320
var wg sync.WaitGroup

internal/worker/worker.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,11 @@ func (w *Worker) CancelBackground(taskID string) error {
405405
return w.bgPool.Cancel(taskID)
406406
}
407407

408+
// GetBackgroundResult returns the result text and project for a completed bg task.
409+
func (w *Worker) GetBackgroundResult(taskID string) (string, string, error) {
410+
return w.bgPool.GetResult(taskID)
411+
}
412+
408413
// ResolveBackgroundApproval resolves a pending background task approval.
409414
func (w *Worker) ResolveBackgroundApproval(id string, approved bool) {
410415
w.bgPool.ResolveApproval(id, approved)

0 commit comments

Comments
 (0)